A host that refuses a send before looking up its id (a host with structured chats turned off)
refuses every resend the same way. Since a message past the host's window is no longer handed back
on wake, such a message was resent forever and held up everything queued behind it. Now, past the
host's window for the id, an answer the host itself gave that settles nothing (a refusal returned
or thrown, or a call turned away) hands the message back with "couldn't confirm" words, unless the
journal shows its row. Only a lost connection keeps it resending, since that says nothing about
the host. Derived from that answer and the id's own time; nothing new is stored.
Also corrects the host-window docstring, which still said an answer that never comes ends it.
The merge with main kept both versions of two delivery tests. Each still sent its first message
outside act() and waited with waitFor, whose polling uses the setTimeout that main's fake clock
freezes, so both hung until the test timed out. The target-switch test also sent its message twice
and looked for "Message delivery is unconfirmed.", a notice this branch replaced with "Sending…".
Both now send inside act() as main's do, keep main's exact probe timing, and read this branch's
notice.
Some messages are handed back because the loaded journal shows no row for them: an older build's
held message, a send a Stop outran, or one past the host's window. That journal could be one kept
through an outage, so losing contact read as "the host has nothing": a Stop followed by a new send
while offline handed the outrun message back at once, and a laptop asleep for over two days handed
a message back on wake without asking the host.
The read side now knows whether its journal is live: a frame of the current subscription makes it
so, and a closed or failed stream ends that. The chat lets the journal decide alone only while it
is live and attached, and a Stop is only given up for a newer send then. A message that slept past
the host's window is sent again first, and the host's answer (expired, read with the journal)
decides.
A message that came back to the composer returned its images by path only, losing the SSH
connection they were uploaded to, so on a remote workspace the preview broke and, after a reload,
the image asked to be attached again. The outbox entry now keeps each image's connection (saved
with it, never sent to the host, which reads paths) and the hand-back gives it back with the image.
Closing a structured chat's tab cleared the chat's whole outbox: a message still being sent
("Orca will keep trying…") or one queued behind it was deleted, its text kept nowhere. Closing now
throws away only a cancelled launch's own prompt that never went out (its notes return to the
shelf). Any other message that never went out comes back to the conversation's draft, with its
notes following the text. A message that went out may be the host's, so it stays in the outbox
and settles when the chat is reopened. The same rule now serves a launch's cancel and a removed
workspace's unpublished launches.
The image hand-back that this needs moved out of the composer hook into its own module, so the
close path doesn't load the composer (and through it the app store) from inside a store slice.
Drafts of a structured chat were saved under the pane (tab id plus a hash
of the session), so the follow-up that keys them by conversation would
have left every draft saved by this build invisible after an upgrade,
never shown and never deleted. They are now keyed by the conversation
(`agent-session:<sessionId>`) from the start: every composer showing the
conversation shares one draft, Stop and a queued card's Edit give text
back to it, closing the tab keeps it, and removing the worktree deletes
it. Terminal-backed chats keep the pane key and lose their draft when the
tab is closed.
A send now leaves whatever was added since it was sent, text typed or
composed and images attached meanwhile, and clears only what was sent;
a draft replaced in the meantime is left alone.
Lifted from #25207 (62ef8e7804): the conversation key, the composer's
draftScopeKey, keep-on-close and delete-on-worktree-removal, and the
leave-what-was-added send settle.
When a message carrying notes came back to the composer, the notes were cleared first and the
text written to the draft after. A crash between the two could lose both. The text is now saved
to the draft first, on every path that hands it back (the host's answer, the journal, a Stop).
Keys whose notes were not in the store yet stayed pending forever when the note never showed up:
a second "delivered" ending for notes already cleared, a page closed or a workspace removed, or a
note edited while its send was on the way. Each kept a store listener that scanned on every store
write for the rest of the run. A key now waits only while its workspace has not loaded; once it
has, a key with no note is dropped, and the listener detaches as soon as nothing is waiting.
A message an older build left waiting on its Retry is never sent again; once the chat's journal
loads it either comes back to the composer or the host's row shows it. Until then it was drawn as
a message on its way and read "Sending…", so QA saw it flash for a moment before its text went back
to the composer. It is now never drawn as one of this client's sends: no bubble and no notice. The
host's row, when there is one, or the composer is where it shows.
Notes sent to a chat had two owners when the message came back: its text went to the composer
and the notes returned to the shelf, so sending both delivered them twice. Notes now follow their
text: once the host has the message, or its text goes back to the composer (turned away, or taken
back by a Stop), the notes are used and leave the shelf. Only a message thrown away with nothing
handed back (a cancelled launch, or this window closing a failed chat) puts them back.
A send that ended before its workspace's or browser page's notes were in the store cleared
nothing, so those notes showed as unsent once they loaded. The keys are now kept until their
owner's notes load, then cleared.
The test that only called a mocked tab refresh now drives the real host-frame entry points (a
paired host's frame, and a local snapshot through the cancelled-launch filter). Stale "Retry"
wording is gone from a test name and a comment.
Notes handed to a chat were kept out of the next "Send notes" only in memory. After a reload,
while the chat still held the unsent message built from them, the notes were offered again and a
second send delivered them twice. A chat send also cleared its notes the moment it was queued,
so a message that came back to the composer left its notes gone from the shelf.
Now the chat's saved message carries the notes' keys (each naming its workspace or browser page),
for "Send notes to > New agent" and for a chat already open. A note stays off the shelf while a
message this client still holds carries its key and the host can still deliver that message; past
the host's window for it the hold lapses on its own. The message's own ending decides the rest:
once the host has it the notes are cleared as sent, from any send or resend and after a reload;
if it comes back, is withdrawn or is discarded, the notes return to the shelf (a returned
message's text is also in the composer). Nothing is released or deleted because a tab list or a
host sync no longer shows the chat.
One mechanism instead of two: the per-message watch and outcome promise are removed; the outbox's
entry endings drive both the launch prompt's result and the notes.
A press whose answer was lost stays quiet because Orca will send the Stop again. If a newer
message then makes Orca give the Stop up before that resend (or a resend is dropped), nothing was
ever said. The press's own notice ("The agent wasn't stopped.") is now shown when the Stop is
given up; it is honest, since the outcome is unknown.
Since the line under a message Orca keeps resending dropped every step, a refusal whose reason is
a failure fact (signed out, history too large, the agent stopped while starting, a Claude account
problem) or an older host lost its cause too, and said only "Orca will keep trying to send it."
for as long as a day. Each now keeps a cause-only sentence ("The agent is not signed in for the
selected account.", "This needs a newer Orca on the computer running this chat.") with no step,
in all six languages.
If the open chat's send had already settled the prompt when the launch picked up the shared send,
no ending reached the launch, which then waited forever. It now takes how that shared send ended.
When the first Stop's answer was lost, the press stayed quiet because Orca would send the Stop
again, but a refusal answering that resend was never said either, so the person was never told
the agent wasn't stopped. A refusal is now said on any attempt; a lost answer stays quiet only
while a resend is still owed.
The line under a message Orca keeps resending said things like "Send your message again" or
"Start a new chat" beside "Orca will keep trying to send it." Following that step while Orca
resends could send the message twice. The line now keeps only what stopped it (when the refusal
names a cause) and that Orca keeps trying:
- an "outcome unknown" the request threw says nothing, as the same answer returned does;
- a stage that couldn't be saved also says Orca keeps trying, which it does;
- a Stop that takes back the message the line is about clears the line.
Also corrects a comment: this build's replay makes the same stand-in record as an older host's,
for an accepted send whose row a new journal epoch dropped.
Two gaps in keeping notes out of another send while a saved chat message
carries them:
- The web client never installed the clearing that removes notes once a
chat sends them on, so after a Retry there the notes stayed listed. It is
now installed, once per renderer, by the background services both the
desktop and the web client load with App.
- A saved message carrying notes was thrown away only by this window's own
close. A chat closed from the phone, another window or while Orca was off,
or closed here without a known host, kept its notes held for good. A chat
the host stops listing (affirmed, and not a launch still starting or
failed) now has its queued messages thrown away once that sync lands, as
does a close that can name no host, so the notes come back.
The hold that keeps notes sent to a new agent out of the next send lived only
in memory. A reload while that chat had not yet sent them put the notes back
on the shelf while the chat's saved message still carried their text, so a
second "Send notes" sent them twice.
The staged message now saves the send keys of the notes it was built from.
The shelf treats a note as on its way while any saved message carries its key,
read from the saved outboxes on first use and kept current by the outbox's one
write funnel. When a message carrying notes is sent on (its own start, a Retry
or the re-check), those notes are cleared from their shelf; when it is thrown
away with its chat, they come back. Browser annotations sent to a new chat use
the same keys while the app runs. Running-agent sends keep their in-memory hold.
This replaces the per-message watch and outcome promise from the previous
change.
The launch caller was answered after the prompt's first attempt. When that attempt got no answer,
the chat kept resending the prompt, but the caller had already given up: the notes stayed on the
shelf and could be sent a second time. Each outbox entry's final ending (the host holds it, or it
came back, was withdrawn or was discarded) is now published per entry, and the launch settlement
waits through resends for it. Delivered fires onPromptDelivered once; anything else reports that
the chat already said what happened.
The outbox sender imported the launch prompt's shared in-flight map from the launch prompt module,
which imports the sender: a dependency cycle CI's lint rejects. The map now lives in
structured-agent-launch-prompt-in-flight-dispatches, which both import. No behaviour change.
A window that had already closed while the journal was still loading would arm a zero-delay timer
that re-armed itself every tick. Only windows still open arm one now; a closed one is settled when
the journal loads. Adds a test that a Stop-outrun send comes back once its window closes.
A repeated hand-back is the same text, so the repeat no longer adds leading spaces (the first
line's indentation is now kept). A launch prompt the host refused comes back to the chat, which
says why, so its caller is told the failure was already shown.
What one send attempt's answer may conclude, tightened where it could duplicate, lose, or strand
a message:
- A request the host turned away (unknown method, bad params, not authorized) proves no record
only on a first attempt; on a resend an earlier attempt may have landed, so it is sent again.
- "First attempt" is checked again when the answer arrives: another view that staged the same id
meanwhile makes it a resend.
- A reused message id on a resend is settled by the journal, as a conflict is.
- An older host's made-up record for a row its journal lost is the chat's only when a loaded row
shows it; otherwise the message comes back with "couldn't confirm" words instead of vanishing.
- A stage that can't be saved no longer hands the message back (an earlier attempt may have
landed): it waits, says "Couldn't save your message." once, and is tried again.
- When a resend stays unanswered because of a refusal Orca can't trust, the chat line says why
once, followed by "Orca will keep trying to send it.", and clears once the message lands.
- Sends a Stop outran, and messages an older build left, end when the host's window for their id
closes, even if nothing else moves by then.
- The queue stays held through the capability check, so a later send can't overtake the head.
- A launch prompt the chat handed back or keeps sending no longer also makes the caller toast
"could not be sent" with a copy button.
- A newer message used to make Orca give up on a Stop's outrun sends even while that Stop's own
request was still out, so a send could come back as "couldn't confirm" before the Stop's real
answer arrived. It now waits for the request to end.
- The first press no longer shows "The agent wasn't stopped." for a lost answer that Orca is about
to resend; it still does when nothing will resend it, and always for a refusal.
- This app's own sends are compared with the press by when they were queued, on this machine's
clock; the comment now says another client's are compared by the time in their id.
Two ways a Stop could leave messages stuck on "Sending…":
- the resend timer picked the first message in doubt even when it was one the Stop outran (never
resent), so a later message in doubt behind it was never resent either; it now resends the
message the queue is actually waiting on;
- a Stop answered while the agent was idle writes nothing to the journal, so its answer's position
was one the chat had already read, and the settling step only ran when the journal moved. It now
also runs when the outbox changes, so the outrun send comes back to the composer at once.
A message handed back to the composer was dropped whenever its text appeared anywhere in the
draft ("go" inside a longer sentence), and its first line lost its indentation. It is now skipped
only when the draft is that text or ends with it after a blank line, and only the end is trimmed.
A send now ends only by what the host said: it holds a record (the host's row shows the message
from then on), it proved there is no record (the text goes back into the conversation's draft and
the reason is said once), or nothing answered yet (the same id goes again, quietly, as "Sending…").
One shared settlement decides it for the open chat and a launch prompt alike.
Removed: the Retry control and its id rotation, the rejected and held-for-Retry outbox states, the
Stop latch, the per-window failure memory, the parked "unknown" entry, the launch prompt's own send
path, and the error-text regex (errors are read by code). A Stop stamps a send already on its way
with the Stop's own id; the Stop's answer settles it. Drafts of a structured chat are keyed by the
conversation and survive their tab closing. Entries older builds left waiting for a Retry are
settled once the journal loads, never sent again.
* Restore the owning Orca CLI path after shell profiles
* Use a literal marker for the Bash lookup regression
* Preserve plain panes and initialize zsh after prompt hook replacement
* Preserve user line-editor dispatchers during deferred startup
* fix: retain CLI startup when global Zsh replaces prompt hooks
* test: replay global Zsh hook replacement after host startup
* test: isolate controlled Zsh widgets from distro keyboard setup
* fix(shell): preserve user hooks during deferred zsh initialization
* Keep completed Zsh startup hooks retired when the wrapper is sourced again
---------
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Orca maintenance <orca-maintenance@users.noreply.github.com>
Co-authored-by: Orca campaign <orca-campaign@local.invalid>
The row keeps #24606's "Sending…" in the time's slot and #24918's muted not-sent line; the notices
keep both: an entry still sending reads as sending, and a recorded rejection no entry carries reads
as not sent.
Notes sent to a new agent were released back to the shelf as soon as the
chat's start failed, while that failed chat kept the same text staged for its
own Retry. Re-sending and pressing Retry put the notes in two chats, and Retry
alone left delivered notes listed as unsent.
For a new chat, the notes now follow the prompt it staged: held while that
message is in the chat's outbox, cleared from the shelf when any dispatch
(Retry or re-check included) sends it on, and back on the shelf when the
chat is closed and its outbox thrown away. A paired server's chat is found
once its start settles. Running-agent sends keep their own result.
While notes are only on their way, the send button reads "Sending…" rather
than "All notes sent".