Skip to content

Information Feed & RSS

vita has two distinct "feed" systems that share a code namespace (scripts/feed/) but serve different audiences:

  1. The inter-agent feed / RSS+JSON digest β€” a token-protected daily narrative about the vita agent system itself, published at vita-rss.ham.xyz for other people building AI agents. It deliberately filters out all personal-health and work content.
  2. Feed v1, the in-app social feed β€” a ranked, threaded, social-style feed inside the React/iOS UI. Pluggable producers emit candidates from internal signals; an orchestrator promotes and composes them into items; a multi-stage ranker orders the slate; and bio-zack's reactions feed a preference-learning loop that reshapes future output.

This page documents the mechanism of both. It never shows feed content β€” the in-app feed contains personal material by design, and the inter-agent feed is defined precisely by what it leaves out.

Note: The feed system co-exists with two sibling systems documented elsewhere: the Media Signal Refinery (external article/podcast ingestion) and the Living Briefing (real-time dashboard). They are separate pipelines.


Part 1 β€” The Inter-Agent Feed (RSS + JSON)

What it is

A daily narrative digest written for other agent builders. Each entry is a short markdown narrative describing what happened inside the vita agent system that day β€” architectural decisions, failure modes and recoveries, emergent behaviors, forge (idea-workshop) experiments, hardware integrations, and how the human-AI relationship is evolving. It is served as both JSON Feed 1.1 and RSS 2.0, behind a bearer token, on a dedicated domain.

Generation pipeline (daily, 7am PT)

The feed_narrative loop runs once a day and walks three stages:

collect_daily_events(date)        # scripts/feed/collect.py
        β”‚   gathers events from overnight /sleep summary, tagged stream
        β”‚   captures (file-this / observation only), daily scratch notes,
        β”‚   and forge ideas modified today
        β–Ό
privacy.filter_events(events)     # scripts/feed/privacy.py  (PASS 1, pre-LLM)
        β”‚   drops any event whose source ∈ BLOCKED_SOURCES
        β”‚   {garmin, cgm, sleep, health, vault, medical}
        β–Ό
narrative.generate_narrative()    # scripts/feed/narrative.py
        β”‚   LLM synthesizes a 3-layer narrative (thread / moves / edge)
        β”‚   with novelty detection vs the last 3 days; emits {"skip": true}
        β”‚   for thin or already-covered days
        β–Ό
privacy.edit_narrative(narrative) # scripts/feed/privacy.py  (PASS 2, post-LLM)
        β”‚   adversarial LLM "privacy editor" rewrites anything that crosses
        β”‚   privacy boundaries (no [redacted] tags β€” natural rewrite)
        β–Ό
data/feed/narratives/YYYY-MM-DD.json

The narrative model is structured output: {thread, narrative_md, discussion_items[], topics[], novelty_score, structure_used[]}. Days with fewer than 2 events, or where the LLM judges nothing new, are stored as a thin-day entry with "skipped": true and are excluded from the served feed.

The two-pass privacy filter is the whole point. Pass 1 is a hard structural drop by source type (no regex content matching). Pass 2 is an LLM editor with an explicit "dinner-party test" prompt β€” it removes medical conditions/medications, named people in the private sphere, employer internals, financial details, and the substance of private conversations, while never touching the terms bio-zack, silicon-zack, sleep gate, activity mentions, or slack (as a data-source label). The narrator prompt also instructs the model to skip workplace content entirely before it ever reaches the editor.

Note: The narrator and privacy editor are LLM calls. The exact model is a deployment detail and may rotate β€” see Model Guidance rather than hard-coding a model id from this page.

Serving the feed

The router (api/src/routers/feed.py) loads recent non-skipped narratives newest-first and renders them. A token from data/feed/config.json gates the per-day endpoints; an unknown token returns 404 (not 403, so the URL space is opaque). The dedicated host vita-rss.ham.xyz is whitelisted in the API's domain guard (api/src/middleware/domain_guard.py) and gets clean root-level shortcut routes registered directly on the app.

Endpoint Auth Purpose
GET /api/v1/feed/{token}/feed.json token JSON Feed 1.1 β€” primary agent format
GET /api/v1/feed/{token}/feed.xml token RSS 2.0 mirror for standard readers
GET /{token}/feed.json token Root shortcut (clean URL on vita-rss.ham.xyz)
GET /{token}/feed.xml token Root shortcut RSS
GET /api/v1/feed/narratives none (internal) List recent narrative entries
GET /api/v1/feed/feed-url none (internal) Returns the subscription URLs + token

