Files
orca/src/shared/agent-session-restart-continuation.ts
T
Brennan Benson a0a7535b63 fix(native-chat): say each chat outcome once and drop internal-only surfaces (#24595)
* fix(native-chat): say each chat outcome once and drop internal-only surfaces

After a restart, resuming a chat reported the same outcome in up to four
places: a toast, the chat's own note, the status bar entry and the resume
dialog. Success also toasted, and a lost answer or unconfirmed dismissal
raised toasts of their own. The chat's note and the status bar entry (which
opens the dialog listing every affected chat) now carry it alone, and the
resume guidance no longer talks about which process "owns this session".

Provider events Orca does not recognise and that carry no words of their own
are logged instead of journaled as raw "codex · notification:foo" rows. Rows
with the provider's own sentence, failures, and records of replies Orca
already sent stay.

The approval card no longer shows the matched ask rule, its settings source
or a "Blocked path" line; it keeps the request, its detail and the reason.

* fix(native-chat): store wordless provider events, hide them only when drawn

Dropping text-less provider events on the host also dropped Codex's
turn/plan/updated row, which the task list (checklist and composer progress
panel) is built from. The host now journals these rows exactly as before,
and desktop and mobile chats leave out a row whose only text is the host's
"provider · kind" label. Failures, notices, rows with the provider's own
sentence and records of requests Orca answered still show. A hidden legacy
Codex collab-call row still counts as the agent acting for which subagent
section stays open.

Claude's local slash command output is read from `content` for that one
event kind only, so it shows as the row's text instead of disappearing.

* fix(native-chat): rows no client draws never fill the Codex cap or split a turn's work

- Codex per-turn cap: a wordless provider row (decided by the same shared rule the clients use)
  has its own silent storage bound, so it no longer crowds out rows that draw and never produces
  a "N more provider notifications not shown" line about rows nobody sees. Plan updates are
  exempt from the cap, so the task list keeps updating after a noisy turn.
- A Claude local command's row an older host stored with only its label (/usage output in the
  payload) stays drawn instead of disappearing.
- A settled turn whose only extra row draws nothing offers no expand arrow, and a wordless row no
  longer splits one tool run into two.

* fix(native-chat): a resume request that never reached the chats says so once, with Retry

When the resume request fails before the host reserved anything (the host is not ready, or its
recovery record cannot be written), the host has nothing to record, and the chats used to fall
silently back to "N chats to resume". The status bar and dialog now show them as failed, with the
existing guidance and Retry, until Retry, Dismiss, a host answer that no longer offers them, or a
relaunch. Comments that still cited the removed toast are corrected.

* fix(native-chat): keep mobile tool runs whole and a failed Dismiss's chat marked

- Mobile draws no task list, so it now drops wordless provider rows, plan updates included,
  before folding tool runs; a mid-run plan update no longer leaves two run headers back to back.
- A Dismiss the host never took keeps a lost resume request's chat shown as failed instead of
  turning it back into a plain offer. The lost request's real error now goes to the renderer log,
  since the row carries this side's own reason code.
- Comments corrected: the Codex cap no longer claims dropped wordless rows stay countable, and
  the resume comments cover a chat a lost request never reached.

* fix(native-chat): tell the user what their Resume click did, in one toast

Clicking Resume in the dialog (or Retry on a row) closes the dialog and acts on
chats that are mostly off-screen, and nothing said what happened. One toast now
summarises that click: "Resumed N chats and asked them to continue", "N chats
couldn't be resumed" with Show (opens the dialog), "Couldn't confirm N chats were
resumed", or the mixed case in the same toast. A request lost before it reached
the chats counts its chats as not resumed. An opted-in automatic resume at launch
raises no toast; dismissal and other bookkeeping stay silent.

* fix(native-chat): Show on the Resume toast re-reads before opening; fix toast translations

Show could open nothing when the host retired the failure while the toast was up,
because the dialog draws nothing without rows and the request stayed latched. Show now
goes through the status bar entry's re-read-then-open path, moved into the store so
both use it. The French, Japanese, Korean and Chinese "Resumed N chats and asked them
to continue" strings are rewritten to match their sibling strings. Tests pin Show on
the toast of a lost resume request whose chats the re-read still lists.

* test(native-chat): click the Resume toast's Show through a typed helper

The tests reached sonner's action handler with Reflect.apply, which the anti-slop lint
rejects. One test-support helper now returns the last toast's Show as a typed click.

* fix(native-chat): name the path an approval needs when the request doesn't show it

The approval card dropped Claude's blocked path along with the ask rule, so a
request like `git push` that needs access to `.git/config` no longer said what
it touches. Bring the path back as "Needs access to: <path>", only when the
description, reason or request detail doesn't already show it (verbatim or
JSON-escaped). One shared rule serves desktop and mobile; the ask rule stays out.

