feat(pages): watch published pages and route comments to the owning agent

Merged
HA
2mo ago
+11955-549

Summary

  • A watcher in host-service polls the cloud for new human comments on published pages and delivers them into the terminal of the agent that owns the page, so the agent can answer with the CLI commands it already has.
  • Comments stop disappearing when an agent republishes, and a new sidebar makes every thread reachable — including ones whose anchor no longer resolves and therefore draw no pin.
  • Desktop gains a watcher picker (replacing the one-shot hand-off menu) and a chip showing what each agent watches; web gains a read-only "an agent is watching" badge.

Design record: plans/20260828-pages-comment-watcher.md

Why

An agent publishes a page, a teammate comments, and nothing carries that comment back. The only path was a desktop-only menu that pushed threads into a terminal by hand, so a reader on a shared link had no route to the agent at all.

Three existing defects sat in the way and are fixed here:

  • publish revoked the agent's own permission to reply. It cleared agentActivatedAt on every thread, and assertActivatedForAgent then rejected the reply — so the publish-then-reply loop the page skill documents returned FORBIDDEN. Activation is now granted by a human writing on a thread and is not revoked by a republish.
  • Comments vanished on republish. Threads were bound to the version they were written on, so publishing v2 hid every v1 thread — including the reply that answered it, at the moment it was written. list now returns every version's threads with the version attached.
  • Threads whose anchor stopped resolving were unreachable. The pin layer skips them silently and there was no other surface. The sidebar groups them explicitly.

How it works

host-service is the only process that can see both halves — it already holds an authenticated cloud client and owns the terminals — so the loop is one ticker in one process with no new transport.

one setInterval, 5s, whole registry
  └─ per watched page, sequentially:
     terminal alive + has a live agent?   local
     quiet for 2h?                        local  → unwatch
     due? (5s active / 60s quiet)         local
     listThreads(pageId)                  cloud read
     select unresolved threads with a human comment newer than cursor
     agent busy? (hold ≤2min)             local
     terminal.send — one batched prompt
     commit cursor + pings                ← only after the send resolves

Deferral is re-evaluation, not a queue: nothing commits, so the next tick re-selects the same threads plus anything new and they arrive as one batch.

Bounded by construction: 20 watchers per host, a 2h idle TTL, a stop after 5 consecutive failures, 5 pings per thread. The last exists because CLI agent attribution is a claim rather than proof — an agent that runs the CLI without SUPERSET_TERMINAL_ID records its reply as human and would otherwise re-arm the trigger forever.

Two nullable columns on pages (watched_by_agent, watch_heartbeat_at) carry just enough for a reader's browser, which has no host access, to show that an agent is listening. Liveness decays from the heartbeat so the badge stops claiming a watcher after the terminal dies.

Also in here

packages/i18n's scripts now run lingui under bun --bun. lingui gates its CLI bootstrap behind import.meta.main, which is undefined in Node below 24, and the lingui shim runs under node — so lingui extract exited 0 having done nothing, and the CI drift check passed no matter how far the catalog drifted. Verified by adding a probe id to a pre-existing file and watching the catalog not change. Happy to split this out if you'd rather it landed separately.

Testing

bun test across the touched packages: 248 pass. New unit tests cover the trigger rules (agent replies never self-trigger, resolved threads skipped, ping ceiling, batching), the manager's bounds (watcher cap, idle TTL, failure stop, terminal-exit sweep, busy hold, max hold, failed-send retry), the 90s watch liveness rule, sidebar grouping, and comment attribution.

Typecheck and biome clean across trpc, db, host-service, cli, mcp, ui, workspace-client, web, desktop.

Migration 0096 is drizzle-generated, applied and verified against a local Postgres.

Manual QA

The loop has been run end to end once: an agent published a page, a comment left from the web viewer was delivered to its terminal, and the agent replied — confirmed in the database (author_kind = agent with a session id).

Not exercised against a live agent: the busy-hold path (only tested against a fake isAgentBusy), the failure/backoff paths, and watcher reassignment from the desktop picker.