The JSON Feed embeds a _vita extension object per item carrying thread, discussion_items, novelty_score, word_count, and sources_used.

config.json holds {token, feed_title, feed_description, author, email, max_items} (max_items defaults to 30). The token is a secret β€” never commit or print it.

Running the public tunnel

./run rss-tunnel start     # ngrok tunnel β†’ vita-rss.ham.xyz
./run rss-tunnel status
./run rss-tunnel log

Like every vita service, the feed tunnel is managed through ./run, never systemd.


Part 2 β€” Feed v1: the in-app social feed

A full social-style feed for the iOS/React app. The data model and pipeline live in scripts/feed/; the HTTP surface is the M7 section of api/src/routers/feed.py; state is a single SQLite database at data/feed/feed.db.

PII: the in-app feed surfaces personal, health, and relationship signals on purpose β€” that is its job for bio-zack. This page documents only the engine. Never reproduce feed item bodies, comments, or producer payloads.

Pipeline at a glance

PRODUCERS ──► candidates ──► ORCHESTRATOR ──► feed_items ──► RANKER ──► ordered slate
(every 15m)   (feed.db)      (every 15m+5)   (feed.db)      (every 15m+7)
                                  β”‚                              β”‚
   bio-zack reacts ◄──────────────┴──── iOS / React UI β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚   like / bookmark / comment / reply / dismiss
        β–Ό
   CLASSIFIER ──► routes ──► preferences Β· research tasks Β· proposals (M6) Β· dialogue Β· dismiss
        β”‚
        └──► PREFERENCE LEARNING + CONSTITUTIONS reshape future producer/composer output

The data model (scripts/feed/models.py)

Pydantic v2 models, each with from_row() / to_db_dict() for the SQLite layer. The core lifecycle is candidate β†’ item β†’ interaction.

Model Role
FeedCandidate Raw producer output, pending promotion. Carries item_kind, stakes, base_decay_lambda (default 0.02), deadline_at, reversible, composite_inputs, status.
FeedItem Promoted, user-visible item. Adds voice_agent_id, rank_score, last_rationale, is_daily_anchor, is_red_tier, is_friction_required, and threading (parent_thread_id, thread_position, thread_total).
FeedInteraction A bio-zack reaction: view, like, bookmark, comment, dismiss, expand, tap_to_chat. Comments/dismisses carry the classifier verdict.
Preference A learned ranking/style rule (M8).
Constitution / ConstitutionAmendment Per-agent governance docs + proposed amendments.
WorkingContext A daily snapshot of "what bio-zack cares about now", fed to composer + ranker.
EpisodicEvent / ArchivalChunk Append-only memory log of feed system events (ranking runs, classifications, amendments).
ItemIntent / ItemOutcome For action-type items: what was intended, and whether it happened.
ShadowCandidate / ComposerSkip / ComposerError M11a A/B shadow path + composer telemetry.

Enums that matter for reimplementation:

Enum Values
ItemKind awareness, research, action, reflection, composite, debate, proposal, system_update
Stakes low, med, high
CandidateStatus pending, promoted, merged, dropped, composing, composed, skipped_by_composer, composer_failed
ClassifierType preference_rule, research_task, feature_request, dialogue, dismiss, observation, command, reflection, data
RuleType downweight, upweight, topic_filter, producer_pref, style
PreferenceStatus pending_review, active, rejected, reverted
ConstitutionKind epistemic_shared, stylistic_per_agent
AmendmentStatus pending, approved, rejected, reverted

Producers (scripts/feed/producers/)

Producers implement a tiny Producer protocol (producer_id, voice_agent_id, produce_candidates() -> list[FeedCandidate]). The runner iterates the PRODUCERS registry, calls each, and inserts candidates into feed.db. A producer exception is isolated (returns -1 in the results dict) and never blocks the others. Per-producer cursor state is persisted under data/feed/producer_state/ so each producer only emits new material; scripts/feed/backfills/ seeds history.

producer_id Source Voice
coach Coaching prescriptions coach
silicon-zack:sleep_reflection Overnight sleep reflection silicon-zack
silicon-zack:sentinel_anomaly Sentinel anomalies silicon-zack
silicon-zack:heartbeat_topic Heartbeat discussion topics silicon-zack
silicon-zack:identity_observation Identity observations silicon-zack
silicon-zack:x_bookmark Saved X/Twitter bookmarks silicon-zack
silicon-zack:hn Hacker News items silicon-zack

