Skip to content

feat: answer inbox requests from the desktop, the cli and paired phones - #911

Merged
leynier merged 1 commit into
mainfrom
feat/inbox-host
Oct 6, 2026
Merged

leynier merged 1 commit into
mainfrom
feat/inbox-host

Conversation

@leynier

@leynier leynier commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Summary

Host side of the external agent inbox. A caller outside any terminal asks an agent from an ext:<name> address. The question is pasted into the agent when its turn ends, even if its task or dispatch is still open. The agent answers with alera orchestration reply, and any surface reads or waits for the reply.

  • Verbs: inbox.summary, inbox.threads, inbox.thread, inbox.targets, inbox.ask, inbox.cancel, inbox.markRead, inbox.purge, inbox.wait. They live outside orchestration.* and go through require_authenticated_local_request.
  • Phones: the mobile allowlist gains exactly the first eight. inbox.wait stays local, since phones use the inboxChanged event. No orchestration.* verb is added.
  • Capability: inboxV1, advertised to local clients and in mobile.hello. No protocol version changes.
  • inbox.ask: takes a terminal, or the single running agent of a workspace narrowed by agent type (with candidates when ambiguous). The origin surface comes from the connection (cli, desktop, or mobile with its device). It snapshots the target and defaults to ext:user. A follow-up inherits the thread's inbox and recipient.
  • inbox.wait: parks per question or per inbox after a cursor. It wakes on replies, cancellation and purge, and does a final store read at the deadline before reporting timeout.
  • Events: inboxChanged {revision} goes to every authenticated client after inbox requests, orchestration requests and delivery stamps.
  • Banner: a question from an inbox tells the agent that the sender cannot read the terminal and to answer with reply --body-stdin.
  • orchestration.reply: now records reply_to_id and gains --body-file and --body-stdin.
  • Guards: an ext: address cannot be the sender of send or ask, the target of ask or dispatch, a coordinator, an assignee or a spawn terminal. Replying to a message an inbox received is refused, so nobody bypasses expiry and the pending limit.
  • Delivery: only messages that fit in one paste are stamped delivered. The rest stay queued for the next turn instead of being marked delivered unseen. This applies to agent messages too.
  • Push: a reply to an inbox sends a push in the existing attention category, with a generic title and ids only. It carries no reply text and no tab id.
  • Hub: inbox verbs are forwarded to the hub by name, so a remote terminal's alera inbox and the phone on the hub share one inbox.
  • History: pruning runs at most hourly, triggered by inbox requests.

Stacked on #910.

Validation

  • New inbox_requests_tests drive the real dispatcher with CLI, desktop and phone clients. They cover recipient resolution and errors, origin, targets, correlation, waking waiters, the read state shared across clients, cancellation waking a waiter, the deadline final check, the inbox listener cursor, the phone allowlist (inbox.wait refused) and the ext: guards.
  • Unit tests cover the inbox banner, the batch count, the push payload with no text, tab or terminal, and hub forwarding by verb name.
  • cargo clippy --workspace --all-targets -- -D warnings passes.
  • cargo test -p alera-cli: 1857 passed. One unrelated workflow worktree test (workflow_worktrees_snapshots_derive_retained_ownership_and_reject_replacement, "workflow integration is busy") failed once under load and passed 3 of 3 in isolation. All integration tests passed.
  • The max-lines ratchet passes.

Risk

The delivery change means a batch over 16 KiB is now pasted across turns instead of telling the agent that messages were omitted. Pruning only touches ext: conversations.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The pasted reply hint, filtered thread pages, and follow-ups that also send workspaceId need a fix before this host is what desktop and phones call.

Reviewed changes