Risks

  • First background loop in host-service that calls the Superset cloud on a timer. Every other ApiClient use is request-scoped or a one-shot at startup. Token refresh with no user present and offline behaviour are untested outside mocks.
  • No backoff yet. ~25s of consecutive cloud errors retires every watcher on the host, logged only to the host console. The design called for the consecutiveFailures-doubling TTL used in pull-requests.ts; it is not implemented.
  • Sequential polling. One cloud round trip per due page, unparallelised, so the 5s cadence degrades past roughly 20–30 pages. The 20-watcher cap sits about where that saturates.
  • pings is never reset per version, so a sixth round of discussion on one thread is discarded rather than held.
  • Rollback is removing the branch; the two columns are additive and nullable, and nothing reads them when no watcher is running.

Known follow-ups

Raised in review and deliberately not fixed here: backoff on cloud failure, per-version ping reset, parallel polling, initialsOf promotion to a shared util, SKILL.md still not mentioning the watcher or --no-watch, and the web sidebar having no toggle or responsive handling.

https://claude.ai/code/session_015wHZeSUafJ58aKUsm7siNu


Summary by cubic

Adds a watcher in host-service that polls published pages for new human comments and delivers them into the owning agent's terminal, so a comment on a shared link reaches the agent without desktop hand-off. Fixes three defects in that loop: publishing no longer revokes the agent's reply permission, republishing no longer hides old threads, and threads whose anchor no longer resolves are reachable via a new sidebar.

Watcher

  • One ticker in host-service polls each watched page every 5s (60s when quiet) and batches new comments into a single terminal prompt; nothing commits until the send resolves, so a failed delivery is retried rather than marked delivered, and a throw between listing and sending counts as a failure without escaping the tick.
  • Delivery holds while the agent is busy, capped at two minutes; a poll whose entry was unwatched, replaced, or dropped mid-flight is discarded so a stale continuation can't re-arm the cloud watch flag.
  • Bounded by construction: 20 watchers per host, a 2h idle TTL, a stop after 5 consecutive cloud failures, and 5 pings per thread; control characters are stripped from comment bodies so a paste terminator can't leak into the terminal as keystrokes.
  • Only the page owner sees the watcher picker, and assigning claims the watch up front so an unwritable page fails loudly instead of silently on every heartbeat.
  • This is the first background loop in host-service that calls the cloud on a timer; token refresh with no user present and offline behavior are only tested against mocks. No backoff yet — about 25s of consecutive cloud errors retires every watcher, logged only to the host console.

Fixes and additions

  • publish no longer clears agentActivatedAt; activation is now granted by a human writing on a thread, so the agent's own republish no longer makes its reply return FORBIDDEN, and the old activate call is deleted.
  • pageComment.list returns threads from every version with the version attached, so republishing no longer hides older threads.
  • Agent replies render as "Agent" with a bot glyph instead of borrowing the credential owner's name and avatar; a new comments sidebar files unresolvable threads explicitly, and waits for the frame's rects before labeling anything "not on this version".
  • Desktop's one-shot hand-off menu is replaced by a watcher picker with reassign and stop; a terminal chip shows what each agent watches, and web shows a read-only badge that decays from the heartbeat (polling every 5 min when nothing is watching).
  • New CLI commands pages watch list and pages watch stop; pages comments list --unresolved sweeps every page concurrently, sorted by thread creation.
  • packages/i18n scripts now run lingui under bun --bun; the check previously exited 0 having done nothing, and all five catalogs now carry the new watcher strings with ko and fr translated and the count message aligned across locales.
  • Migration 0097 adds two nullable columns to pages; nothing reads them when no watcher runs, so rollback is just removing the branch.
  • Desktop's page viewer now uses the desktop auth client after merging main, so the sidebar and watch badge load under the right session.

Summary by CodeRabbit

  • New Features
    • Added page comment watching for terminal agents monitoring published pages.
    • Added controls to assign or stop watching, with status indicators on pages and terminal panes.
    • Added a comments sidebar with thread browsing, version indicators, and resolve/reopen actions.
    • Added CLI commands to publish with watching, list or stop watched pages, and list comments across pages or workspaces.
  • Bug Fixes
    • Improved agent reply attribution and preserved comment activity across republishing.
    • Replaced page handoff controls with streamlined watcher controls.