* fix(native-chat): count a blocked path as shown only when the text names the whole path

'/srv/data' inside '/srv/data-old/x' hid the "Needs access to" line although
the path itself was never shown. A match now needs the next character to end
the path: end of text, whitespace, a quote or a separator. Also pin that the
desktop approval mapping passes the blocked path through and drops the ask rule.

* fix(native-chat): the automatic resume at launch gets the same one outcome toast as a click

An opted-in launch resume now calls the same announce as a clicked Resume, with the same copy:
how many resumed, how many couldn't (with Show), unconfirmed, or mixed, said once. Dismiss and
bookkeeping stay silent; a launch whose chats all moved on raises nothing.

* fix(native-chat): the resume success toast says "Resumed N chats"

Each chat's own note already says Orca asked it to continue, so the toast drops "and asked
it/them to continue" in every locale; the couldn't-resume and couldn't-confirm counts and Show
are unchanged.
2026-10-06 12:59:32 -07:00

62 lines
4.0 KiB
TypeScript

// The message Orca sends when a user asks an interrupted agent to carry on.
//
// Identical for both providers, and deliberately not promptless. Codex's `turn/start`
// would accept an empty `input`, but Claude's SDK has no promptless form, so a bare continuation
// would make the two lanes behave differently — and the wording is the part that tells an agent to
// VERIFY its last action before repeating it. Both reasons point the same way.
//
// Sending this needs the user's OPT-IN, not their presence: the restart prompt's resume sends it,
// and so does a launch the user ticked "resume automatically" for. That is acceptable because the
// work being continued is the user's own, the wording above tells the agent to VERIFY its last
// action before repeating it, and each chat it reaches carries a note saying so. Reattaching
// without a send remains a separate operation that never comes here.
import type { AgentSessionResumeMarker } from './agent-session-resume-marker'
export const AGENT_SESSION_RESTART_CONTINUATION_MESSAGE =
"Orca restarted, so your previous reply was cut off partway through. Before continuing, check whether your most recent action completed — don't repeat it if it did. Then carry on."
/** For a marker that may stand for more than a reply: subagents, background commands, monitors or
* a prompt the restart stopped. Names none of them — the provider restates what it lost on
* resume, and a body that varied with the journal would change between retries of one offer. */
export const AGENT_SESSION_RESTART_WORK_CONTINUATION_MESSAGE =
"Orca restarted, which stopped what you were doing: a reply in progress, any subagents or background commands you had running, and any request waiting on the user. Before continuing, check what finished before the restart and don't repeat it. Restart anything still needed, ask again for anything still waiting on the user, then carry on."
/**
* The continuation a marker gets. A function of the marker alone, because the payload fingerprint
* covers the body and a retried offer must reach the ledger with the same one. A marker from a
* build that recorded only a working lead keeps the original wording.
*/
export function restartContinuationMessage(
marker: Pick<AgentSessionResumeMarker, 'activity'>
): string {
return marker.activity
? AGENT_SESSION_RESTART_WORK_CONTINUATION_MESSAGE
: AGENT_SESSION_RESTART_CONTINUATION_MESSAGE
}
/**
* Host-authored journal note marking the send as Orca's rather than the user's.
*
* Attribution lives in the journal, not on the provider wire. Codex's `turn/start` has no parameter
* we already send that could carry it, and adding one would be a new client-controlled field on a
* path that deliberately allowlists its params — untestable against older app-servers and
* Codex-only, which would break the both-providers symmetry above. The journal is ours, carries no
* compatibility risk, behaves identically for both providers, and is queryable with the rest of the
* session history.
*/
export const AGENT_SESSION_RESTART_CONTINUATION_NOTE =
'Orca asked this agent to continue after a restart. Your own prompt was not re-sent.'
/** Host-authored notes left in a chat the continuation did not carry on, so the chat itself says
* what happened and what to do. The next message the user sends is the manual continuation. */
export const AGENT_SESSION_RESTART_CONTINUATION_REFUSED_NOTE =
"Orca couldn't continue this chat after the restart. Send a message to continue it."
export const AGENT_SESSION_RESTART_CONTINUATION_UNCONFIRMED_NOTE =
"Orca asked this agent to continue after the restart but couldn't confirm it did. Check its latest reply before sending another message."
/** For a chat Orca could not get hold of. Why decides the fix, which the restart list gives; advice
* to send a message would meet the same refusal. */
export const AGENT_SESSION_RESTART_NOT_CONNECTED_NOTE =
"Orca couldn't reconnect this chat after the restart, so it didn't ask the agent to continue."