make_candidate() (in producers/base.py) stamps each candidate's metadata.voice_agent_id so the orchestrator can route it to the right composer voice at promotion time.

Orchestrator (scripts/feed/orchestrator/core.py)

Runs every tick (feed_orchestrator, every 15 min, +5 offset). One tick:

  1. Pull up to 200 pending candidates.
  2. Cluster them (clustering.py). Singletons go down the promotion path; multi-member clusters become composites via synthesis.py (capped per tick by FEED_SYNTHESIS_CAP, default 5; leftovers stay pending for the next tick).
  3. RED-tier detection (detect_red_tier): a candidate is RED-tier when stakes=high AND its deadline_at is within 24h, or when metadata.severity == "critical". RED-tier items are flagged for urgent surfacing.
  4. Promote each singleton through one of three modes, resolved per producer by composer/migration.py:
  5. legacy β€” promote with the candidate's own template body.
  6. composer β€” call the per-voice composer (Claude) to write the item body; a composer Skip is authoritative (the item is dropped, not legacy-fallback'd). Composer exceptions fall back to legacy.
  7. shadow β€” emit the legacy item and side-write a ShadowCandidate so the new composer output can be A/B compared against the legacy path without affecting the live feed.
  8. Batched composing (default on, batched_composer_enabled in data/feed/composer/config.json): singletons are grouped by voice and composed in chunks (max_batch_size_per_voice, default 15) via composer/batch.py to cut LLM round-trips.
  9. Daily anchor β€” once per day, anchor.py selects a "hero" card surfaced at the top of the feed.
  10. Reap candidates stuck in composing for more than 5 minutes.

Composer voices and their prompts live in scripts/feed/composer/prompts/ (voice_silicon-zack.md, voice_coach.md, a shared system prompt, a critic prompt, etc.). The daily working context (feed_working_context, 4am PT) regenerates the snapshot that grounds both composer and ranker in what bio-zack currently cares about.

Ranker (scripts/feed/ranker/core.py)

rank_feed() runs every tick (feed_ranker, every 15 min, +7 offset) as a multi-stage pipeline:

  1. Pre-filter (prefilter.py) β€” a fast, LLM-free weighted score over all rankable items:
score = 0.4·recency_decay        # exp(-λ·hours_since), λ = base_decay_lambda
      + 0.2Β·producer_prior       # Beta-Bernoulli cold-start prior, default 0.5
      + 0.2Β·topic_match          # keyword overlap with working-context topics
      + 0.1Β·deadline_multiplier  # 1.0 evergreen β†’ up to 3.0 as deadline nears
      + 0.1Β·stakes_boost         # low 0.0 / med 0.05 / high 0.15
  1. LLM rerank (llm_rerank.py) β€” listwise rerank of the top 20 pre-filtered items using ranker/prompts/listwise_rerank.md and the built ranker context. On any failure it falls back to pre-filter ordering (mode = "prefilter_fallback").
  2. Slate diversity (slate_diversity.py) β€” a sliding-window constraint: at most 2 items from the same voice_agent_id per window of 5.
  3. Persist β€” LLM-reranked items get a score offset by +100 to sit above pre-filter scores; the rest keep their pre-filter score. The run is logged as an EpisodicEvent (feed_rank) with items_scored, top_5_ids, cost_usd, mode.

No engagement-surveillance signals. The pre-filter explicitly bans dwell_ms, scroll_velocity, scroll_depth, session_length, etc. β€” a defense-in-depth check (_validate_no_banned_signals) raises if any leak into the feature dict. The ranker learns from explicit reactions and producer priors, not from passively measured attention.

Cold-start priors (ranker/cold_start.py)

Producer priors bootstrap from existing engagement data so a brand-new producer is not scored blind. Engagement is mined from heartbeat history and the proactive message queue, then turned into a Beta-Bernoulli posterior mean:

alpha = engaged + 1
beta  = ignored + 1
prior = alpha / (alpha + beta)     # 0.5 for an unknown producer

Priors are written atomically to data/feed/producer_priors.json and retrained weekly by the feed_cold_start_retrain loop (Sunday 3am PT). The current keyspace is {silicon-zack, coach, doctor}.

Interactions and the comment classifier

The UI posts reactions to the item endpoints. Likes and bookmarks are idempotent per (item_id, session_id). Comments and replies are two different primitives:

  • A comment is a FeedInteraction(type="comment") attached to one item. It is the conversational surface: bio-zack comments, and silicon-zack answers in the same chronological discussion.
  • A reply creates a new FeedItem parented to the thread root and bumps thread_total across the item thread. It is content-level feed threading, not the comment conversation.

Posting a comment returns as soon as the interaction is stored. The API then schedules classify_and_route() as a process-local asyncio task (or a daemon thread when there is no running event loop):

POST /items/{id}/comment
        β”‚
        β”œβ”€ store bio-zack COMMENT interaction
        └─ background classify_and_route(interaction_id)
              β”‚
              β”œβ”€ classify for learning/action routing
              β”‚    (preference, research, feature request, dismiss)
              └─ always run the full-agent reply path
                    β”‚
                    β”œβ”€ store silicon-zack COMMENT + episodic event
                    └─ enqueue Telegram desk notification + permalink

The classifier (scripts/feed/classifier/core.py) uses the Claude daemon with read-only tools and prompts/classify_comment.md. Per-type confidence thresholds remap uncertain results to the dialogue fallback; the verdict is saved on bio-zack's interaction and routed through scripts/feed/classifier/routes/:

Classified as Route Effect
preference_rule routes/preference_rule.py Extract durable ranking/style rules (M8).
research_task routes/research_task.py Spawn a research task.
feature_request (M6 pipeline) Auto-commission a proposal report (below).
dialogue No learning side effect The universal reply step below owns the conversation.
dismiss routes/dismiss.py Increment metadata.dismiss_count.

Classification and conversation are deliberately independent. Every bio-zack comment gets a reply attempt regardless of its classification, including when classification fails. routes/dialogue.py reconstructs the full discussion from the post body plus chronological comment interactions, then invokes a fresh full agent through the Claude daemon. The reply agent has the normal toolset and autonomy: a remark can receive a short answer, while a real request can research, run commands, or make proportionate changes. Its 600-second timeout is independent of the 60-second read-only classifier.

A successful answer is another COMMENT interaction with metadata.is_agent_reply=true, voice_agent_id, and in_response_to_interaction_id. It also records a classifier.dialogue_reply episodic event. Agent-authored interactions are guarded from re-entry so a reply cannot trigger an agent-to-agent loop. The per-item ceiling is 12 silicon-zack replies; after that, the route records TAP_TO_CHAT with reason=dialogue_cap_reached instead of calling another agent.

The web discussion renders bio-zack and silicon-zack comments chronologically and shows silicon-zack is thinking… while the latest visible comment is bio-zack's. A successful reply also enqueues a feed_agent_reply Telegram notification with a deduplicated link to /feed/post/<item-id>. That notification is a convenience: admission rejection does not roll back the reply already stored in feed.db.

Bio-zack can also author a top-level post (POST /api/v1/feed/posts), which is classified by classify_post against prompts/classify_post.md.

/feed/post/<item-id> is the dedicated web page for one post, its feed-item thread, and its comment discussion. It auto-focuses the comment composer and exposes copy id for build. The copied value is the bare item UUID, which any Vita agent can resolve:

./run feed-show <full-id-or-unique-prefix>

scripts/feed/show_item.py prints the post, feed-item children, and chronological comment discussion as Markdown. Exact IDs win; a prefix must resolve to exactly one of the ten newest matches or the command exits with the candidate list. This is the bridge from a lightweight feed discussion to a dedicated build session: paste the ID and say "let's work on this feed item."

Failure and recovery behavior

Failure What remains true Recovery
Classifier daemon/JSON/routing failure The bio-zack comment is durable; the universal reply is still attempted. Inspect api.log; learning side effects can be replayed only with an explicit classifier invocation.
Full-agent failure, timeout, or empty result The comment remains visible, but no agent reply or Telegram notification is stored. Comment again or explicitly rerun classify_and_route() for the interaction; there is no durable retry queue.
Telegram enqueue is rejected or delivery later fails The feed reply and episodic event remain durable. Open the permalink directly; queue diagnostics live in the proactive system.
API restarts after storing the comment The process-local background task can be lost. Re-run classification for the interaction or add another comment.

The dedicated page invalidates its item query immediately after a comment, but it does not poll continuously for the asynchronous reply. Window focus, another query refresh, or the Telegram permalink reveals the completed answer; until then the visible thinking… state can linger.

Preference learning (M8)

When a comment is classified as preference_rule, handle_preference_rule() extracts one or more Preference rows. Each rule has a rule_type (downweight/upweight/topic_filter/producer_pref/style), a rule_target, and a weight. A high-confidence verdict (>= 0.95) activates the preference immediately; otherwise it lands in pending_review for bio-zack to approve. Preferences move through a small state machine, enforced server-side (_ALLOWED_PREFERENCE_TRANSITIONS):

pending_review β†’ active | rejected
active         β†’ reverted
rejected       β†’ active
reverted       β†’ active

Active preferences flow back into ranking (via producer priors / topic filters) and into composer style.

Constitutions (scripts/feed/constitutions/)

Constitutions are governance documents that shape how each voice writes:

  • Epistemic (shared) β€” kind = epistemic_shared. Immutable in the DB; amendments are rejected with epistemic_immutable. To change it, edit the code/seed.
  • Stylistic (per-agent) β€” kind = stylistic_per_agent, one per voice. These are amendable.

They are idempotently seeded from constitutions/seed/*.md (epistemic_shared.md, stylistic_silicon-zack.md, stylistic_coach.md, stylistic_doctor.md, stylistic_arbiter.md). The extractor.py proposes amendments from interaction evidence; approving an amendment (approve_amendment) deactivates the current row, inserts a new version with the proposed rule appended to the body, bumps version, and logs a constitution.amended episodic event. Amendments carry the evidence interaction IDs so the UI can show the source quotes that justified each proposed change.

Proposal commission + commit watcher (M6)

This closes a request β†’ proposal β†’ shipped loop entirely inside the feed:

  1. Commission (feed_proposal_commission, every 5 min): for each pending feature_request, proposals/core.py marks it generating, calls an LLM (proposals/generator.py + prompts/proposal_generator.md) to write a proposal report into data/reports/, inserts a PROPOSAL feed item threaded to the original request, and marks it generated. Terminal states (generating/generated/shipped/dismissed/failed) prevent re-processing.
  2. Commit watcher (feed_commit_watcher, every 5 min): commit_watcher.py scans git for commits whose message matches feed-proposal: <slug>, finds the matching PROPOSAL item by metadata.proposal_slug, and emits a SYSTEM_UPDATE item threaded back to the original feature request β€” so the requester sees the shipped notice in the same thread. State (last scanned HEAD sha) lives at data/feed/proposals/commit_watcher_state.json; cold start scans ~14 days back.

So to "ship" a feed-requested feature, the committer simply tags the commit:

git commit -m "feed: add dark-mode anchor card

feed-proposal: dark-mode-anchor"

Daily digest + derived signals (M11)

  • Daily digest (composer/digest.py, feed_daily_digest, 5pm PT) summarizes the day's feed activity for bio-zack.
  • Derived signals (derived_signals.py, M11l) surface bio-zack's recent feed reactions, active preferences, pending proposals, the latest digest, and per-voice composer pulse counts into the Executive Loop's orient phase. This module lives outside scripts/feed/composer/ on purpose: an AST guard test (tests/test_feed_composer_no_engagement_in_prompt.py) forbids the composer prompt path from importing engagement symbols, so keeping derived signals out of that namespace makes any accidental coupling fail the test immediately.

Push notifications (M10)

The iOS app registers an APNs device token; the dispatcher (scripts/feed/push/, apns.py + dispatcher.py) sends pushes (e.g. for RED-tier items via red_tier.py) and tracks per-device failure_count.

Endpoint Purpose
POST /api/v1/feed/push/register Register/refresh an APNs token (idempotent on token)
POST /api/v1/feed/push/unregister Disable a token (preserves dispatch-log FK)
GET /api/v1/feed/push/devices Debug: list active push devices

In-app HTTP surface (M7)

Endpoint Purpose
GET /api/v1/feed/items List ranked items, anchor-first, cursor paginated
GET /api/v1/feed/items/{id} Item + thread + interactions
GET /api/v1/feed/items/{id}/thread Just the thread children
GET /api/v1/feed/anchor Today's daily anchor (falls back to most recent)
POST /api/v1/feed/items/{id}/like Β· /bookmark Β· /comment Β· /reply Β· /dismiss Reactions
POST /api/v1/feed/posts Bio-zack authored post
GET /api/v1/feed/preferences Β· PATCH /api/v1/feed/preferences/{id} Β· DELETE … Manage learned preferences
GET /api/v1/feed/constitutions/active List active constitutions
GET /api/v1/feed/constitutions/amendments?status=… List amendments by status
POST /api/v1/feed/constitutions/amendments/{id}/approve Β· /reject Resolve amendments
GET /api/v1/feed/shadow Β· GET /api/v1/feed/shadow/stats M11a shadow A/B viewer (admin)

All in-app endpoints run on the main API (./run api, port 33800); the public read-only mirror is on port 33802; the Vite dev UI on 33801.

The corresponding discussion page is a web route, GET /feed/post/{item_id}, served by the Vita UI rather than the /api/v1/feed router.


Scheduled loops

All feed loops are registered in api/src/loop_engine/registry.py and executed via api/src/services/scheduler_feed.py. They are in-process scheduler loops, not cron.

Loop Cadence What it does
feed_producers Every 15 min Run all producers, insert candidates
feed_orchestrator Every 15 min (+5 offset) Cluster, promote, compose, anchor
feed_ranker Every 15 min (+7 offset) Pre-filter β†’ LLM rerank β†’ slate diversity
feed_working_context 4am PT daily Regenerate the working-context snapshot
feed_narrative 7am PT daily Generate + privacy-filter the inter-agent digest
feed_daily_digest 5pm PT daily Summarize the day's feed activity
feed_cold_start_retrain Sunday 3am PT Recompute producer priors
feed_proposal_commission Every 5 min Commission proposals for feature requests
feed_commit_watcher Every 5 min Emit system-update items for shipped proposals

File Locations

Path Contents
api/src/routers/feed.py RSS/JSON endpoints + M7 in-app API
api/src/schemas/feed_v1.py Pydantic request/response schemas for M7
api/src/loop_engine/registry.py Loop registrations + cadences
api/src/services/scheduler_feed.py Loop execution helpers
scripts/feed/models.py Pydantic models + enums (candidate/item/interaction/…)
scripts/feed/db.py SQLite data-access layer
scripts/feed/collect.py Β· narrative.py Β· privacy.py Inter-agent digest pipeline
scripts/feed/producers/ Producer registry + per-producer modules
scripts/feed/orchestrator/ Promotion, clustering, synthesis, anchor
scripts/feed/composer/ Per-voice LLM composers, batch, digest, drift audit
scripts/feed/ranker/ Pre-filter, LLM rerank, cold-start, slate diversity
scripts/feed/classifier/core.py Read-only classification, learning/action routing, universal reply orchestration
scripts/feed/classifier/routes/dialogue.py Full-agent reply, 12-exchange cap, episodic event, Telegram permalink notification
scripts/feed/show_item.py ./run feed-show build-handoff resolver and Markdown renderer
scripts/feed/constitutions/ Constitution bootstrap, extractor, seeds
scripts/feed/proposals/ M6 commission + commit watcher
scripts/feed/push/ APNs registration + dispatch
scripts/feed/derived_signals.py M11l signals for the executive loop
data/feed/feed.db All in-app feed state (SQLite)
data/feed/config.json Inter-agent feed token + metadata (secret)
data/feed/narratives/YYYY-MM-DD.json Daily narrative entries
data/feed/producer_state/ Per-producer cursor state
data/feed/producer_priors.json Cold-start ranking priors
data/feed/composer/config.json Batched-composer config
data/feed/proposals/commit_watcher_state.json Last-scanned git sha
web/src/components/feed/CommentThread.tsx Chronological bio-zack/silicon-zack discussion UI
web/src/components/feed/CopyItemId.tsx Bare-ID clipboard handoff
web/src/routes/feed/post/$itemId.tsx Dedicated post permalink page

Known limitations

  1. Comment classification and agent replies use process-local background work, not a durable job queue. A restart in the narrow post-insert/pre-reply window requires a manual replay or another comment.
  2. Each reply is a fresh daemon invocation whose prompt reconstructs the discussion; there is no long-lived agent session behind a feed item.
  3. The web discussion does not continuously poll for replies, and its comment-only renderer does not currently surface the TAP_TO_CHAT cap interaction.
  4. The 12-reply ceiling is counted per item, not across child FeedItem thread members.

Where to go next