Files
orca/src
Brennan Benson dd39dcba5f feat(orchestration): a native chat gets the orchestration pointer a CLI agent gets, through the same send as your messages (#25078)
* feat(orchestration): agent mail to a busy chat waits in the chat's own queue

A mail notice for a busy structured chat used to wait in the orchestration lane's
own invisible "until the chat is free" gate. It now goes through the queue a
person's message uses: sendAgentTurn(..., { delivery: 'queue', source }) makes
the host hold it as a draft card the person sees and can Steer or delete, and
the queue sends it when the turn ends.

- The lane's busy gate (turnRunning / awaitingHuman) is gone; the queue's own
  hold decides. Its parking now only waits for its own send or card to settle.
- A queued card's hand-off (sent under a fresh id) is matched by
  queuedMessageId, so it stamps the mail once and never sends a second notice.
- A card from before an Orca restart is still the lane's: no second card.
- A card the person deleted counts as handled for that batch; newer mail
  notifies again. No stored flag: read off the card and delivered_at.
- `orchestration check` waits for the lane to withdraw a card whose mail it
  read, so a stale notice is never sent; more mail replaces the card.
- Each card records who queued it in queued_messages.source_json (versioned,
  schema-validated): the person, or Orca for agents, with every distinct
  sender as an orchestration party plus host id, and the mailbox, dispatch,
  run and message ids. The restart pause holds only the person's cards.
- sendAgentTurn answers with a snapshot, not the journal's live submission.

* test(orchestration): type the mail fixture as a pointer batch message

* fix(orchestration): the queue judges an agent's mail notice as it sends it

A mail notice queued in a busy chat could go out stale or twice: it was
kept true from outside the queue, by orchestration check withdrawing it,
which missed other readers, raced an attempt in flight, lost the card
when /clear carried it (a second notice), and moved it to the back when
new mail replaced it. An agent's card behind a person's paused card never
sent after a restart, so an unattended coordinator stalled.

- The drain asks the card's sender, in its own serialized step, whether
  it still stands: send, restate (count and senders of the mail owed
  now, written onto the card in the hand-off's transaction), or withdraw
  as the host. A failing judge sends as written. Send-now restates but
  never withdraws. Orchestration registers the judge on the host.
- check no longer touches any queue (back to main); onMailRead,
  notifyOrchestrationMailRead and the lane's withdrawals are gone.
- One unsettled card per agent message, enforced at insert; new mail is
  counted when the card sends, never replaces or moves it.
- The lane finds its card by what it is (a notice for the mailbox in the
  session the mailbox reaches now); a decline is a withdrawal a person's
  operation stamped. /clear moves agent cards as the host. Pointer rows
  last only while a direct send is in flight and end with the session.
- Pauses (restart, Stop, /clear) hold only a person's cards, and a held
  card never traps an agent's card behind it, keyed on the message kind.
- The source is named for the message (agent-session-message-source),
  one payload shape per message kind, no relative host id.

* fix(orchestration): Orca's mail notice waits in the queue out of sight

The notice card read as something the person typed and came and went on
its own, the transient-state message the queue should not show. It is
hidden until B can label who sent it.

- The host leaves mail-notice cards out of the published queue, so the
  list, its count, Steer/Delete/Edit, steer-newest and the paused header
  never see them, on every client. Keyed on the message kind: D6's task
  card will be shown, labelled, in B. They still count toward the queue
  limit (temporary, until B).
- A refusal no longer returns a hidden card for the person to act on:
  the host withdraws it, and the lane points again only after a turn ran
  since, the rule it already had for a refused notice.
- Send-now no longer restates a card: no one can reach a hidden one.
- The stored sender drops the pane key, a mailbox credential.
- The gate covers an abandoned worker: a mailbox that reaches no session
  withdraws its notice (tested).

* fix(orchestration): a hidden mail notice never waits on the person

Hiding the notice left three paths where it waited on someone who
cannot see it.

- A failed hand-off write put a send_failed hold on it, which only a
  person's Send or Delete clears: that mailbox's notices stopped for
  good. The host now withdraws a hidden card instead and hands its
  mailbox back to the lane through the ordinary redrive, since an idle
  chat gets no other edge. A write that keeps failing is retried once
  per edge.
- A person's Stop while the agent started on the notice put it back to
  waiting, where no pause holds it, so it was sent again at once. The
  host now withdraws it; the lane points again after a turn ran or when
  newer mail makes it a different notice, as on main. A restart still
  puts it back.
- A notice queued before the person's message went first. One order
  rule now: the person's cards go in order and never past one waiting
  on them; a card they cannot see never delays one they can, and goes
  only when none of theirs may.

The drain moves to its own module (structured-agent-session-queued-
drain.ts). Comments that still described the notice as visible are
corrected.

* fix(orchestration): an accepted notice hand-off stamps its mail; a judge that cannot look decides nothing

- When the agent opened its mail in the notice's own turn (the normal
  flow), an open batch left nothing owed, so the lane never marked the
  mail the accepted hand-off carried as delivered, unlike an accepted
  direct send. The lane now stamps exactly the ids a handed-off notice
  carried whenever undelivered mail remains, owed or not. A later
  conversation is no longer told again about mail already opened.
- At quit the host registry is cleared before teardown, so the drain's
  judge could not resolve the mailbox and withdrew the notice. A judge
  that cannot read its inputs now defers: the card waits for a step
  that can look. A mailbox that resolves to another session or none is
  still withdrawn.
- The judge, the owed-batch selection and the notice body move to
  structured-mail-notice.ts. A failing hand-back of a dropped card is
  logged.

* refactor(orchestration): a chat receives the agent's mail itself, queued like a person's message

A structured chat used to get a derived "You have N orchestration messages,
run check" notice, which could go stale while it waited in the chat's queue,
so the queue judged, restated or withdrew it as it sent. It now gets the mail
itself as the turn, through sendAgentTurn with delivery 'queue', the path a
person's message takes.

- One turn per mailbox batch: the mailbox's unread mail at delivery, in mail
  order, each message led by "[message from <sender>]", its type, subject,
  body, payload and reply hint (what check prints). Mail arriving while that
  card is still in the queue waits for the next card; a card is never edited.
- An idle chat takes it at once; a busy one queues it as an ordinary card the
  person sees and can Steer or delete. Mail is marked read when the chat
  accepts the turn, so check does not return it again; a deleted card leaves
  its mail unread for check, and it is not pushed again.
- The card stores who it is from (source_json): every distinct sender, and
  each message's id, run and sender. Kind 'mail-notice' becomes 'mail'.
- Removed: the send-time judge (QueuedAgentCardVerdict), structured-mail-
  notice, structured-pointer-notice-cards, queued-message-restatement, the
  hidden-card rules, the agent-card exemptions from the restart, Stop and
  /clear pauses, the dropped-card hand-back, and the drain split. An agent's
  card now follows the same pauses as a person's.
- Terminal agents are unchanged: they keep the typed pointer and check.

* fix(orchestration): a chat's check skips mail already queued to it; restarts hold only the person's cards

- When a structured chat runs `orchestration check` (consuming, --peek or
  --wait), mail an agent's card in that chat's own queue still carries
  (any card not deleted) is left out: it is on its way as a turn, so the
  agent does not read it twice. Derived from the queued rows at read time;
  a deleted card no longer carries it, so it comes back. Terminal callers
  and every other read are unchanged. New batches pass the exclusion to
  getOrCreateMailboxDelivery; peeks filter it.
- A restart's queue pause now holds only the person's cards: an unattended
  coordinator's agent card sends after an app restart without waiting for a
  Resume, since the mailbox is the record and reading is marked. Stop and
  /clear still hold agent cards. One rule, in queuePauseHolding. An agent
  card queued behind the person's held card still waits behind it: the
  queue never reorders.
- The duplicated direct-mailbox snapshot routing in check-run and
  check-worker becomes one helper, which keeps check-worker under its line
  limit.

* fix(orchestration): the mailbox is the only record of read mail; a chat's check takes its waiting cards

A card held an exclusive claim on its mail that nothing reconciled with the
mailbox: queueing it stamped the mail delivered, and a chat's check hid every
card that was not deleted. So a chat's own check --wait in one turn could not
see a result its waiting card held, a hand-off that ended in doubt or came back
"Not sent" stranded its mail, a check racing the card's build read the mail
twice, and a card left behind by run-use still sent and marked it read.

What a card or send holds is now derived each time from the chat's queue and
its sends: an accepted send marks its mail read; a waiting card or an
unanswered send holds its mail; everything else unread is pushed again or
returned by check. A card the person deleted is recorded as not to be pushed,
at the delete. The lane withdraws returned, partly read and moved cards. A
chat's consuming check withdraws the waiting cards holding what it reads, one
at a time with the lane. The sender is stored on the sent submission too
(host-only), so read state survives the card row's prune. A refusal before
anything started ends its operation; a restart pause is raised only by the
person's cards; an unreadable agent source stays an agent's.

* refactor(orchestration): a chat gets the pointer a terminal agent gets, sent through the chat's own queue

A structured chat now receives exactly what a terminal agent is typed: "You have N orchestration
messages ... run `orca orchestration check`" (formatMessagePointer, same CLI name). It goes
through the shared sendAgentTurn with delivery 'queue', the composer's queue-if-active: an idle
chat gets it at once, a busy chat's queue holds it as a normal card and sends it when the turn
ends. The lane's idle gate is gone; the send decides, as for the person's message.

The lane queues no second pointer while one of its cards still waits, read from the queued rows;
mail arriving meanwhile, or after the card is sent, is pointed again as the terminal lane points
new unread mail. A queued pointer counts as delivered, as an accepted one does. `check` and mail
read state are main's: only `check` reads mail.

The card records who it is from (queued_messages.source_json, kind 'agent', message
'mail-notice', with its senders) for the next PR to render. A restart's pause is raised by and
holds only the person's cards; Stop and /clear still hold an agent's.

Removed from the earlier designs: the message-as-turn batching, the check exclusion and taking,
the derived claims, lock and reconciliation, the sender on submissions, and their tests.

* fix(orchestration): a chat's pointer follows a card that leaves its queue unsent

The lane holds new mail while a pointer card waits, and retried it only on the chat's next
status change. A card that leaves the queue without a turn after it (the person deletes it
while Stop holds it, or its hand-off comes back) made none, so that mail sat unpointed until
something unrelated happened. The shared queue wiring now tells its host when an agent's card
stops waiting, read only when the queue changed, and the runtime redrives that chat's parked
mail. The runtime forwards the new host dep like the others.

Tests: that case end to end, and /clear carrying an agent card keeps who it is from. Agent card
bodies in tests are the pointer text; the restart pause's header says why an agent card may send.

* refactor(orchestration): no special handling for agent notices in the chat queue

A structured chat's orchestration notice is now sent through the chat's own send
like any message: an idle chat takes it as a turn, a busy chat's queue holds it
and sends it at turn end, under the same Stop, restart and /clear pauses as the
person's cards. Native chat no longer branches on who a card is from; the card
only records it.

- Restore main's queue pause logic (no restart exemption for agent cards).
- Remove the queue watch that redrove mail when an agent card left the queue,
  and the host's queued-row read the lane used for it.
- The lane reads nothing of the chat's queue. It sends no second notice while
  mail it already pointed at is unread, read off the mailbox alone.

* fix(orchestration): point new mail like a terminal, even while earlier pointed mail is unread

Drops the native-chat-only rule that held back a notice while the agent had not yet read mail it
was already pointed at. Also fixes main's stop-note test, which still built a provider handle in
the shape #24991 replaced.

* test(native-chat): keep the queued-message rig fixture under the line limit

* fix(test): drop main's duplicate codexProviderHandle import (same line as #25713)
2026-10-05 17:34:06 -07:00
..