Host side of the external inbox: an ext: address asks a live agent, the question is pasted when the turn ends, and desktop, CLI, and paired phones read or wait for the reply.

  • Inbox verbs. inbox.summary, threads, thread, targets, ask, cancel, markRead, purge, and local-only inbox.wait, advertised as inboxV1 with no protocol bump.
  • Answers. orchestration.reply records reply_to_id and accepts --body-file and --body-stdin. The paste banner tells the agent the sender cannot see the terminal.
  • Delivery. Only the messages that fit in one paste are stamped delivered. The rest stay queued for the next turn, including agent-to-agent batches.
  • Clients. Phones are allowlisted for the first eight verbs and receive inboxChanged. Hub reverse forwards the inbox verbs by name. A reply push stays in attention and carries ids only.
  • Guards. An ext: address cannot act as a terminal in send, ask, dispatch, or spawn, and a reply to a message an inbox received is refused. Orchestration requests now apply the mobile allowlist.

⚠️ Desktop drops inboxChanged

broadcast_inbox_change emits inboxChanged, and phones forward every event, but the desktop client only publishes names in runtimeHostEventNames. inboxChanged is not in that set, so a desktop listener never sees the revision and will not refresh until reconnect.

Technical details
# Register inboxChanged on the desktop client

## Affected sites
- `rust/alera-cli/src/terminal_host/server/inbox_requests.rs:70` - broadcasts `inboxChanged`
- `lib/src/features/workbench/infra/terminal_host/terminal_host_client_models.dart:309` - `runtimeHostEventNames` has no `inboxChanged`
- `lib/src/features/workbench/infra/terminal_host/terminal_host_client_session_events.dart:63` - drops events missing from that set

## Required outcome
- An authenticated desktop client receives `inboxChanged` the same way it receives `orchestrationBoardChanged`.
- Phones already forward every event. Do not add a second filter there.

## Suggested approach
- Add `inboxChanged` to `runtimeHostEventNames` in the same change as the broadcast.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using grok-4.7 | 𝕏

Comment thread rust/alera-cli/src/terminal_host/orchestration/message_formatter.rs Outdated
Comment thread rust/alera-cli/src/terminal_host/server/inbox_requests.rs Outdated
Comment thread rust/alera-core/src/runtime/inbox_queries.rs Outdated
@leynier
leynier changed the base branch from main to feat/inbox-store October 6, 2026 16:17
@leynier
leynier added this pull request to stack #919 October 6, 2026 16:17

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta.

Reviewed changes

Re-reviewed the host changes since f6306a4. Overdue questions now expire on their own, and a paste in flight is no longer treated as a settled cancel or expiry.

  • Expiry sweep. The presence tick expires overdue questions about every 30 seconds, wakes parked inbox.waits, and broadcasts the new revision.
  • Paste window. Cancel is refused while that terminal's paste is in flight, and a wait does not report expiry until the paste's Enter decides. A successful stamp still restores an expired inbox row to delivered.
  • Thread-bound sends. orchestration.send to an inbox must continue a question that inbox asked this terminal, and the message is stored on that thread.
  • Expiry bounds. expiresInMs that is not a whole number of milliseconds up to 7 days is refused before a question is written.
  • Gone recipient. A follow-up that inherits its terminal fails when that session is no longer known.

Pullfrog  | Fix it ➔ | View workflow run | Using grok-4.7 | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta.

Reviewed changes

Re-reviewed the host changes since eeced6ae. The pasted reply hint is now shell-portable, and a follow-up that also sends a workspace keeps the thread's recipient.

  • Reply hint. The banner no longer suggests a bash heredoc. It points at --body or --body-file.
  • Follow-ups. inbox.ask with threadId and workspaceId and no to inherits the thread recipient. A workspace still only chooses the recipient of a new thread.
  • Stack. The store files left this diff. They now live on feat/inbox-store.

Pullfrog  | Fix it ➔ | View workflow run | Using grok-4.7 | 𝕏

Base automatically changed from feat/inbox-store to main October 6, 2026 19:32
@leynier
leynier merged commit 7175f38 into main Oct 6, 2026
20 checks passed
@leynier
leynier deleted the feat/inbox-host branch October 6, 2026 20:11
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.

1 participant