Skip to content

feat: agent→human console replies + exclusive board subscriptions#198

Open
ivanmkc wants to merge 2 commits into
feat/global-inboxfrom
feat/console-subscriptions-reply
Open

feat: agent→human console replies + exclusive board subscriptions#198
ivanmkc wants to merge 2 commits into
feat/global-inboxfrom
feat/console-subscriptions-reply

Conversation

@ivanmkc

@ivanmkc ivanmkc commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Stacked on feat/global-inbox (#190). Two console additions so multiple watching sessions coordinate, and so the human can see the agent respond.

Agent → human replies

  • POST /reply (token-gated) appends a kind:reply event into the same per-scope console thread as human messages and broadcasts it live over SSE — the human sees what the agent is doing in real time.
  • Replies are excluded from unread and the ?all=1 aggregate, so an agent never nudges itself with its own reply.
  • Viewer renders replies as left-aligned agent bubbles. CLI: termchart reply --project --agent --message.

Board subscriptions (takeover model)

  • POST /subscribe / /unsubscribe + GET /subscriptions — one owner per scope so two sessions don't both act on the same board's messages.
  • A fresh subscribe takes over; the response and the SSE subscribe broadcast name the evicted session so it can stand down. unsubscribe only releases if you still own it. CLI: termchart subscribe/unsubscribe/subscriptions --session <id>.

Design choices (per request): takeover (newest wins), explicit unsubscribe for clean release (takeover reclaims crashed sessions), persistent reply in the thread.

TestsSubscriptionStore (takeover, owner-only release, isolation), reply-excluded-from-unread/aggregate, server /reply + /subscribe + SSE broadcasts, CLI reply/subscribe/unsubscribe/subscriptions. Full suite green (viewer 494, CLI 184); verified end-to-end against a live viewer.

Review note: base is feat/global-inbox; the diff will collapse to just this commit once #190 merges.

Two console additions on top of the workspace-wide inbox:

- Agent → human replies: POST /reply (token-gated) appends a kind:reply
  event into the SAME per-scope thread as human messages and broadcasts it
  live, so the human can watch what the agent is doing. Replies are excluded
  from unread / the ?all=1 aggregate so an agent never nudges itself. The
  viewer renders them as left-aligned agent bubbles. CLI: termchart reply.

- Board subscriptions (takeover model): POST /subscribe / /unsubscribe +
  GET /subscriptions give one owner per scope so two watching sessions don't
  both act on the same board. A fresh subscribe takes over and the SSE
  subscribe broadcast names the evicted session; unsubscribe only releases if
  you still own it. CLI: termchart subscribe/unsubscribe/subscriptions
  --session <id>.

Tests: SubscriptionStore (takeover, owner-only release, isolation), reply
excluded from unread/aggregate, server /reply + /subscribe + SSE broadcasts,
CLI reply/subscribe/unsubscribe/subscriptions. Full suite green (viewer 494,
CLI 184); verified end-to-end against a live viewer.
…s advisory

Code-review follow-ups on the replies + subscriptions work:

- CLI inbox now knows kind:reply — a plain read prints '[n] reply <text>'
  instead of mislabeling the agent's own reply as a human 'message' (the
  consume path returns replies; only unread/--all drop them).
- Cap subscribe session id at 200 chars (parity with MAX_INBOX_*).
- Comments/help no longer overclaim: ownership is ADVISORY (server doesn't
  gate writes on it), and the subscribe SSE broadcast is a forward hook with
  no consumer yet — say so instead of asserting eviction is delivered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants