Skip to content

Offer to reconnect native chats that were working when Orca restarted - #21096

Merged
brennanb2025 merged 41 commits into
mainfrom
brennanb2025/nc-resume-on-restart
Sep 17, 2026
Merged

brennanb2025 merged 41 commits into
mainfrom
brennanb2025/nc-resume-on-restart

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3174 $\color{#cf222e}{\Huge{\mathbf{−}}}$​230 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2944
Prod 62 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3178 $\color{#cf222e}{\Huge{\mathbf{−}}}$​266 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2912

ELI5

Before this change, restarting Orca while a native chat was answering left that chat interrupted with no guided recovery. After this change, the next launch can show the affected local chats and let the user reconnect them or explicitly ask them to continue. Reconnecting never sends a message.

What Changed

  • During shutdown, Orca first reduces provider events it has already accepted, then records only chats that are still actively generating. Completed work, newer user messages, questions, and approval prompts are excluded.
  • Recovery evidence lives in a small, non-backed-up capsule separate from conversation history. Startup atomically consumes the whole capsule before attempting any reconnection, so restored conversation backups cannot restore spent recovery authority.
  • The modal groups qualifying chats by project and workspace, supports folder workspaces, and shows the exact continuation message before it can be sent.
  • Reconnect restores the session without sending. Reconnect and continue is an explicit action that uses the existing operation ledger for at-most-once delivery and rechecks eligibility immediately before dispatch.
  • Refused and unconfirmed outcomes are reported to the user. A bookkeeping failure cannot turn a proven refusal into an ambiguous delivery result or block the underlying action.
  • “Don't ask again” only enables automatic reconnection. It never enables automatic continuation.

Why

The original failure is a lifecycle ownership problem: the desktop runtime owns the provider child, so an app restart can preserve conversation history while losing the in-progress reply.

Recovery evidence must have a shorter lifetime than conversation history. Storing it beside backed-up session data can bring stale permission back after a restore. The separate capsule gives the evidence one owner and one bounded lifecycle: shutdown publishes it, startup consumes it, and dismissal, expiry, superseding provider state, or successful take ends it.

This is deliberately fail-closed and advisory. If Orca exits after consuming the capsule but before reconnecting, the offer can be lost; the conversation remains available to open normally. That tradeoff avoids waking an agent from stale evidence. Reconnect and continue remain separate because sending work requires an explicit user action and delivery accounting.

Linked Issue

N/A — this PR has no linked issue.

Visual Proof

Captured from the exact reviewed head in a hidden macOS Electron renderer over CDP. A neutral RPC fixture supplied eligible chats and result states while the production modal, grouping, action handlers, and notification code ran unchanged.

Grouped recovery offer. Three interrupted chats are selected by default across a git workspace and a folder workspace. Reconnect is the primary action; continuing remains explicit.

Grouped restart recovery modal

Exact continuation message. The popover reads the same shared constant sent by the action.

Continuation message popover

Mixed delivery results. Accepted, refused, and unconfirmed outcomes are reported separately so the UI does not turn uncertainty into success.

Mixed continuation result notifications

Testing

  • I manually tested these changes locally on macOS with the Electron app over CDP in background mode.
  • Automated tests added/updated.

Validated the recovery capsule, teardown ordering, queued provider lifecycle events, concurrent continuation requests, operation settlement failures, folder workspace grouping, renderer result feedback, current/older wire compatibility, and desktop/mobile at-most-once delivery contracts. Fresh final readiness runs passed 428 tests across 37 files, alongside node/web typechecks, changed-code quality, localization, and whitespace checks.

The Electron pass measured one resume call and zero continuation calls for reconnect, reconnect-all, and automatic reconnect. Explicitly continuing one selected chat issued exactly one continuation call. It also checked keyboard focus, accessible names, the exact message text, and mixed result feedback. Host-side eligibility and delivery semantics are covered by deterministic tests; this UI fixture does not prove real provider delivery or a full process restart.

Windows packaging and the cross-version checks run in CI. The feature remains limited to locally hosted structured chats; existing launch resolution excludes SSH, WSL, and paired remote execution. Native mobile UI is unaffected.

AI Disclosure

Review

The final implementation was reviewed repeatedly for functional correctness, lifecycle ownership, delivery semantics, crash boundaries, security, cross-platform behavior, and performance. The architecture uses the existing host journal, acquisition path, and operation ledger instead of adding a parallel provider lifecycle or delivery system.

Agent skill upstream boundary

  • Not applicable.

Notes

  • A successful capsule take is intentionally single-use and may lose the offer if the app exits before acquisition; opening the conversation normally remains available.
  • No polling, per-event filesystem writes, retained timers, broad profile scans, or provider subprocesses were added.
  • Linux and Windows runtime behavior is covered by platform-neutral code and CI/package checks; live GUI validation was performed on macOS.

Checklist

  • This PR is focused on restart recovery for structured native chats.
  • I explained the user-facing before/after, the mechanism, and why this design was chosen.
  • Current screenshots are attached above.
  • Self-reviewed for correctness, security, and performance.
  • Cross-platform, SSH/remote, folder workspace, mobile, and backwards-compatibility impact considered.
  • Focused tests and local checks pass; CI covers the full repository and packages.

Teardown records a marker for every session this host was genuinely running a
turn for, derived from the LIVE runtime rather than a persisted status row, so
a stale `running` row left by an older crash can never trigger a resume. On the
next launch a modal lists exactly which chats would resume and resumes them via
native continuation (Claude resume/resumeSessionAt, Codex thread id) — never by
re-sending the prompt, which is what makes an agent redo finished work.

A session resumes only when all of these hold: a teardown marker exists and has
not expired, the record's lease is released and reconciled, a provider resume
cursor exists and still matches the marker, the journal's own turn record names
the same turn, and the marker has not already been spent. Markers are consumed
before the resume is submitted, so a crash mid-resume cannot double-fire, and an
admission gate refuses a second concurrent resume for one session. Resumes are
staggered three at a time rather than spawning every provider at once.

The modal's "Don't ask again" checkbox writes the nativeChatResumeWorkOnRestart
setting, which Settings can turn back off; automatic mode runs the identical
predicate and staggering and reports what it did. Declining consumes the markers
so the prompt cannot return every launch — nothing is lost, because opening a
chat still re-acquires it at the same cursor.
…resume

Four defects QA found in the restart-resume offer, fixed together because the
first two interact: shipping the root fix without the state fix would convert a
silent no-op into actively offering finished chats.

1. Claude was never offered (0/4). The marker recorded agentSessionProviderHandleKey,
   which embeds Claude's leaf uuid — a branch cursor. The adapter's own close path
   appends a `resumed` link with an advanced leaf during the SAME teardown, so the
   marker went stale seconds after it was written and the drift guard refused every
   Claude session forever. Record and compare agentSessionProviderHandleRoot instead:
   the root is the part a resume must preserve, and changing it is a fork, which is
   exactly what this guard is for. Codex is unaffected (its thread id is the whole
   key) but uses the root too, so the rule is uniform.

2. The predicate compared turn IDENTITY but discarded turn STATE, so a `completed`
   turn satisfied it as readily as an interrupted one. Eviction rewrites `running`
   to `interrupted` and never to `completed`, so the state is what separates work
   that was cut off from work that finished. Require `interrupted` or `unverifiable`.

3. A chat blocked on a pending approval or question was marked as working, because
   the teardown reader accepted any `running` turn while the product's own projection
   calls that state `attention`. Teardown now defers to that projection: an agent
   waiting on the USER is not interrupted work.

4. "Resume all" could silently no-op. The modal fetched candidates at mount; by click
   time the chat's own pane may have bound and taken the hold, moving the lease to
   `live` so the predicate dropped it and the call returned no results, leaving the
   dialog open behind a dead button. Re-derive at click time and settle an
   already-live session as resumed — it is running, which is what the user asked for.

Test fakes now model the Claude close path that advances the leaf, which is why no
unit test could previously exhibit defect 1. Ablation covers all eleven guards.
… predicate

Two follow-ups from re-QA, both cases of a rule stated by intent rather than by
discriminator.

1. The already-live path bypassed the predicate. "Resume all" sends no session
   ids, so the fallback's target set was every marker, and it was gated only on
   the session having a live provider child. A chat the predicate had refused --
   a completed turn, say -- whose pane happened to own the lease was therefore
   settled as `already_live` and had its marker spent, inflating the "Resumed N"
   count with chats that were never eligible. No provider spawned and no tokens
   were spent, but a marker the predicate rejected must never be consumed.

   The resumable set now takes an explicit `leaseState`. The already-live path
   derives a second set with ONLY the released-lease clause relaxed, and settles
   a session just when it is in that set. Every other clause still applies.

2. The `attention` rule was one-sided. Teardown refuses to mint a marker for a
   chat blocked on the user, but the set predicate had no equivalent, so a marker
   arriving by any other route was offered once eviction rewrote its turn to
   `interrupted` -- the same asymmetry the completed-turn case had.

   Gated on projectStructuredAgentSessionStatus === 'attention'. That projection
   tests for a pending approval or question BEFORE it looks at turn state, so it
   still reports `attention` after the turn is settled, which makes it the durable
   signal and keeps one source of truth with teardown.

Ablation now covers thirteen guards, including one for each of the above.
…iving it

The awaits-user clause could never fire. It asked the live projection for
`attention`, which needs a prompt whose resolution is still `pending` -- but
teardown CANCELS that prompt a few phases after it writes the marker. By the next
launch the evidence is gone, for precisely the sessions the clause was written
for. QA measured the injection still being offered and then resumed.

This is the same shape as the leaf-drift bug: state read after teardown is not the
state that justified the marker. The discriminator, now applied across the whole
predicate:

  - a fact teardown itself destroys or mutates must be CAPTURED on the marker
    while it is still true;
  - a fact that evolves on its own must be RE-DERIVED at read time, never
    snapshotted.

So `awaitsUser` is now recorded at teardown and the predicate reads the recorded
value. Teardown still declines to mint a marker for such a session, so the
recorded flag is the second line rather than the only one.

Audit of every other clause against the same test:

  - turn id (captured) -- teardown rewrites turn STATE but never the id. Correct.
  - provider handle root (captured) -- the close path appends a resumed link, and
    appendAgentSessionProviderHandleLink refuses one that changes the root, so the
    root is invariant under exactly the mutation that broke the key. Correct.
  - turn state (re-derived) -- DELIBERATE exception, stated here rather than left
    implicit: we are not reading the state that justified the marker, we are
    reading teardown's receipt that it settled the turn. A turn still `running`
    means eviction never finished, and we refuse. Correct, and intentionally so.
  - lease reconciled / released / handoff stage (re-derived) -- these answer a
    different, launch-time question: may this host take the lease NOW. The
    teardown-time value would be meaningless, and `unreconciled` is cleared by
    this launch's own reconciliation. Correct.
  - adapter support, marker TTL, marker consumption (re-derived) -- all evolve
    independently of teardown. Correct.

Only awaitsUser was on the wrong side.
The captured flag was dead code. `awaitsUser` could only be true when the
projected status was `attention`, and `attention` hits the `continue` above the
push -- so every marker teardown can ever write carries `false` (QA measured
22 of 22 across two real teardowns). The predicate clause reading it was
unreachable by any production path.

A flag that is structurally always false is worse than no flag: it reads as a
safeguard, so the next person to touch this trusts it. The asymmetry it was
added to close was only ever reachable by fault injection, because teardown is
the sole writer of markers and already refuses attention sessions.

Removing it also drops an upgrade discontinuity: as a required field it made a
marker written by the previous build fail validation and be silently discarded,
costing a resume offer on precisely the upgrade where the user was mid-turn.
Markers predating the providerHandleRoot rename still will not parse, but those
carry a leaf-sensitive key the predicate would refuse anyway, so nothing usable
is lost.

In its place the teardown gate now states that `status !== 'working'` is the
SINGLE gate for awaiting-user sessions, why a predicate-side mirror would be
unreachable, and why it could not even re-derive the fact -- so the reasoning is
inherited rather than rediscovered.

Ablation is back to twelve guards; every other clause is unchanged.
Two changes, both independent of the parked continuation decision.

1. The copy claimed something QA disproved. "Resuming continues each agent where
   it left off" is false: reconnection restores the session at the point it
   stopped, with full context and without re-sending the prompt, but the
   interrupted reply does not continue on its own. The toast's "Resumed N chats"
   implied work had restarted.

   Audited every user-facing string against the rule that none may claim work
   continues or that a reply resumes -- which caught more than the three strings
   the fix started from. The title, the row button, "Resume all", "Resuming...",
   the not-now hint ("picks it up where it left off"), the checkbox and its hint
   ("resume on their own"), the list's aria-label and the Settings row all made
   the same claim. The user-facing verb is now reconnect throughout; the body and
   update variant state outright that the interrupted reply will not continue.
   en.json synced, runtime boot catalog regenerated.

   If we later decide to send a continuation instruction, this is one commit to
   change back. Shipping text we know to be false was the worse option.

2. Rows now show each offer's age. The TTL is 24 hours and a stale offer looked
   identical to a fresh one. The marker already carried `recordedAt`, so this is
   a render change plus one field on the renderer's candidate type, formatted
   with the existing formatUiRelativeTime helper rather than a new one.

   The clock is stamped once when the list arrives rather than read during render:
   ages then stay stable across re-renders, and the render stays pure, which the
   react(purity) rule requires.

Guards, predicate and RPC are untouched; ablation still covers twelve.
A row read `codex · folder:8f3a1c22-… · 8 hours ago`. Recognising which chats
would reconnect is the entire point of the list, and at twenty rows a UUID
identifies nothing.

No RPC or host change was needed: the renderer can already resolve this id.
Resolved the way automation dispatch resolves the same id space
(resolveAutomationDispatchWorkspace) -- a folder workspace by its full
`folder:<uuid>` key via getKnownWorktreeById, a git worktree by its bare
`repoId::path` id via allWorktrees. Both return a Worktree, whose displayName is
a required field, and DetectedWorktree extends Worktree so either shape answers.

Falls back to the id when nothing resolves, which is what the row showed before
and also covers the window before the worktree store has hydrated.

The lookup lives in a per-row subcomponent because a hook cannot run inside
`map`, and its selector returns a primitive string so repeated selector runs
cannot churn referential equality.
…in continuation

Grouping. Rows are now grouped under a worktree heading with the repo glyph and
an agent count, using the sidebar's own collapse mechanics. Only presentational
pieces are reused -- RepoIconGlyph, CompactAgentExpansion, AgentIcon and
formatShortTimeAgo. The sidebar's agent row cannot be: worktree-card-compact-agent-row
imports DashboardAgentRow, the dashboard's own type, so both surfaces render one
live-agent model requiring a pane, tab and status entry. Every chat offered here
is by definition stopped, so supplying that would mean inventing live state.

Two things I had assumed were reusable and were not:

  - DashboardHostBadge returns null unless hostKind is ssh or remote. Structured
    chat is local-only, so it would always render nothing. The host line is
    omitted rather than faked; the badge is the right element to add if and when
    structured chat gains remote support.
  - No state dot. Every AgentDotState misleads here: idle and unverifiable both
    presuppose a live pane, interrupted renders red like an error, done green,
    working a spinner. A missing dot beats one saying these agents are running.

One worktree renders flat with no heading -- a name, count and chevron around a
single group says nothing the dialog has not already said.

The age column now uses formatShortTimeAgo for sidebar consistency. It takes
(timestamp, now) and subtracts internally rather than taking a delta, so the call
is (recordedAt, listedAt); passing the old delta would have rendered plausible
nonsense. The clock is still stamped once into state, so ages stay stable and the
render stays pure.

Continuation. A secondary "Reconnect and continue" action sends one message, from
a single shared constant, identical for both providers. Reconnect is unchanged and
still sends nothing. An info popover quotes the literal message read from that
same constant, so what is shown cannot drift from what is sent.

Ablation now covers fourteen guards. Two are new: continuation only follows a
reconnect that actually happened, and -- inversely -- a send injected into the
reconnect path must turn the test red, since "don't ask again" rests on reconnect
never sending.
…quality gate

The modal lists stopped chats with no way to tell that CLI agents are fine, and
the true state of the world is counterintuitive: the terminal sessions survived
the restart and the chats did not. One line now says so, next to the heading
where it frames the list rather than as a footnote at the bottom.

Wording follows the app's own vocabulary rather than inventing a term: the
catalog settles on "terminal sessions" (terminalSessionCount, "Terminal sessions
are grouped by workspace", "No terminal sessions yet"), and UpdateCard already
reassures with "Your terminal sessions won't be interrupted during the update" in
the same text-xs text-muted-foreground treatment. "kept running" rather than
"were restored" -- nothing reconnected them, they never stopped, and the line
says nothing about why.

Also clears check:code-quality:changed, which I had not been running -- oxlint
alone covers neither the design-system nor the casting audit, so 18 findings had
accumulated across the branch.

  - design system (4): Button spacing hand-rolled as gap-1/px-2 is just size="xs";
    PopoverContent and DialogTitle own their typography and spacing, so the
    text-xs moved to the popover's own children and the title's icon gap moved to
    a plain wrapper.
  - casting (14): production code loses its assertions outright via Reflect.get,
    the idiom already used in managed-hook-detection-commands and
    worktree-name-retirement. The marker validator reads each field through
    Reflect.get and now checks recordedAt is a number rather than asserting it;
    the store-file parse uses the existing `file` shape instead of a second
    assertion; the runner narrows the admission error's owner with typeof.
    Test fixtures keep their assertions behind the line-specific SAFETY:
    rationale the repo mandates for exactly this case.

One trap worth recording: the audit reports an assertion at the line its
EXPRESSION OPENS, not where `as` appears, so a disable-next-line above the
closing brace of a multi-line literal is inert and silently changes nothing.

Guards unchanged; ablation re-proved 14/14 at this head.
…e name

Every row rendered the provider as a bare AgentIcon, whose svg carries no
aria-label, title or alt. With a Claude chat and a Codex chat in one worktree the
two rows were identical to any non-visual consumer, and the dialog offered
several identically-named "Reconnect" buttons with nothing to tell them apart.

A regression from 233e37b, where the row read `${agent} · ${workspace} · …` as
text. Moving the workspace name into the group heading was right; dropping the
provider to an unlabelled glyph is what lost the information.

AgentIcon takes no label prop, so the icon is wrapped the way
NativeChatSupportedAgents already names it: a span with role="img" and an
aria-label from formatAgentTypeLabel, the same labeller the sidebar and dashboard
rows use.

The per-row button also names its agent now ("Reconnect Claude chat"). The
identical buttons were half the reported harm, and an accessible name that opens
with the visible word keeps WCAG 2.5.3 satisfied. Say so if you would rather ship
only the icon label -- it is one attribute and one catalog key to drop.

Age code untouched, as asked: formatShortTimeAgo still takes (timestamp, now) and
is still called with (recordedAt, listedAt).
…e-on-restart

# Conflicts:
#	src/renderer/src/i18n/en-runtime-required.json
#	src/shared/structured-agent-session-live-turn.ts
…eal dispatch

Three defects in the restart-resume path, all of which could resume a session
that was not genuinely working or claim one was continued when it was not.

Launch scoping. A durable marker with a 24h TTL is a write-ahead latch: a
teardown write that failed or timed out, or a store restored from its backup,
left a previous generation's marker actionable, and automatic reconnect would
have acted on it silently. Markers now carry the id of the launch that wrote
them, and only the launch immediately after may claim them. The launch id lives
in its own file with no backup mechanism, so it cannot roll back in step with
the markers it is proving adjacency for. Startup claims the previous launch's
markers into launch-scoped memory and deletes every durable copy in the same
step, so the durable fact dies at claim time rather than at use time. Both
halves fail closed: an unprovable predecessor and a clear that throws each
claim nothing.

Dispatch states. The send layer answers ok as soon as Orca owns the message;
the provider's own answer lives in the submission. Continuation read only the
envelope, so a rejected turn/start was reported as continued and stamped the
journal saying the agent had been asked to carry on. All four states are now
preserved, and only an accepted dispatch appends the attribution note.

Claude pre-echo sends. Claude cannot write a running turn until the SDK echoes
the user message back, which is seconds on a real journal, so a turn-id-only
marker dropped exactly the sessions that were working hardest. A send that has
not become a turn now carries its own identity, and the launch-side predicate
asks the journal about that submission's dispatch state instead.
…ore judging

Two defects found in QA, both reproduced twice.

Follow the submission forward. The launch-side predicate accepted a
submission-shaped marker only while its dispatch was pending or unknown, but the
window in which work is submission-shaped is precisely the window in which the
dispatch is about to be accepted: the send settles during teardown and the turn
it opened is then cut off as interrupted. Judgement was frozen at the moment the
marker was written, so the predicate refused the very sessions this was built
for and fired only when the send never reached the provider. An accepted
submission is now followed to the turn it opened -- matched through the user
item key a turn names and a submission is aliased by -- and that turn is judged
by the existing turn rule. Accepted alone still proves nothing: without the link,
or with a turn that completed, this refuses as before.

Settle before judging. A send resolves as soon as Orca owns the message, while
its dispatch is still pending; that is the ordinary successful path. Reading the
dispatch off the send result therefore reported every delivered continuation as
pending and never wrote the attribution note. The outcome is now decided on the
settled submission, through the host's existing settlement waiter, with the send
result as fallback when nothing settles in time.

The failed-note path no longer swallows its error. It stays best effort -- a
journal that refuses the note must not turn a delivered continuation into a
failure -- but the failure is reported through the host's error sink instead of
being discarded, so it cannot regress unseen again.

The surface's send is typed against the wire result rather than a hand-written
subset, which is what let a test assert a shape the host never returns. Binding
the surface to the host moves into its own file: the host was one line under the
line cap, and the bindings carry decisions that belong beside their consumer.
…bar does

The offer is a list of workspaces, so it should read like the one users already
know. Rows are now three tiers -- repo or project, then workspace, then the agent
sessions inside it -- and each agent carries a checkbox rather than its own
button, checked by default, with the footer acting on whatever is ticked.

Reused rather than rebuilt. The host chip is the sidebar's own: its markup lived
inline in the card's meta row, so it moves to a shared component both surfaces
render, and the label comes from getHostContextLabel, which is where "Local Mac"
has always come from. The repo glyph is RepoIconGlyph; a group with no repo uses
the FolderTree the sidebar's own project-group metadata uses. The agent row
reuses AgentIcon, the agent-type label helpers, formatShortTimeAgo and the same
model treatment.

Two things could NOT be reused, and both are deliberate. The sidebar's
CompactAgentRow needs a live pane, tab and status entry, and every chat here is
stopped by definition. And the sidebar has no git-worktree-vs-folder glyph
resolver at all -- both kinds render the same card, and the difference people
read is its status lane choosing GitBranch when a workspace has branch identity;
that single precedent is what the workspace glyph follows.

The model, the execution host and the workspace kind now travel with each
offered chat. All three are read off the durable record the predicate already
holds -- the model through the same normalizer the status feed uses -- so the
glyph is never inferred from a name and no new data source appears. They are
optional on the wire, so an older host still renders a row.

Selection changes which ELIGIBLE chats are acted on, never what is eligible. Ids
are seeded from the host's own answer and intersected back against it before any
call, and the host re-derives the predicate regardless of what it is sent.
Continuing still requires an explicit click, and the automatic path still calls
the reconnect method, which contains no send.

The badge's treatment becomes a variant instead of a pile of overrides, which is
what the design-system gate asks for once the markup is somewhere it can see it.
A folder workspace's synthetic worktree borrows the `repoId` slot to name the
project group it belongs to, so that field is NEVER null. The reconnect offer
read a non-null `repoId` as proof of a git repo, looked it up in the repos list,
found nothing, and rendered the raw `folder-workspace:<uuid>` string as the group
header. The project glyph written for the no-repo case was unreachable for the
one workspace kind it was meant for, and the string fallback behind it was dead
for the same reason.

The project group name was available all along and the sidebar already titles
these with it, which is what this list is meant to mirror.

Recognising the id now lives beside the code that mints it, so the two cannot
drift: there was no such helper, only forward constructions of the same prefix in
five places. The header choice itself moved into a pure resolver, so the branch
that was wrong is now the branch under test.

The dead fallback string is gone, along with its catalog entries.
…turn for

QA: a chat that was genuinely working was silently dropped from the offer. The
discriminator was how far the send had progressed -- it was the last chat
prompted before quitting, reachable by quitting a second or two after sending.

Mechanism, reproduced against the predicate. The marker was written while the
send was still pending, so it is submission-shaped. During teardown the dispatch
then settled to `accepted`, which took it out of the pending/unknown branch and
into the follow-forward branch. But the provider died before writing a turn row
for that send, so there was no turn to follow forward TO, and the branch demanded
a proved link before it would answer. Both the no-turn-at-all case and the
newest-turn-belongs-to-an-earlier-exchange case therefore refused.

An accepted send that never became a turn cannot be finished work, because
finishing writes a turn row. The marked send is also the newest work in the
session, so any turn it opened would be the newest turn.

That makes the link unnecessary to prove for a safe answer. When the newest turn
is interrupted or unverifiable the two readings agree: if the row really is this
send's under a key we failed to match, it was cut off; if it belongs to an
earlier exchange, this send opened no turn at all. Either way the work was
interrupted. A journal with no turn row at all is the same case with nothing to
disagree about.

The readings only diverge on a `completed` row, where an unmatched one might be
this very send's finished turn under a key we did not recognise. That stays
refused. Ambiguity resolves to no, because resuming finished work is the one
outcome never worth risking.
Five separators in the reconnect-offer redesign were written as raw NUL bytes
instead of the `\0` escape. The runtime strings were correct and the app behaved,
but git classifies a file containing a NUL as binary -- so the two central files
of that redesign rendered as "Binary file not shown" in review, and `rg` skipped
them silently, returning no matches rather than an error.

The escape produces the identical string, so the NUL separator is kept: the
previous separator was a space, and a workspace id containing one would corrupt
the join/split pair this grouping depends on.

Nothing could have caught this. Typecheck, lint, the quality gate, the
localization verifiers and the full suite all passed throughout, because none of
them look at file encoding. So this adds a check that does, wired into the
pre-commit hook where it costs nothing and catches the next one at the moment it
is written.

Two files already on main carry a raw NUL for the same reason -- one a template
separator, one a deliberately tricky test alphabet whose neighbours are all
written as escapes. They are grandfathered rather than fixed here, since they
belong to their own change, and the gate fails if the list ever grows or goes
stale.
…e-on-restart

# Conflicts:
#	src/main/runtime/agent-session-claim-key-retention.ts
#	src/main/runtime/agent-session-record-store.ts
@brennanb2025
brennanb2025 marked this pull request as ready for review September 17, 2026 02:07

@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

Two issues worth addressing before merge: the auto-reconnect preference can reconnect chats the user explicitly unchecked, and the already-live settlement branch in the resume path does not enforce consume-once.

Reviewed changes

  • Durable resume markers — teardown writes a marker (work identity, provider-handle root, launch id, TTL) for every session with a live provider child and a working projection, stored beside the records in one whole-file transaction.
  • Launch-generation adjacency — a new backup-less agent-session-launch.json is rotated once per launch; only markers stamped by the immediately preceding launch are claimed, and claimMarkers clears every durable marker in the same step.
  • Eligibility predicate — a marker and the journal's own turn/submission state must independently agree the work was cut off; lease, cursor-root, and expiry checks all fail closed.
  • Resume runner — consumes the marker before taking a resume-capable hold, behind per-session admission and a concurrency of 3.
  • Continuation path — a separate agentSession.restartContinue sends one fixed message only on explicit click, judged from the settled dispatch state; the automatic restartResume path contains no send.
  • RPC + renderer surface — four new agentSession.* methods, a grouped reconnect modal, a settings toggle, and a shared hostContext badge variant extracted from the sidebar meta row.
  • Supporting changes — teardown phase ordering/triggers, a store read/write file split, and a new check:no-nul-bytes gate.

ℹ️ The launch-adjacency clock advances on host install, not on app launch

rotateAgentSessionLaunchGeneration has exactly one production caller — install() in structured-agent-session-runtime.ts:198. A launch that never constructs the structured host (setting off with no persisted-store startup restore, or a degraded/renderer-failed startup) leaves both the launch id file and the durable markers untouched. The next launch that does install then reads the older id and computes previous as the launch before it, so markers written two app launches back can become claimable again — the exact case the tests pin as refused. Impact is bounded (24h TTL, and those sessions were genuinely working at the teardown that wrote them), but the documented invariant "only the launch immediately after it may act on it" is not what the wiring enforces.

Technical details
# Launch id is coupled to runtime install

## Affected sites
- `src/main/runtime/structured-agent-session-runtime.ts:198` — the only `rotateAgentSessionLaunchGeneration` call, inside `install()`.
- `src/main/runtime/agent-session-launch-generation.ts:18-20` — documents rotation as once per launch and rollback as under-offer only.
- `src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts:127-136` — filters by `launchId === launch.previous`.

## Sequence
1. L1 installs the host (id A), quits, writes marker M stamped `launchId: A`.
2. L2 never reaches `install()`; the id file still holds A and M stays durable because claim is lazy and host-gated.
3. L3 installs: reads A, computes `previous = A`, claims M. M is two app launches old but reported adjacent.

## Required outcome
The adjacency clock should advance per app launch (e.g. rotate at startup, outside the host install),
or the marker claim should additionally verify that the generating launch was the immediately
preceding app launch, not merely the preceding install.

## Open questions for the human
Is "launch" here intended to mean "app launch" or "structured-host install"? The comments and tests
say app launch; the wiring says install.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/renderer/src/components/NativeChatResumeOnRestartModal.tsx Outdated
…ethods

Two CI failures, both ours.

Static analysis. `Reflect.get` was adopted to clear the casting audit, and the
anti-slop rule forbids it -- the two gates disagree, and the rule text says what
both want: parse dynamic input into a named type once, then read typed fields off
it. Markers re-enter from a file this process may not have written and decide
whether an agent is handed a provider child, so they now go through a single zod
parse. Unknown keys still pass, and a malformed marker is still dropped rather
than thrown, so a bad entry cannot make a user's sessions unreadable. The launch
stamp is parsed the same way, the resume-admission refusal becomes a named error
carrying a typed `owner` instead of a bag assigned onto `new Error`, and the test
harness gets a named journal type instead of reaching into `unknown`.

Cross-version wire. The four restart methods are added to the manifest rather
than the count being bumped, so the suite now exercises them in both skews. They
are bare additions, not capability-negotiated: an unknown RPC method answers
`method_not_found`, which is explicit and visible during negotiation, unlike a
stream opcode that is dropped in silence. The whole `agentSession.*` surface
already sits behind its runtime capability, so an old client is told it does not
exist and never reaches a host method.

The stub's spies stay a flat map because callers iterate it asserting each entry
is a spy that did not run; a composer reassembles the member the host really
exposes. The manifest and its params builders move to their own module, which is
what keeps the suite under its line cap as the surface grows.
@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds restart recovery for structured native-chat sessions. Teardown records working sessions in a durable recovery capsule. Startup derives eligible candidates, consumes markers, and resumes sessions with bounded concurrency. Users can reconnect or continue sessions through new RPC methods and a renderer modal. Settings, notifications, translations, workspace grouping, and cross-version test fixtures are also updated.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 5761c

Restart recovery can silently omit interrupted chats, and a recovery failure during shutdown can leave cleanup incomplete. Address these paths before merging; baseline-cleanup and platform-test risks also remain unresolved.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 102 functions across 59 files. (2 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers the user impact, implementation, rationale, visual proof, testing, platform scope, and checklist. However, the required Linked Issue section contains “N/A”, although the templat… Add the issue reference in the required format, such as “Fixes #1234”. If no issue exists, create or link the appropriate tracking issue before merging.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: offering reconnection for native chats interrupted by an Orca restart.
Full details: Docstring Coverage

Explanation

Docstring coverage is 37.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 102 functions across 59 files. (2 skipped: 2 unsupported.)

Full details: Description check

Explanation

The description covers the user impact, implementation, rationale, visual proof, testing, platform scope, and checklist. However, the required Linked Issue section contains “N/A”, although the template states that a linked issue must always be provided.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: b59bc1d9-0753-480e-a199-6f2e28387845

📥 Commits

Reviewing files that changed from the base of the PR and between 7f5141a and d4a182f.

⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (57)
  • config/nul-byte-baseline.txt
  • config/scripts/check-no-nul-bytes.mjs
  • package.json
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-set.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-wiring.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-teardown-handoff-drain.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts
  • src/main/runtime/agent-session-launch-generation.test.ts
  • src/main/runtime/agent-session-launch-generation.ts
  • src/main/runtime/agent-session-operation-admission.ts
  • src/main/runtime/agent-session-record-store-file.ts
  • src/main/runtime/agent-session-record-store-write.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/agent-session-recovery-publish-fault.test.ts
  • src/main/runtime/agent-session-reservation-admission.test.ts
  • src/main/runtime/agent-session-resume-marker-file.ts
  • src/main/runtime/agent-session-resume-marker-store.test.ts
  • src/main/runtime/agent-session-resume-marker-store.ts
  • src/main/runtime/agent-session-store-serialization.ts
  • src/main/runtime/agent-session-store-transaction-queue.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-resume.ts
  • src/main/runtime/rpc/methods/structured-agent-session-schemas.ts
  • src/main/runtime/rpc/methods/structured-agent-session.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session.ts
  • src/main/runtime/structured-agent-session-runtime-teardown.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/main/startup/main-process-quit.ts
  • src/renderer/src/app-shell/AppRootSurfaces.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.tsx
  • src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts
  • src/renderer/src/components/native-chat-resume-on-restart-grouping.ts
  • src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx
  • src/renderer/src/components/sidebar/WorktreeHostContextBadge.tsx
  • src/renderer/src/components/sidebar/worktree-card-meta-row.tsx
  • src/renderer/src/components/ui/badge.tsx
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/shared/agent-session-restart-continuation.ts
  • src/shared/agent-session-resume-marker.ts
  • src/shared/default-global-settings.ts
  • src/shared/folder-workspace-worktree.test.ts
  • src/shared/folder-workspace-worktree.ts
  • src/shared/global-settings-types.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
  • src/shared/structured-agent-session-live-turn.ts
  • src/shared/structured-agent-session-projection.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +87 to +93
const 'journal')
if (target !== null && typeof 'object') {
Reflect.set(target, 'appendItem', async (_envelope: unknown, body: unknown) => {
if (input.noteFails) {
throw new Error('journal refused the note')
}
noted.push({ sessionId, text: String(Reflect.get(body ?? {}, 'text')) })

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace Reflect.get to clear the lint failure.

The static analysis gate reports anti-slop(no-reflect-get) at Line 87 and Line 93. Both sites read a known shape, so typed access works.

♻️ Proposed fix
   for (const [sessionId, session] of sessions) {
-    const 'journal')
+    const target: { appendItem?: unknown } | null = session.journal as {
+      appendItem?: unknown
+    } | null
     if (target !== null && typeof 'object') {
       Reflect.set(target, 'appendItem', async (_envelope: unknown, body: unknown) => {
         if (input.noteFails) {
           throw new Error('journal refused the note')
         }
-        noted.push({ sessionId, text: String(Reflect.get(body ?? {}, 'text')) })
+        const note = (body ?? {}) as { text?: unknown }
+        noted.push({ sessionId, text: String(note.text) })
       })
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const target = Reflect.get(session, 'journal')
if (target !== null && typeof target === 'object') {
Reflect.set(target, 'appendItem', async (_envelope: unknown, body: unknown) => {
if (input.noteFails) {
throw new Error('journal refused the note')
}
noted.push({ sessionId, text: String(Reflect.get(body ?? {}, 'text')) })
const target: { appendItem?: unknown } | null = session.journal as {
appendItem?: unknown
} | null
if (target !== null && typeof target === 'object') {
Reflect.set(target, 'appendItem', async (_envelope: unknown, body: unknown) => {
if (input.noteFails) {
throw new Error('journal refused the note')
}
const note = (body ?? {}) as { text?: unknown }
noted.push({ sessionId, text: String(note.text) })
🧰 Tools
🪛 GitHub Check: static analysis

[failure] 93-93: anti-slop(no-reflect-get)
src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts:93:46: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.


[failure] 87-87: anti-slop(no-reflect-get)
src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts:87:20: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.

Source: Linters/SAST tools

Comment thread src/main/runtime/agent-session-resume-marker-store.ts Outdated
Comment on lines +72 to +90
const kind = Reflect.get(value, 'kind')
return (kind === 'turn' || kind === 'submission') && isMarkerField(Reflect.get(value, 'id'))
}

export function isAgentSessionResumeMarker(value: unknown): value is AgentSessionResumeMarker {
if (typeof value !== 'object' || value === null) {
return false
}
const recordedAt = Reflect.get(value, 'recordedAt')
const trigger = Reflect.get(value, 'trigger')
return (
isMarkerField(Reflect.get(value, 'sessionId')) &&
isAgentSessionResumeWork(Reflect.get(value, 'work')) &&
isMarkerField(Reflect.get(value, 'providerHandleRoot')) &&
isMarkerField(Reflect.get(value, 'launchId')) &&
typeof recordedAt === 'number' &&
Number.isSafeInteger(recordedAt) &&
recordedAt >= 0 &&
(trigger === 'quit' || trigger === 'update')

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace Reflect.get reads so the static analysis check passes. The changed code reads dynamic fields with Reflect.get in three files. The repository's anti-slop(no-reflect-get) rule rejects that pattern, and the static analysis check reports a failure at each site. Narrow each dynamic value to a named record shape once, then read its fields by property access.

  • src/shared/agent-session-resume-marker.ts#L72-L90: narrow value to a record of the expected marker fields in isAgentSessionResumeWork and isAgentSessionResumeMarker, then read kind, id, sessionId, work, providerHandleRoot, launchId, recordedAt, and trigger from it.
  • src/main/runtime/agent-session-launch-generation.ts#L47-L47: narrow the JSON.parse result to { current?: unknown } and destructure current.
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts#L99-L100: narrow the caught error to { owner?: unknown }, and rename the local so it no longer shadows the owner parameter.
🧰 Tools
🪛 GitHub Check: static analysis

[failure] 84-84: anti-slop(no-reflect-get)
src/shared/agent-session-resume-marker.ts:84:30: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.


[failure] 83-83: anti-slop(no-reflect-get)
src/shared/agent-session-resume-marker.ts:83:19: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.


[failure] 81-81: anti-slop(no-reflect-get)
src/shared/agent-session-resume-marker.ts:81:19: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.


[failure] 80-80: anti-slop(no-reflect-get)
src/shared/agent-session-resume-marker.ts:80:22: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.


[failure] 73-73: anti-slop(no-reflect-get)
src/shared/agent-session-resume-marker.ts:73:70: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.


[failure] 72-72: anti-slop(no-reflect-get)
src/shared/agent-session-resume-marker.ts:72:16: Replace Reflect.get with typed property access. Parse dynamic input into a named domain type before reading it.

📍 Affects 3 files
  • src/shared/agent-session-resume-marker.ts#L72-L90 (this comment)
  • src/main/runtime/agent-session-launch-generation.ts#L47-L47
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts#L99-L100

Source: Linters/SAST tools

@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 — the new commits fix the two CI failures without introducing new concerns. The prior review's two open findings are in files these commits do not touch and remain outstanding, so this review is not an approval.

Reviewed changes

Since the prior pullfrog review (d4a182f7a4), the branch advanced only via 01476b52b3 and a merge from main.

  • Marker and launch-stamp parsing — agent-session-resume-marker.ts and agent-session-launch-generation.ts replace Reflect.get-style field reads with a single zod parse; unknown keys still pass, and a malformed marker is dropped rather than thrown.
  • Resume-admission refusal — structured-agent-session-restart-resume-runner.ts introduces a named error carrying a typed owner, replacing a property bag assigned onto Error.
  • Test-harness typing — HarnessJournal / HarnessSession replace unknown and Reflect in the claim suite's journal stubbing.
  • Cross-version surface — the four agentSession.restart* methods are declared in the wire manifest (with installableHost reassembling the flat spy map into the host's restartResume member), and the manifest and params builders move to structured-agent-session-surface-manifest.ts. The registered agentSession.* count and the manifest length now agree at 26.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai 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.

Actionable comments posted: 7


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 67143e6f-7fed-4b9b-94e3-2fb46c16766b

📥 Commits

Reviewing files that changed from the base of the PR and between d4a182f and c9fe3d4.

⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (60)
  • config/nul-byte-baseline.txt
  • config/scripts/check-no-nul-bytes.mjs
  • package.json
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-set.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-wiring.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-teardown-handoff-drain.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts
  • src/main/runtime/agent-session-launch-generation.test.ts
  • src/main/runtime/agent-session-launch-generation.ts
  • src/main/runtime/agent-session-operation-admission.ts
  • src/main/runtime/agent-session-record-store-file.ts
  • src/main/runtime/agent-session-record-store-write.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/agent-session-recovery-publish-fault.test.ts
  • src/main/runtime/agent-session-reservation-admission.test.ts
  • src/main/runtime/agent-session-resume-marker-file.ts
  • src/main/runtime/agent-session-resume-marker-store.test.ts
  • src/main/runtime/agent-session-resume-marker-store.ts
  • src/main/runtime/agent-session-store-serialization.ts
  • src/main/runtime/agent-session-store-transaction-queue.ts
  • src/main/runtime/rpc/methods/structured-agent-session-restart-resume.ts
  • src/main/runtime/rpc/methods/structured-agent-session-schemas.ts
  • src/main/runtime/rpc/methods/structured-agent-session.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session.ts
  • src/main/runtime/structured-agent-session-runtime-teardown.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/main/startup/main-process-quit.ts
  • src/renderer/src/app-shell/AppRootSurfaces.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.tsx
  • src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts
  • src/renderer/src/components/native-chat-resume-on-restart-grouping.ts
  • src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx
  • src/renderer/src/components/sidebar/WorktreeHostContextBadge.tsx
  • src/renderer/src/components/sidebar/worktree-card-meta-row.tsx
  • src/renderer/src/components/ui/badge.tsx
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/shared/agent-session-restart-continuation.ts
  • src/shared/agent-session-resume-marker.ts
  • src/shared/default-global-settings.ts
  • src/shared/folder-workspace-worktree.test.ts
  • src/shared/folder-workspace-worktree.ts
  • src/shared/global-settings-types.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
  • src/shared/structured-agent-session-live-turn.ts
  • src/shared/structured-agent-session-projection.ts
  • tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
  • tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts
  • tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/i18n/locales/en.json

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Comment thread config/scripts/check-no-nul-bytes.mjs Outdated
}

const fixed = cleared.filter((file) => grandfathered.has(file))
if (fixed.length > 0 && requested.length === 0) {

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Enforce baseline cleanup for staged files.

When lint-staged passes a cleaned grandfathered file, requested.length > 0 skips this check. The commit can retain a clean path in the baseline despite the stated shrink-only contract. Check fixed for requested files too.

Proposed fix
-if (fixed.length > 0 && requested.length === 0) {
+if (fixed.length > 0) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (fixed.length > 0 && requested.length === 0) {
if (fixed.length > 0) {

// predecessor and mistake it for its own, so this launch refuses adjacency in both directions.
it('claims no predecessor when the new stamp could not be written', async () => {
const first = await rotateAgentSessionLaunchGeneration(directory)
await chmod(directory, 0o500)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,100p' src/main/runtime/agent-session-launch-generation.test.ts
cat package.json | sed -n '1,80p'
rg -n 'windows|win32|runs-on:.*windows|matrix.*os|chmod\(.*0o500' .github package.json config src/main/runtime --glob '!node_modules'

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- test ---'
cat -n src/main/runtime/agent-session-launch-generation.test.ts | sed -n '1,130p'
printf '%s\n' '--- package scripts and platform declarations ---'
python3 - <<'PY'
import json
from pathlib import Path
p = Path('package.json')
data = json.loads(p.read_text())
print(json.dumps(data.get('scripts', {}), indent=2))
PY
rg -n --glob '!node_modules/**' --glob '!**/vendor/**' \
  'runs-on:|matrix:|os:|windows-latest|ubuntu-latest|macos-latest|win32|platform\(\)|process\.platform|chmod\(.*0o500|agent-session-launch-generation' \
  .github package.json config src/main/runtime
printf '%s\n' '--- test configuration ---'
rg -n --glob '!node_modules/**' \
  'vitest|jest|mocha|test.*include|include.*test|platform|windows' \
  package.json vite.config.* vitest.config.* jest.config.* .github 2>/dev/null || true

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- focused test ---'
sed -n '1,110p' src/main/runtime/agent-session-launch-generation.test.ts
printf '%s\n' '--- package scripts ---'
python3 - <<'PY'
import json
with open('package.json') as f:
    data = json.load(f)
for key, value in data.get('scripts', {}).items():
    print(f'{key}: {value}')
PY
printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/*' '.github/actions/*' | sort
printf '%s\n' '--- workflow platform lines ---'
for f in $(git ls-files '.github/workflows/*' '.github/actions/*'); do
  case "$f" in
    *.yml|*.yaml) rg -n -e 'runs-on:' -e 'matrix:' -e 'os:' -e 'windows-latest' -e 'ubuntu-latest' -e 'macos-latest' "$f" || true ;;
  esac
done
printf '%s\n' '--- focused platform guards and write injection ---'
rg -n -F 'process.platform' src/main/runtime/agent-session-launch-generation.test.ts src/main/runtime
rg -n -e 'chmod' -e 'writeFile' -e 'rename' -e 'mock.*write' -e 'spyOn.*write' src/main/runtime/agent-session-launch-generation.test.ts src/main/runtime

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- unit-test workflow ---'
sed -n '1,180p' .github/workflows/unit-tests.yml
printf '%s\n' '--- pull-request test workflow platform/test references ---'
rg -n -C 3 -e 'agent-session-launch-generation|vitest run|pnpm run test|matrix:|runs-on:' .github/workflows/unit-tests.yml .github/workflows/pr.yml .github/workflows/pr-test-loc.yml
printf '%s\n' '--- implementation ---'
sed -n '1,220p' src/main/runtime/agent-session-launch-generation.ts
printf '%s\n' '--- vitest config include/exclude ---'
rg -n -C 3 -e 'include|exclude|environment|test' config/vitest.config.ts

Repository: stablyai/orca

Length of output: 22649


🌐 Web query:

Node.js fs promises chmod Windows only write permission official documentation

💡 Result:

<search_synthesis>
In Node.js, the fsPromises.chmod method behaves differently on Windows compared to POSIX-compliant systems [1][2]. While the method signature is identical, the following limitations apply on Windows: 1. Write Permission Only: On Windows, only the write permission (represented by the read-only file attribute) can be manipulated using chmod [1][2]. Other POSIX permission bits are ignored [1]. 2. No User/Group Distinction: There is no distinction among owner, group, or others when applying permissions; the change effectively acts on the file globally in terms of the read-only attribute [1][3]. 3. Official Documentation Caveats: While the official Node.js documentation for fsPromises.chmod often lists the mode argument as "Not supported on Windows" in newer versions [4][5][6], this refers to the full POSIX-style permission set. Historically and in practice, the function remains available and maps to the underlying Windows C runtime _wchmod function, which specifically impacts the read-only attribute [7][1]. In summary, you cannot use fsPromises.chmod on Windows to set fine-grained POSIX-style permissions. You can only use it to toggle whether a file is read-only [1][2]. If you require granular permission control on Windows (such as NTFS ACLs), you must use platform-specific tools or native modules, as Node.js&#39;s built-in fs module does not provide this functionality [2].
</search_synthesis>

<source_evidence>

<title>doc: document file mode caveats on Windows · a5c5714 · nodejs/node</title> https://github.com/nodejs/node/commit/a5c571424a # Commit: nodejs/node@a5c5714 - Repository: nodejs/node | Node.js JavaScript runtime ✨🐢🚀✨ | 117K stars | JavaScript ## doc: document file mode caveats on Windows - Author: [`@joyeecheung`](https://github.com/joyeecheung) - Committer: [`@targos`](https://github.com/targos) - Date: 2018-05-09T17:26:42Z - SHA: a5c571424ac9009203126b8e2c9ed51ae26dbf7d - Changes: +8 -3 (1 files) - On Windows only the write permission (read-only bit) can be manipulated, and there is no distinction among owner, group or others. - mkdir on Windows does not support the mode argument. PR-URL: https://github.com/nodejs/node/pull/20636 Fixes: https://github.com/nodejs/node/issues/20498 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anatoli Papirovski <apapirovski@mac.com> Backport-PR-URL: https://github.com/nodejs/node/pull/21172 --- ## Files Changed | File | Status | Add | Del | | --- | --- | --- | --- | | doc/api/fs.md | modified | +8 | -3 | --- ## Diffs ### doc/api/fs.md ```diff @@ -1089,6 +1089,10 @@ For example, the octal value `0o765` means: * The group may read and write the file. * Others may read and execute the file. +Caveats: on Windows only the write permission can be changed, and the +distinction among the permissions of group, owner or others is not +implemented. + ## fs.chmodSync(path, mode) <!-- YAML added: v0.6.7 @@ -1987,7 +1991,7 @@ changes: --> * `path` {string|Buffer|URL} -* `mode` {integer} **Default:** `0o777` +* `mode` {integer} Not supported on Windows. **Default:** `0o777`. * `callback` {Function} * `err` {Error} @@ -2007,7 +2011,7 @@ changes: --> * `path` {string|Buffer|URL} -* `mode` {integer} **Default:** `0o777` +* `mode` {integer} Not supported on Windows. **Default:** `0o777`. Synchronously creates a directory. Returns `undefined`. This is the synchronous version of [`fs.mkdir()`][]. @@ -2127,7 +2131,8 @@ changes: Asynchronous file open. See open(2). `mode` sets the file mode (permission and sticky bits), but only if the file was -created. +created. Note that on Windows only the write permission can be manipulated, +see [`fs.chmod()`][]. The callback gets two arguments `(err, fd)`. ``` <title>docs/5_security/permissions.md</title> https://github.com/ehmicky/cross-platform-node-guide/blob/main/docs/5_security/permissions.md # docs/5_security/permissions.md - Branch: main - Repository: ehmicky/cross-platform-node-guide --- # 🔒 Permissions ## Core concepts Unix uses POSIX permissions but Windows is based on a combination of: - file attributes like `readonly`, `hidden` and `system`. `winattr` and `hidefile` can be used to manipulate those. - ACLs (also called NTFS permissions or just "file permissions"). - share permissions. ## Lack of support Node.js does not support Windows permissions. `fs.chmod()`, `fs.stat()`&`#39`;s `mode`, `fs.access()`, `fs.open()`&`#39`;s `mode`, `fs.mkdir()`&`#39`;s `options.mode` and `process.umask()` only work on Unix with some minor exceptions: - `fs.access()` `F_OK` works. - `fs.access()` `W_OK` checks the `readonly` file attribute on Windows. This is quite limited as it does not check other file attributes nor ACLs. - The `readonly` file attribute is checked on Windows when the `write` POSIX permission is missing for any user class (`user`, `group` or `others`). On the other hand `fs.open()` works correctly on Windows where flags are being translated to Windows-specific file attributes and permissions. ## Other differences On Windows, to execute files their extension must be listed in the environment variable `PATHEXT`. Directories can be locked on Windows. Erasing or removing them will fail. This can be solved by retrying few milliseconds later using the `maxRetries` option of `fs.rm()`, `graceful-fs` or `rimraf`. Finally `fs.lchmod()` is only available on Mac. ## Summary File permissions are not cross-platform in Node.js. **Next** _(🔒 Users)_\ **Previous** _(🔒 Security)_\ **Top** <title>Node.js fsPromises.chmod() Method - GeeksforGeeks</title> https://www.geeksforgeeks.org/node-js/node-js-fspromises-chmod-method/ Node.js fsPromises.chmod() Method - GeeksforGeeks # Node.js fsPromises.chmod() Method Last Updated : 8 Oct, 2021 The fsPromises.chmod() method is used to change the permissions of a given path. These permissions can be specified using string constants or octal numbers that correspond to their respective file modes. Note: The Windows platform only supports the changing of the write permission. It does not support the distinction between the permissions of the user, group, or others. It changes the permissions of a file then resolves the Promise with no arguments upon success. Syntax: ``` fsPromises.chmod( path, mode) ``` Parameters: This method accepts two parameters as mentioned above and described below: - path: It is a string, Buffer or URL that denotes the path of the file of which the permission has to be changed. - mode: It is string or octal integer constant that denotes the permission to be granted. The logical or operator can be used to separate multiple permissions. Below examples illustrate the fsPromises.chmod() method in Node.js: Example 1: This example shows the usage of octal integer constants to give the file permissions. javascript ``` // Node.js program to demonstrate the // fsPromises.chmod() method // Import the filesystem module const fs = require(&`#39`;fs&`#39`;); const fsPromises = require(&`#39`;fs&`#39`;).promises; console.log("Granting only read access to user"); fsPromises.chmod("example.txt", 0o400 ) console.log("\nReading the file contents"); console.log(fs.readFileSync("example.txt", &`#39`;utf8&`#39`;)); console.log("\nTrying to write to file"); try { fs.writeFileSync(&`#39`;example.txt&`#39`;, "This file has now been edited."); } catch (e) { console.log("Error Code:", e.code); } console.log("\nGranting read and write access to user"); fsPromises.chmod("example.txt", 0o600 ) console.log("Trying to write to file"); fs.writeFileSync(&`#39`;example.txt&`#39`;, "This file has now been edited."); console.log("\nReading the file contents"); console.log(fs.readFileSync("example.txt", &`#39`;utf8&`#39`;)); ``` Output: ``` Granting only read access to user Reading the file contents This file has now been edited. Trying to write to file Error Code: EPERM Granting read and write access to user Trying to write to file Reading the file contents This file has now been edited. ``` Example 2: This example shows the usage of string constants and or operator to give the file permissions ``` // Node.js program to demonstrate the // fsPromises.chmod() method // Import the filesystem module const fs = require(&`#39`;fs&`#39`;); const fsPromises = fs.promises; // Grant only read permission to user console.log("Granting only read access to user"); fsPromises.chmod("example.txt", fs.constants.R_OK); // Reading the file console.log("File Contents:", fs.readFileSync("example.txt", &`#39`;utf8&`#39`;)); // Trying to write to file try { console.log("\nTrying to write to file"); fs.writeFileSync(&`#39`;example.txt&`#39`;, "This file now has been edited."); } catch (e) { console.log( "Error Occurred, Error Code:", e.code); } // Granting both read and // write permission console.log("\nGranting both read and " + &`#39`;write permission to user"); fsPromises.chmod("example.txt", fs.constants.R_OK | fs.constants.W_OK); // Check the file mode console.log("Current File Mode:", fs.statSync("example.txt").mode); console.log("Trying to write to file"); try { fs.writeFileSync(&`#39`;example.txt&`#39`;, "This file now has been edited."); } catch (e) { console.log("Error ", e.code); } console.log("File Contents:", fs.readFileSync("example.txt", &`#39`;utf8&`#39`;)); ``` Output: ``` Granting only read access to user File Contents: This file has now been edited. Trying to write to f…[truncated] <title>File system | Node.js v24.20.0 Documentation</title> https://nodejs.org/docs/latest-v24.x/api/fs.html ##### filehandle.chmod(mode)# ... Modifies the permissions on the file. See chmod(2). ... Promises.access( ... #### fsPromises.chmod(path, mode)# ... Changes the permissions of a file. ... #### fsPromises.lchmod(path, mode)# ... #### fsPromises.mkdir(path[, options])# ... - `path` | | - `options` | - - `recursive` Default:`false` - `mode` | Not supported on Windows. See File modes for more details. Default:`0o777`. - Returns: Upon success, fulfills with`undefined` if`recursive` is`false`, or the first directory path created if`recursive` is`true`. ... bits), or an object ... ` property and a` ... ` property indicating whether parent directories should be ... . Calling`fsPromises. ... ()` when`path` is a directory that exists results in a rejection only when`recursive` is false ... #### fsPromises.open(path, flags[, mode])# ... - `path` | | - `flags` | See support of file system`flags`. Default:`&`#39`;r&`#39`;`. - `mode` | Sets the file mode (permission and sticky bits) if the file is created. See File modes for more details. Default:`0o666`(readable and writable) - Returns: Fulfills with a object. <title>Promises API | Node.js 26.8.1 Documentation</title> https://beta.docs.nodejs.org/fs/promises-api ### filehandle.chmod ... `filehandle.chmod(mode): Promise` ... Modifies the permissions on the file. See `chmod(2)`. ... ## fsPromises.chmod ... `fsPromises.chmod(path, mode): Promise` ... Changes the permissions of a file. ... ## fsPromises.lchmod ... Not supported on Windows. See File modes for more details. Default: `0o777`. ... Sets the file mode ... permission and sticky bits) if the file is created. See File modes for more details. Default: `0o666` (readable and writable)

Citations:


Use a deterministic write failure.

If this test runs on Windows, chmod(directory, 0o500) does not provide reliable POSIX directory-write protection. A privileged Unix process can also bypass the mode bits. The second rotation can therefore write successfully, so second.previous is not null and the test fails.

Inject or mock writeTempFileDurable so the test always exercises the intended failure path.

): Promise<void> {
// Drain an in-flight recovery before stopping children; recovery may still
// be writing lifecycle rows or acquiring a replacement child.
await installed.waitForRecovery()

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,110p' src/main/runtime/structured-agent-session-runtime-teardown.ts
sed -n '270,330p' src/main/runtime/structured-agent-session-runtime.ts
rg -n 'tearDownRuntime|stopStructuredAgentSessionRuntime|closeAll\(' src/main

Repository: stablyai/orca

Length of output: 18793


🏁 Script executed:

#!/bin/bash
sed -n '130,180p' src/main/runtime/structured-agent-session-runtime.ts
sed -n '315,370p' src/main/runtime/structured-agent-session-runtime.ts
sed -n '1,95p' src/main/runtime/structured-agent-session-runtime-teardown.ts
sed -n '120,155p' src/main/startup/main-process-quit.ts

Repository: stablyai/orca

Length of output: 8989


Continue cleanup if the initial recovery drain fails.

If installed.waitForRecovery() rejects, tearDownRuntime exits before installed.host.flushAllStreamedEvents() and installed.adapter.closeAll() run. stopStructuredAgentSessionRuntime() only records the failed runtime for a later stop; it does not provide equivalent cleanup. Provider children and session resources can remain active during shutdown.

Capture this failure with the other teardown failures, then continue the cleanup sequence.

Proposed fix
-  await installed.waitForRecovery()
   const failures: unknown[] = []
+  try {
+    await installed.waitForRecovery()
+  } catch (error) {
+    failures.push(error)
+  }

})
// Rotated once per launch, before anything can act on a marker. Deliberately NOT inside the
// record store directory: see the module for why its rollback domain must stay separate.
const launchGeneration = await rotateAgentSessionLaunchGeneration(deps.stateDirectory)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/main/runtime/structured-agent-session-runtime.ts \
  --match 'ensureStructuredAgentSessionHost|install' --view expanded

rg -n -C 5 \
  'class StructuredAgentSessionHost|constructor\s*\(|function createStructuredClaudeRuntimeAdapter|class StructuredAgentSessionAdapterRouter|function setStructuredAgentSessionHost' \
  src/main --glob '*.ts'

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runtime file outline ---'
ast-grep outline src/main/runtime/structured-agent-session-runtime.ts

printf '%s\n' '--- runtime file relevant sections ---'
cat -n src/main/runtime/structured-agent-session-runtime.ts | sed -n '1,280p'

printf '%s\n' '--- exact symbol references ---'
rg -n -C 8 \
  'ensureStructuredAgentSessionHost|rotateAgentSessionLaunchGeneration|StructuredAgentSessionHost|createStructuredClaudeRuntimeAdapter|StructuredAgentSessionAdapterRouter|setStructuredAgentSessionHost' \
  src/main/runtime --glob '*.ts' --glob '!*.test.ts' | head -n 1200

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

set -euo pipefail
cat -n src/main/runtime/structured-agent-session-runtime.ts | sed -n '1,280p'
rg -n -C 8 'ensureStructuredAgentSessionHost|rotateAgentSessionLaunchGeneration|StructuredAgentSessionHost|createStructuredClaudeRuntimeAdapter|StructuredAgentSessionAdapterRouter|setStructuredAgentSessionHost' src/main/runtime --glob '*.ts' --glob '!*.test.ts' | head -n 1200

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- install remainder ---'
cat -n src/main/runtime/structured-agent-session-runtime.ts | sed -n '275,380p'

printf '%s\n' '--- exact constructor/function definitions ---'
rg -n -l \
  'export class StructuredAgentSessionHost|class StructuredAgentSessionHost|export class StructuredAgentSessionAdapterRouter|class StructuredAgentSessionAdapterRouter|export function createStructuredClaudeRuntimeAdapter|function createStructuredClaudeRuntimeAdapter|export class CodexStructuredSessionAdapter|class CodexStructuredSessionAdapter|function rotateAgentSessionLaunchGeneration|export async function rotateAgentSessionLaunchGeneration' \
  src/main --glob '*.ts' --glob '!*.test.ts' |
while IFS= read -r file; do
  echo "--- $file ---"
  rg -n -C 12 \
    'export class StructuredAgentSessionHost|class StructuredAgentSessionHost|export class StructuredAgentSessionAdapterRouter|class StructuredAgentSessionAdapterRouter|export function createStructuredClaudeRuntimeAdapter|function createStructuredClaudeRuntimeAdapter|export class CodexStructuredSessionAdapter|class CodexStructuredSessionAdapter|function rotateAgentSessionLaunchGeneration|export async function rotateAgentSessionLaunchGeneration' \
    "$file"
done

Repository: stablyai/orca

Length of output: 11630


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/main/runtime/structured-agent-session-runtime.ts | sed -n '275,380p'
rg -n -l 'StructuredAgentSessionHost|StructuredAgentSessionAdapterRouter|createStructuredClaudeRuntimeAdapter|CodexStructuredSessionAdapter|rotateAgentSessionLaunchGeneration' src/main --glob '*.ts' --glob '!*.test.ts' | while IFS= read -r file; do
  echo "--- $file ---"
  rg -n -C 12 'class StructuredAgentSessionHost|class StructuredAgentSessionAdapterRouter|function createStructuredClaudeRuntimeAdapter|class CodexStructuredSessionAdapter|function rotateAgentSessionLaunchGeneration' "$file"
done

Repository: stablyai/orca

Length of output: 3583


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- host constructor ---'
rg -n -C 30 'constructor\s*\(' src/main/native-chat/agent-session-wire/structured-agent-session-host.ts | head -n 220

printf '%s\n' '--- codex constructor ---'
cat -n src/main/codex/codex-structured-session-adapter.ts | sed -n '45,115p'

printf '%s\n' '--- claude adapter factory and constructor ---'
cat -n src/main/runtime/structured-claude-runtime-adapter.ts | sed -n '1,100p'
rg -n -C 20 'constructor\s*\(' src/main/claude/claude-structured-session-adapter.ts | head -n 180

printf '%s\n' '--- launch generation helper ---'
cat -n src/main/runtime/agent-session-launch-generation.ts | sed -n '1,140p'

Repository: stablyai/orca

Length of output: 18896


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 30 'constructor\s*\(' src/main/native-chat/agent-session-wire/structured-agent-session-host.ts | head -n 220
cat -n src/main/codex/codex-structured-session-adapter.ts | sed -n '45,115p'
cat -n src/main/runtime/structured-claude-runtime-adapter.ts | sed -n '1,100p'
rg -n -C 20 'constructor\s*\(' src/main/claude/claude-structured-session-adapter.ts | head -n 180
cat -n src/main/runtime/agent-session-launch-generation.ts | sed -n '1,140p'

Repository: stablyai/orca

Length of output: 18765


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- host constructor continuation ---'
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-host.ts | sed -n '85,180p'

printf '%s\n' '--- direct post-rotation helpers ---'
rg -n -C 8 \
  'function createStructuredAgentSessionHostHandoff|createStructuredAgentSessionHostHandoff|class StructuredAgentSessionBackgroundTaskChannel|class StructuredAgentSessionHostRuntimeState|function createRestartReconciler|setStructuredAgentSessionHost' \
  src/main/native-chat/agent-session-wire --glob '*.ts' --glob '!*.test.ts' | head -n 800

printf '%s\n' '--- explicit throws in direct construction files ---'
rg -n -C 3 'throw new|throw ' \
  src/main/native-chat/agent-session-wire/structured-agent-session-host.ts \
  src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts \
  src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.ts \
  src/main/native-chat/agent-session-wire/structured-agent-session-handoff.ts \
  src/main/native-chat/agent-session-wire/structured-agent-session-registry.ts \
  src/main/codex/codex-structured-session-adapter.ts \
  src/main/claude/claude-structured-session-adapter.ts \
  src/main/runtime/structured-claude-runtime-adapter.ts || true

Repository: stablyai/orca

Length of output: 25144


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-host.ts | sed -n '85,180p'
rg -n -C 8 'createStructuredAgentSessionHostHandoff|class StructuredAgentSessionBackgroundTaskChannel|class StructuredAgentSessionHostRuntimeState|function createRestartReconciler|setStructuredAgentSessionHost' src/main/native-chat/agent-session-wire --glob '*.ts' --glob '!*.test.ts' | head -n 800
rg -n -C 3 'throw new|throw ' src/main/native-chat/agent-session-wire/structured-agent-session-host.ts src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.ts src/main/native-chat/agent-session-wire/structured-agent-session-handoff.ts src/main/native-chat/agent-session-wire/structured-agent-session-registry.ts src/main/codex/codex-structured-session-adapter.ts src/main/claude/claude-structured-session-adapter.ts src/main/runtime/structured-claude-runtime-adapter.ts || true

Repository: stablyai/orca

Length of output: 25016


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- host handoff factory ---'
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts | sed -n '1,150p'

printf '%s\n' '--- host restore/restart wiring ---'
rg -n -l 'createStructuredAgentSessionHostRestore|structuredAgentSessionRestartResumeSurfaces|createStructuredAgentSessionRestartResume' src/main/native-chat/agent-session-wire --glob '*.ts' --glob '!*.test.ts' |
while IFS= read -r file; do
  echo "--- $file ---"
  rg -n -C 12 'createStructuredAgentSessionHostRestore|structuredAgentSessionRestartResumeSurfaces|createStructuredAgentSessionRestartResume' "$file"
done

printf '%s\n' '--- retry/rotation tests ---'
rg -n -C 8 'rotateAgentSessionLaunchGeneration|ensureStructuredAgentSessionHost|install.*reject|retry.*install|launchGeneration' src/main --glob '*.test.ts' | head -n 1000

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts | sed -n '1,150p'
rg -n -l 'createStructuredAgentSessionHostRestore|structuredAgentSessionRestartResumeSurfaces|createStructuredAgentSessionRestartResume' src/main/native-chat/agent-session-wire --glob '*.ts' --glob '!*.test.ts' | while IFS= read -r file; do
  echo "--- $file ---"
  rg -n -C 12 'createStructuredAgentSessionHostRestore|structuredAgentSessionRestartResumeSurfaces|createStructuredAgentSessionRestartResume' "$file"
done
rg -n -C 8 'rotateAgentSessionLaunchGeneration|ensureStructuredAgentSessionHost|install.*reject|retry.*install|launchGeneration' src/main --glob '*.test.ts' | head -n 1000

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PTY gate attach ---'
rg -n -C 15 'attachRecordLookup|class .*Pty.*Gate' src/main/runtime/agent-session-pty-write-gate.ts

printf '%s\n' '--- lease renewal startup ---'
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts | sed -n '1,130p'
rg -n -C 12 'startLeaseRenewal' src/main/native-chat/agent-session-wire --glob '*.ts' --glob '!*.test.ts'

Repository: stablyai/orca

Length of output: 13892


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 15 'attachRecordLookup|class .*Pty.*Gate' src/main/runtime/agent-session-pty-write-gate.ts
cat -n src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.ts | sed -n '1,130p'
rg -n -C 12 'startLeaseRenewal' src/main/native-chat/agent-session-wire --glob '*.ts' --glob '!*.test.ts'

Repository: stablyai/orca

Length of output: 13838


Rotate the launch generation once per process launch. ensureStructuredAgentSessionHost() clears installing when install() rejects, so a later call starts a new attempt. Each attempt runs rotateAgentSessionLaunchGeneration() at line 198. If the synchronous adapter or host setup after that line throws, the retry writes a second generation and refuses markers from the actual previous launch. Move rotation to process-launch scope and reuse that generation across installation retries.

Comment thread src/renderer/src/components/NativeChatResumeOnRestartModal.tsx Outdated

@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 critical issues — one stale invariant comment inline, plus a description that still documents the removed launch-scoping.

Reviewed changes

Since the prior pullfrog review (52ffed7114), the branch advanced by one commit, b3e29dcd58, touching 30 files (+474/−621).

  • Durable markers moved into an isolated advisory capsule — AgentSessionRecoveryCapsule replaces the store-resident resumeMarkers: record publishes the whole set, take reads, clears in the same locked step, and returns only fresh markers. The capsule is its own file with no .bak, so it no longer shares a rollback domain with the session store or its transaction queue.
  • Launch-generation adjacency removed — agent-session-launch-generation.ts and its stamp file are deleted, and the marker's launchId becomes teardownId, used only to key the continuation's deterministic clientOperationId. A failed clear now leaves the capsule for a later launch instead of refusing it.
  • Teardown record is bounded and diagnosed — record-resume-markers runs behind withPhaseTimeout(…, 2s), turning a stall into a logged failure rather than a wait, and still runs before eviction settles turns.
  • Shared whole-file lock generalized — agent-session-store-transaction-lock.ts becomes src/main/file-transaction-lock.ts with an optional retries override (0 for the capsule); the store, its loader, and the capsule all use it.
  • Coverage — new agent-session-recovery-capsule.test.ts (take-once across competing lock owners, failed-clear exposes nothing then allows a later take, corrupt capsule refuses without partial candidates, freshness window, legacy store markers ignored and preserved) plus claim/ownership/drain tests for the capsule. The four affected suites run green at this head (82 tests), ownership green (16).

ℹ️ The description still specifies launch-scoped markers

The PR body's "Why the marker is launch-scoped" and "Guarantee → Markers only survive one launch" sections still present launch adjacency as the mechanism that stops a stale marker being acted on. This commit deletes that module and its check, so the description no longer describes the shipped design — a reviewer reading only the body would believe a stronger invariant than the code enforces.

Technical details
# PR body describes the removed launch-adjacency guard

## Affected sites
- `b3e29dcd58` — deletes `src/main/runtime/agent-session-launch-generation.ts` and the `launchId === previous` filter in `structured-agent-session-restart-resume-host.ts`.
- `src/shared/agent-session-resume-marker.ts:54` — `teardownId` is documented as "not launch ancestry".

## Required outcome
- Update the safety sections to the capsule model (isolated file, no backup, consumed atomically on `take`), or state plainly that adjacency was dropped and why the remaining predicate still bounds a stale marker.
- Confirm the intent: a capsule that survives a failed clear, or a launch that never installs the host, can now be taken by a later launch within the 24h TTL, so "only the immediately preceding launch" is intentionally relaxed rather than enforced.

## Open questions for the human
Is dropping adjacency deliberate and permanent, or an intermediate step toward rotating a launch id inside the capsule?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 67daa053-6186-4e10-93f4-30305440e149

📥 Commits

Reviewing files that changed from the base of the PR and between 519f8c7 and 5761c9f.

📒 Files selected for processing (28)
  • src/main/file-transaction-lock.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-test-harness.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-set.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-teardown-handoff-drain.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts
  • src/main/runtime/agent-session-record-store-file.ts
  • src/main/runtime/agent-session-record-store-security.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/agent-session-recovery-capsule.test.ts
  • src/main/runtime/agent-session-recovery-capsule.ts
  • src/main/runtime/agent-session-store-transaction-queue.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx
  • src/renderer/src/components/NativeChatResumeOnRestartModal.tsx
  • src/renderer/src/components/native-chat-restart-action-notifications.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/shared/agent-session-resume-marker.ts
  • src/shared/structured-agent-session-projection.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-runner.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-teardown-handoff-drain.test.ts
  • src/main/runtime/agent-session-record-store-file.ts
  • src/renderer/src/i18n/locales/en.json

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.

await this.publish([])
return markers.filter((marker) => !isExpiredAgentSessionResumeMarker(marker, now))
},
{ retries: 0 }

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,115p' src/main/runtime/agent-session-recovery-capsule.ts
sed -n '1,330p' src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
sed -n '1,90p' src/main/file-transaction-lock.ts
rg -n "restartResume|\.list\(|recoveryCapsule\.take|waitForRecovery|flushAllStreamedEvents|recordResume" src/main --glob '*.ts'

Repository: stablyai/orca

Length of output: 41943


🏁 Script executed:

#!/bin/bash
sed -n '1,125p' src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts
sed -n '1,105p' src/main/runtime/structured-agent-session-runtime-teardown.ts
sed -n '100,175p' src/main/runtime/structured-agent-session-runtime.ts
sed -n '300,340p' src/main/runtime/structured-agent-session-runtime.ts
sed -n '1,120p' src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
sed -n '245,285p' src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
sed -n '1,120p' src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts
sed -n '300,345p' src/main/native-chat/agent-session-wire/structured-agent-session-restart-claim.test.ts
sed -n '1,95p' src/main/runtime/structured-agent-session-runtime-teardown.test.ts 2>/dev/null || true
rg -n "createStructuredAgentSessionRuntime|structuredAgentSessionRuntimeTeardown|waitForRecovery|flushAllStreamedEvents|recordResumeMarkers|restartResume\.list" src/main --glob '*.ts'

Repository: stablyai/orca

Length of output: 35906


🏁 Script executed:

#!/bin/bash
rg -n -C 5 "stopStructuredAgentSessionRuntime|ensureStructuredAgentSessionHost|waitForStructuredAgentSessionRecovery|structuredAgentSessionTeardown" . --glob '*.ts' --glob '*.tsx' --glob '*.js'
rg -n -C 8 "restartResume\.list\(\)|agentSession\.restartResume|restartResumable" src --glob '*.ts' --glob '*.tsx'
rg -n -C 6 "start.*runtime|stop.*runtime|teardown.*startup|startup.*teardown" src/main --glob '*test*.ts' --glob '*.ts'

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
rg -n -l "stopStructuredAgentSessionRuntime|ensureStructuredAgentSessionHost|waitForStructuredAgentSessionRecovery" src/main/startup src/main/runtime src/main/orcad 2>/dev/null
sed -n '1,180p' src/main/startup/main-process-quit.ts
rg -n -C 12 "ensureStructuredAgentSessionHost|stopStructuredAgentSessionRuntime" src/main/startup/main-process-runtime-service.ts src/main/orcad/orcad-entry.ts 2>/dev/null
rg -n -C 12 "stopStructuredAgentSessionRuntime|ensureStructuredAgentSessionHost" src/main --glob '*.ts' --glob '!*.test.ts' --glob '!*.spec.ts' | head -n 240

Repository: stablyai/orca

Length of output: 37670


🏁 Script executed:

#!/bin/bash
sed -n '180,285p' src/main/startup/main-process-quit.ts
sed -n '1,220p' src/main/startup/desktop-startup-ordering.test.ts
rg -n -C 8 "structuredAgentSessionShutdown|stopStructuredAgentSessionRuntime|app\.quit|quitTeardown" src/main/startup/main-process-quit.ts

Repository: stablyai/orca

Length of output: 27263


Retry take() during capsule teardown.

record() and take() use the same file lock. Because take() passes { retries: 0 }, it can fail immediately while record() publishes markers. claimMarkers() catches that failure, caches claimed = [], and does not retry, so restartResume.list() returns no launch offer.

The shutdown path waits before app.quit(), but stopStructuredAgentSessionRuntime() clears installing before awaiting teardown. A new host can therefore start while the previous record() is still pending.

Keep record() bounded, but use the default bounded retry policy for take().

Proposed fix
-      { retries: 0 }
+      // Startup can wait briefly for an in-progress teardown marker write.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ retries: 0 }
// Startup can wait briefly for an in-progress teardown marker write.

@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 found.

Reviewed changes

Since the prior pullfrog review (b3e29dcd58), the branch advanced by one commit, 5761c9f368, touching 9 files (+286/−93).

  • Tightened the journal-agreement predicate — the former acceptedSubmissionWasCutOff becomes submissionWorkWasCutOff, and every non-rejected dispatch (pending, unknown, accepted) is now judged by the journal's newest turn. Previously a pending/unknown send returned "cut off" unconditionally, so a turn the provider completed without a delivery acknowledgement could still be offered.
  • Simplified the submission branch — rejected is the only dispatch state that refuses outright; the completed/running turn ambiguity now resolves to no whether or not a turn can be linked back to the marked send.
  • Extracted renderer outcome reporting — the new native-chat-restart-action-notifications.ts buckets each requested chat into succeeded / unconfirmed (pending|unknown) / refused and toasts each bucket, keeping the existing success copy.
  • Surfaced failed actions instead of closing silently — both reconnect and reconnect-and-continue now catch, announce unconfirmed for the requested chats, and resolve; the automatic reconnect path announces unconfirmed rather than letting the rejection escape.
  • Fixed the stale teardownId comment — structured-agent-session-working-at-teardown.ts:75 now describes a per-teardown identity for continuation dedup, retiring the prior review's open thread.
  • Coverage — ownership (refuses a completed turn even when its original delivery acknowledgement is unknown), resume-set (pending/unknown + completed turn), and modal (refused/unconfirmed/lost-response) tests, plus the matching plural i18n keys.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 found.

Reviewed changes

Since the prior pullfrog review (5761c9f368), the branch advanced one commit, c6ce0e1ad5, touching 4 files (+87/−3).

  • Admission recheck now sees queued provider lifecycle — runSettledAgentSessionMutation flushes the session's streamed event sink before invoking plan.beforeRun(), so the restart continuation's supersession guard judges the journal with a completion/message the host already accepted applied, rather than a stale snapshot.
  • Thrown sends are reported as unverifiable — the continuation distinguishes its own pre-dispatch RestartContinuationSupersededError (rethrown → refused) from any other send rejection (logged, returned as unknown); performSend only throws at its post-dispatch persistence step, where a lost reply cannot prove non-delivery.
  • Coverage — the ownership suite adds a completion/message test proving a queued event flips the decision to agent_session_restart_work_superseded, plus a settlement-failure test pinning unknown with no redispatch. I confirmed the two queued-event tests fail when the flush is removed, so they exercise the new guard.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 found.

Reviewed changes

Since the prior pullfrog review, the branch advanced one commit, a6bd3a5549 ("fix(native-chat): rederive recovery evidence before continuing"), touching 17 files (+395/−112).

  • Teardown re-derives each marker after the provider stops — captureMarkers snapshots candidate markers into memory before eviction, and evict-owned-sessions calls confirmStoppedMarker after the child is proven stopped and its tail drained (via onStoppedWork inside settleWork), so only confirmed markers reach the capsule. A terminal event (approval/question/completed) that lands during closeSession now suppresses the offer instead of being recorded as working.
  • A stopped provider makes a running turn count as cut off — turnWasCutOff gains a teardown-only providerStopped flag, and the resumable predicate gains a waitingOnUser clause that refuses work projecting as attention. The flag is only ever set from confirmStoppedMarker; launch-side derivation leaves it off.
  • Eviction is serialized per session — evictOwnedStructuredAgentSessions now routes each close through the host's per-session serialize queue, so a settlement cannot race an in-flight mutation.
  • Teardown phase order extracted — capture-resume-markers runs first, record-resume-markers moves to after evict-owned-sessions, and the wiring consolidates into flushStructuredAgentSessionHost.
  • Admission barrier bounded — runSettledAgentSessionMutation wraps the pre-beforeRun event flush in a 2s timeout and throws the new AgentSessionPreDispatchError when evidence is unavailable; RestartContinuationSupersededError now extends it, and the continuation rethrows any pre-dispatch failure as a refusal.
  • A proven refusal survives failed bookkeeping — the settlement catch returns a refusal outcome rather than rethrowing when its own write fails.
  • Capsule sweeps expired temp debris on publish.
  • Coverage — new structured-agent-session-operation-settlement.test.ts, the does not offer work superseded by a provider %s during close teardown cases, approval/question parametrizations in the ownership and resume-set suites, and a capsule debris-reclaim test. I ran the 10 changed suites (181 tests) green at this head; no failing check runs on a6bd3a5549.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 found.

Reviewed changes

Since the prior pullfrog review (a6bd3a5549), the branch advanced one commit, b12ea7a104 ("Validate restart continuation at provider dispatch"), touching 10 files (+181/-30).

  • Admission barrier moved to provider dispatch — the pre-beforeRun flush and its 2s timeout left runSettledAgentSessionMutation and now run inside dispatchSafely, immediately before adapter.dispatch; only the restart-continuation send sets beforeRun, so no ordinary mutation path is affected.
  • Post-flush pending-event guard — the host exposes hasPendingStreamedEvents (failed || queuedOperations > 0) through the mutation and turn contexts, and dispatchSafely refuses with agent_session_admission_evidence_unavailable when the barrier resolves while newer provider events are still queued, closing the window between "drained" and "dispatch".
  • Pre-dispatch refusals are journaled — performSend catches AgentSessionPreDispatchError, records the submission rejected, publishes, and rethrows; the operation ledger stays unknown and a replay reconstructs the rejected row without re-dispatching.
  • Continuation no longer supersedes itself — derive filters the continuation's own submission key out of the snapshot, so its pending user row, now written before the recheck, is not read as newer user work.
  • Coverage — ownership cases for provider evidence accepted during appendSubmission and for events accepted after the barrier was captured, plus the stalled-with-refusal-write settlement case; the changed suites run green (41 tests) at this head.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 found.

Reviewed changes

Since the prior pullfrog review (b12ea7a104), the branch advanced one commit, 358673de68 ("fix(native-chat): finish restart refusal and attribution delivery"), touching 9 files (+130/−13). All prior Pullfrog threads are resolved, and the four changed suites run green (67 tests) at this head.

  • Continuation attribution is published — StructuredAgentSessionRestartResumeSurfaces gains a publish(sessionId, journal) member (host binds this.subscribers.publish; the wiring returns Omit<…, 'publish'>), and the resume host's note callback calls it after appending the status item. The "Orca sent this" note now reaches subscribed renderers without waiting on a subsequent provider event.
  • Rejected sends no longer render as sent — projectStructuredAgentSessionMessages filters out journal items whose id matches a dispatchState === 'rejected' submission, and derives journalled from the filtered set so a retained outbox draft still renders. A refused continuation, which has no outbox entry, stops appearing as a sent user message.
  • Pre-dispatch refusals skip the redundant uncertainty write — the settlement catch rethrows an AgentSessionPreDispatchError immediately when markUnknownBeforeRun is set, since the pre-run unknown row is already durable and performSend has journaled the submission rejected. Only the continuation sets beforeRun, so no ordinary send path changes; the ledger stays unknown and replay finds the rejected row without re-dispatching.
  • Capsule-take failure no longer logs the error — catch {} replaces catch (error) on the recovery-capsule take warning, keeping capsule payloads and filesystem paths out of logs.
  • Coverage — settlement pins the stalled-write pre-dispatch refusal (exactly one uncertainty write, no dispatch), claim pins the log-privacy behavior, ownership pins attribution publication over a subscriber plus the corrected settlementFails expectation, and the renderer projection covers the hidden rejected send and the retained refused draft.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 found.

Reviewed changes

Since the prior pullfrog review (358673de68), the branch advanced one commit, 40a9b89a73 ("fix(native-chat): keep recovery teardown errors out of logs"), touching 4 files (+37/−9). The three changed suites run green (44 tests).

  • Teardown recovery failures no longer log the error object — structured-agent-session-host-teardown.ts drops error from the capture-resume-markers catch and from the record-resume-markers .catch, so both warnings are a single fixed string. This extends the same privacy treatment 358673de68 gave the capsule take to the two teardown phases, keeping capsule payloads and filesystem paths off the console.
  • Coverage — structured-agent-session-restart-teardown.test.ts adds a parametrized captureMarkers/recordMarkers failure test asserting the exact one-argument warning, that the private failure message does not appear, and that teardown still completes (lease released, session ownership unknown). The ownership capsule-failure test now pins the one-arg warning and a root-path-absent check, and the handoff-drain test adds expect(vi.getTimerCount()).toBe(0).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Check continuation authority after the SDK input queue wait and arm replay correlation only after authorization. Preserve typed pre-dispatch refusal, ordinary send behavior, and cleanup when the provider exits or capacity fills during authorization.

@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 found.

Reviewed changes

Since the prior pullfrog review (40a9b89a73), the branch advanced one commit, b2f72902e8 ("fix(native-chat): validate restart continuation at provider dispatch"), touching 13 files (+141/−29).

  • Pre-dispatch validation moved inside the adapter dispatch — StructuredAgentSessionAdapter.dispatch gains an optional beforeDispatch, and dispatchClaudeTurn / CodexStructuredSessionAdapter.dispatch now await it after provider preparation (Claude after content translation; Codex after captureTurnProcesses) and before registering a Claude dispatch waiter or writing turn/start. The router forwards the whole input, so the contract holds for both providers.
  • The admission barrier is now that callback — dispatchSafely builds an async beforeDispatch that flushes streamed events, refuses with agent_session_admission_evidence_unavailable when events are still queued, then runs the plan's continuation authority recheck. Because the refusal now surfaces from inside adapter.dispatch, dispatchSafely rethrows AgentSessionPreDispatchError instead of converting it to unknown; performSend still journals the submission rejected.
  • Attribution-note failure reporting simplified — the restart-resume surface drops its reportError wiring and always logs one fixed string, keeping session ids and error payloads off the console; the note stays best-effort and cannot fail the continuation.
  • Coverage — new Claude and Codex dispatch-boundary tests pin the ordering (refusal sends nothing / writes no turn/start), the host test harness adapter invokes input.beforeDispatch, and the ownership suite pins the fixed attribution warning. The seven touched suites run green at this head (86 tests).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 found.

Reviewed changes

Since the prior pullfrog review (b2f72902e8), the branch advanced one commit, 3d4bd63b55 ("Revalidate restart continuation when Claude dequeues input"), touching 6 files (+243/−86).

  • The continuation authority recheck now runs when the SDK input pump dequeues the frame, not when the send is queued. dispatchClaudeTurn takes an optional beforeDispatch and forwards it through ClaudeStreamJsonConnection.send into createClaudeUserMessageQueue, whose pump awaits it immediately before yielding the frame; ClaudeStructuredSessionAdapter.dispatch passes input.beforeDispatch. A continuation waiting behind a running turn is therefore re-judged against the journal at the moment it would be written, closing the window the prior head left open.
  • Replay correlation is armed only after authorization. The new arm() helper increments dispatchSequence and registers the waiter; with a beforeDispatch present, pending.replay starts undefined and authorize arms it only after the callback resolves and the queue-full check passes. A refused continuation registers no waiter at all.
  • Capacity is re-checked inside authorization, throwing claudeUnwrittenUserMessageError(DISPATCH_REJECTED_QUEUE_FULL) when the queue filled during the wait; the dispatch catch maps that to rejected (the reason still classifies as internal through the provider_write_failed: prefix).
  • Typed pre-dispatch refusals survive the queue hop. When authorization throws before arming, dispatchClaudeTurn rethrows an AgentSessionPreDispatchError unchanged rather than folding it into unknown, so performSend still journals the submission rejected.
  • fail() no longer rejects a frame that is still awaiting authorization. It rejects the in-flight frame only when handedOff (already yielded); an authorizing frame is rejected as provably unwritten by the pump's post-callback failure check, so provider exit during the wait settles rejected rather than unknown.
  • Extraction and coverage. The waiter helpers move to claude-structured-dispatch-waiters.ts; the new claude-structured-dispatch-queue-boundary.test.ts drives the real queue to pin that the recheck does not run at send time and a superseded continuation is refused after the pump wait with no leaked waiter, and that provider exit and capacity-fill during authorization settle rejected without leaked correlation. The fake connection now invokes beforeDispatch, keeping the existing boundary test valid.

I traced the queue/dispatch interleavings independently and found no path where a beforeDispatch frame's promise hangs, a waiter is orphaned, or a write-unknown frame is reported as accepted/rejected.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

# Conflicts:
#	src/main/claude/claude-stream-json-connection.ts
#	src/main/claude/claude-structured-session-test-support.ts
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status for head 6c9157df1aaa30b577cb140565ac9b0c87a36e68:

READY/CLEAN for the PR patch. A fresh same-model review and the final readiness pass found no proven in-scope functional, architectural, performance, security, lifecycle, cross-platform, compatibility, or scope issues. The branch was rebased by merging current main (8b2502fc925c) to clear conflicts; the resulting diff remains limited to the restart-recovery change.

Validation passed: 160 focused recovery/ownership/continuation/capsule/teardown tests, pnpm tc:node, RPC catalog and localization checks, cross-version wire compatibility, static analysis, desktop packaging, Windows packaging, and the mobile RPC recording/verification gates. The existing hidden-CDP Electron screenshots and interaction evidence remain valid because the merge only integrated non-UI upstream changes; no recovery UI files changed in that update.

The current PR check run has one unrelated failure in terminal-pane-hook-order-parity.test.ts: current main already has 211 flattened hooks while its unchanged ratchet still expects 209. The PR has no terminal-pane diff, and the failure reproduces on the merged main baseline; the aggregate verify failure is derivative. I left that unrelated baseline test untouched to keep this PR focused. The worktree is clean and the pushed branch matches the reviewed head.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Follow-up validation for the spacing polish at head 2ce87116ee:

  • Added a padded inner wrapper to the “What Orca sends” popover and normalized the heading, explanation, and quoted message to a consistent compact vertical rhythm.
  • Focused modal tests: 13 passed.
  • Changed-code quality gate: passed with 0 findings.
  • Hidden Electron CDP fixture: the popover text remains exact, keyboard focus stays inside the dialog, and reconnect/continue RPC behavior is unchanged.

orca-spacing-popover.png

@brennanb2025
brennanb2025 merged commit 434365d into main Sep 17, 2026
28 of 30 checks passed
ivanvykopal pushed a commit to ivanvykopal/orca that referenced this pull request Sep 20, 2026
…stablyai#21096)

* feat(native-chat): resume structured chats that were working at restart

Teardown records a marker for every session this host was genuinely running a
turn for, derived from the LIVE runtime rather than a persisted status row, so
a stale `running` row left by an older crash can never trigger a resume. On the
next launch a modal lists exactly which chats would resume and resumes them via
native continuation (Claude resume/resumeSessionAt, Codex thread id) — never by
re-sending the prompt, which is what makes an agent redo finished work.

A session resumes only when all of these hold: a teardown marker exists and has
not expired, the record's lease is released and reconciled, a provider resume
cursor exists and still matches the marker, the journal's own turn record names
the same turn, and the marker has not already been spent. Markers are consumed
before the resume is submitted, so a crash mid-resume cannot double-fire, and an
admission gate refuses a second concurrent resume for one session. Resumes are
staggered three at a time rather than spawning every provider at once.

The modal's "Don't ask again" checkbox writes the nativeChatResumeWorkOnRestart
setting, which Settings can turn back off; automatic mode runs the identical
predicate and staggering and reports what it did. Declining consumes the markers
so the prompt cannot return every launch — nothing is lost, because opening a
chat still re-acquires it at the same cursor.

* fix(native-chat): compare handle ROOT and turn state when offering a resume

Four defects QA found in the restart-resume offer, fixed together because the
first two interact: shipping the root fix without the state fix would convert a
silent no-op into actively offering finished chats.

1. Claude was never offered (0/4). The marker recorded agentSessionProviderHandleKey,
   which embeds Claude's leaf uuid — a branch cursor. The adapter's own close path
   appends a `resumed` link with an advanced leaf during the SAME teardown, so the
   marker went stale seconds after it was written and the drift guard refused every
   Claude session forever. Record and compare agentSessionProviderHandleRoot instead:
   the root is the part a resume must preserve, and changing it is a fork, which is
   exactly what this guard is for. Codex is unaffected (its thread id is the whole
   key) but uses the root too, so the rule is uniform.

2. The predicate compared turn IDENTITY but discarded turn STATE, so a `completed`
   turn satisfied it as readily as an interrupted one. Eviction rewrites `running`
   to `interrupted` and never to `completed`, so the state is what separates work
   that was cut off from work that finished. Require `interrupted` or `unverifiable`.

3. A chat blocked on a pending approval or question was marked as working, because
   the teardown reader accepted any `running` turn while the product's own projection
   calls that state `attention`. Teardown now defers to that projection: an agent
   waiting on the USER is not interrupted work.

4. "Resume all" could silently no-op. The modal fetched candidates at mount; by click
   time the chat's own pane may have bound and taken the hold, moving the lease to
   `live` so the predicate dropped it and the call returned no results, leaving the
   dialog open behind a dead button. Re-derive at click time and settle an
   already-live session as resumed — it is running, which is what the user asked for.

Test fakes now model the Claude close path that advances the leaf, which is why no
unit test could previously exhibit defect 1. Ablation covers all eleven guards.

* fix(native-chat): gate the already-live settlement on the full resume predicate

Two follow-ups from re-QA, both cases of a rule stated by intent rather than by
discriminator.

1. The already-live path bypassed the predicate. "Resume all" sends no session
   ids, so the fallback's target set was every marker, and it was gated only on
   the session having a live provider child. A chat the predicate had refused --
   a completed turn, say -- whose pane happened to own the lease was therefore
   settled as `already_live` and had its marker spent, inflating the "Resumed N"
   count with chats that were never eligible. No provider spawned and no tokens
   were spent, but a marker the predicate rejected must never be consumed.

   The resumable set now takes an explicit `leaseState`. The already-live path
   derives a second set with ONLY the released-lease clause relaxed, and settles
   a session just when it is in that set. Every other clause still applies.

2. The `attention` rule was one-sided. Teardown refuses to mint a marker for a
   chat blocked on the user, but the set predicate had no equivalent, so a marker
   arriving by any other route was offered once eviction rewrote its turn to
   `interrupted` -- the same asymmetry the completed-turn case had.

   Gated on projectStructuredAgentSessionStatus === 'attention'. That projection
   tests for a pending approval or question BEFORE it looks at turn state, so it
   still reports `attention` after the turn is settled, which makes it the durable
   signal and keeps one source of truth with teardown.

Ablation now covers thirteen guards, including one for each of the above.

* fix(native-chat): capture awaits-user on the marker instead of re-deriving it

The awaits-user clause could never fire. It asked the live projection for
`attention`, which needs a prompt whose resolution is still `pending` -- but
teardown CANCELS that prompt a few phases after it writes the marker. By the next
launch the evidence is gone, for precisely the sessions the clause was written
for. QA measured the injection still being offered and then resumed.

This is the same shape as the leaf-drift bug: state read after teardown is not the
state that justified the marker. The discriminator, now applied across the whole
predicate:

  - a fact teardown itself destroys or mutates must be CAPTURED on the marker
    while it is still true;
  - a fact that evolves on its own must be RE-DERIVED at read time, never
    snapshotted.

So `awaitsUser` is now recorded at teardown and the predicate reads the recorded
value. Teardown still declines to mint a marker for such a session, so the
recorded flag is the second line rather than the only one.

Audit of every other clause against the same test:

  - turn id (captured) -- teardown rewrites turn STATE but never the id. Correct.
  - provider handle root (captured) -- the close path appends a resumed link, and
    appendAgentSessionProviderHandleLink refuses one that changes the root, so the
    root is invariant under exactly the mutation that broke the key. Correct.
  - turn state (re-derived) -- DELIBERATE exception, stated here rather than left
    implicit: we are not reading the state that justified the marker, we are
    reading teardown's receipt that it settled the turn. A turn still `running`
    means eviction never finished, and we refuse. Correct, and intentionally so.
  - lease reconciled / released / handoff stage (re-derived) -- these answer a
    different, launch-time question: may this host take the lease NOW. The
    teardown-time value would be meaningless, and `unreconciled` is cleared by
    this launch's own reconciliation. Correct.
  - adapter support, marker TTL, marker consumption (re-derived) -- all evolve
    independently of teardown. Correct.

Only awaitsUser was on the wrong side.

* fix(native-chat): drop the unreachable awaits-user marker flag

The captured flag was dead code. `awaitsUser` could only be true when the
projected status was `attention`, and `attention` hits the `continue` above the
push -- so every marker teardown can ever write carries `false` (QA measured
22 of 22 across two real teardowns). The predicate clause reading it was
unreachable by any production path.

A flag that is structurally always false is worse than no flag: it reads as a
safeguard, so the next person to touch this trusts it. The asymmetry it was
added to close was only ever reachable by fault injection, because teardown is
the sole writer of markers and already refuses attention sessions.

Removing it also drops an upgrade discontinuity: as a required field it made a
marker written by the previous build fail validation and be silently discarded,
costing a resume offer on precisely the upgrade where the user was mid-turn.
Markers predating the providerHandleRoot rename still will not parse, but those
carry a leaf-sensitive key the predicate would refuse anyway, so nothing usable
is lost.

In its place the teardown gate now states that `status !== 'working'` is the
SINGLE gate for awaiting-user sessions, why a predicate-side mirror would be
unreachable, and why it could not even re-derive the fact -- so the reasoning is
inherited rather than rediscovered.

Ablation is back to twelve guards; every other clause is unchanged.

* fix(native-chat): say reconnect, not resume, and show each offer's age

Two changes, both independent of the parked continuation decision.

1. The copy claimed something QA disproved. "Resuming continues each agent where
   it left off" is false: reconnection restores the session at the point it
   stopped, with full context and without re-sending the prompt, but the
   interrupted reply does not continue on its own. The toast's "Resumed N chats"
   implied work had restarted.

   Audited every user-facing string against the rule that none may claim work
   continues or that a reply resumes -- which caught more than the three strings
   the fix started from. The title, the row button, "Resume all", "Resuming...",
   the not-now hint ("picks it up where it left off"), the checkbox and its hint
   ("resume on their own"), the list's aria-label and the Settings row all made
   the same claim. The user-facing verb is now reconnect throughout; the body and
   update variant state outright that the interrupted reply will not continue.
   en.json synced, runtime boot catalog regenerated.

   If we later decide to send a continuation instruction, this is one commit to
   change back. Shipping text we know to be false was the worse option.

2. Rows now show each offer's age. The TTL is 24 hours and a stale offer looked
   identical to a fresh one. The marker already carried `recordedAt`, so this is
   a render change plus one field on the renderer's candidate type, formatted
   with the existing formatUiRelativeTime helper rather than a new one.

   The clock is stamped once when the list arrives rather than read during render:
   ages then stay stable across re-renders, and the render stays pure, which the
   react(purity) rule requires.

Guards, predicate and RPC are untouched; ablation still covers twelve.

* feat(native-chat): show the workspace name on each reconnect row

A row read `codex · folder:8f3a1c22-… · 8 hours ago`. Recognising which chats
would reconnect is the entire point of the list, and at twenty rows a UUID
identifies nothing.

No RPC or host change was needed: the renderer can already resolve this id.
Resolved the way automation dispatch resolves the same id space
(resolveAutomationDispatchWorkspace) -- a folder workspace by its full
`folder:<uuid>` key via getKnownWorktreeById, a git worktree by its bare
`repoId::path` id via allWorktrees. Both return a Worktree, whose displayName is
a required field, and DetectedWorktree extends Worktree so either shape answers.

Falls back to the id when nothing resolves, which is what the row showed before
and also covers the window before the worktree store has hydrated.

The lookup lives in a per-row subcomponent because a hook cannot run inside
`map`, and its selector returns a primitive string so repeated selector runs
cannot churn referential equality.

* feat(native-chat): group the reconnect modal by worktree and add opt-in continuation

Grouping. Rows are now grouped under a worktree heading with the repo glyph and
an agent count, using the sidebar's own collapse mechanics. Only presentational
pieces are reused -- RepoIconGlyph, CompactAgentExpansion, AgentIcon and
formatShortTimeAgo. The sidebar's agent row cannot be: worktree-card-compact-agent-row
imports DashboardAgentRow, the dashboard's own type, so both surfaces render one
live-agent model requiring a pane, tab and status entry. Every chat offered here
is by definition stopped, so supplying that would mean inventing live state.

Two things I had assumed were reusable and were not:

  - DashboardHostBadge returns null unless hostKind is ssh or remote. Structured
    chat is local-only, so it would always render nothing. The host line is
    omitted rather than faked; the badge is the right element to add if and when
    structured chat gains remote support.
  - No state dot. Every AgentDotState misleads here: idle and unverifiable both
    presuppose a live pane, interrupted renders red like an error, done green,
    working a spinner. A missing dot beats one saying these agents are running.

One worktree renders flat with no heading -- a name, count and chevron around a
single group says nothing the dialog has not already said.

The age column now uses formatShortTimeAgo for sidebar consistency. It takes
(timestamp, now) and subtracts internally rather than taking a delta, so the call
is (recordedAt, listedAt); passing the old delta would have rendered plausible
nonsense. The clock is still stamped once into state, so ages stay stable and the
render stays pure.

Continuation. A secondary "Reconnect and continue" action sends one message, from
a single shared constant, identical for both providers. Reconnect is unchanged and
still sends nothing. An info popover quotes the literal message read from that
same constant, so what is shown cannot drift from what is sent.

Ablation now covers fourteen guards. Two are new: continuation only follows a
reconnect that actually happened, and -- inversely -- a send injected into the
reconnect path must turn the test red, since "don't ask again" rests on reconnect
never sending.

* feat(native-chat): say terminal sessions kept running, and clear the quality gate

The modal lists stopped chats with no way to tell that CLI agents are fine, and
the true state of the world is counterintuitive: the terminal sessions survived
the restart and the chats did not. One line now says so, next to the heading
where it frames the list rather than as a footnote at the bottom.

Wording follows the app's own vocabulary rather than inventing a term: the
catalog settles on "terminal sessions" (terminalSessionCount, "Terminal sessions
are grouped by workspace", "No terminal sessions yet"), and UpdateCard already
reassures with "Your terminal sessions won't be interrupted during the update" in
the same text-xs text-muted-foreground treatment. "kept running" rather than
"were restored" -- nothing reconnected them, they never stopped, and the line
says nothing about why.

Also clears check:code-quality:changed, which I had not been running -- oxlint
alone covers neither the design-system nor the casting audit, so 18 findings had
accumulated across the branch.

  - design system (4): Button spacing hand-rolled as gap-1/px-2 is just size="xs";
    PopoverContent and DialogTitle own their typography and spacing, so the
    text-xs moved to the popover's own children and the title's icon gap moved to
    a plain wrapper.
  - casting (14): production code loses its assertions outright via Reflect.get,
    the idiom already used in managed-hook-detection-commands and
    worktree-name-retirement. The marker validator reads each field through
    Reflect.get and now checks recordedAt is a number rather than asserting it;
    the store-file parse uses the existing `file` shape instead of a second
    assertion; the runner narrows the admission error's owner with typeof.
    Test fixtures keep their assertions behind the line-specific SAFETY:
    rationale the repo mandates for exactly this case.

One trap worth recording: the audit reports an assertion at the line its
EXPRESSION OPENS, not where `as` appears, so a disable-next-line above the
closing brace of a multi-line literal is inert and silently changes nothing.

Guards unchanged; ablation re-proved 14/14 at this head.

* fix(native-chat): give the reconnect row's provider icon an accessible name

Every row rendered the provider as a bare AgentIcon, whose svg carries no
aria-label, title or alt. With a Claude chat and a Codex chat in one worktree the
two rows were identical to any non-visual consumer, and the dialog offered
several identically-named "Reconnect" buttons with nothing to tell them apart.

A regression from 233e37b, where the row read `${agent} · ${workspace} · …` as
text. Moving the workspace name into the group heading was right; dropping the
provider to an unlabelled glyph is what lost the information.

AgentIcon takes no label prop, so the icon is wrapped the way
NativeChatSupportedAgents already names it: a span with role="img" and an
aria-label from formatAgentTypeLabel, the same labeller the sidebar and dashboard
rows use.

The per-row button also names its agent now ("Reconnect Claude chat"). The
identical buttons were half the reported harm, and an accessible name that opens
with the visible word keeps WCAG 2.5.3 satisfied. Say so if you would rather ship
only the icon label -- it is one attribute and one catalog key to drop.

Age code untouched, as asked: formatShortTimeAgo still takes (timestamp, now) and
is still called with (recordedAt, listedAt).

* fix(native-chat): scope resume markers to one launch and report the real dispatch

Three defects in the restart-resume path, all of which could resume a session
that was not genuinely working or claim one was continued when it was not.

Launch scoping. A durable marker with a 24h TTL is a write-ahead latch: a
teardown write that failed or timed out, or a store restored from its backup,
left a previous generation's marker actionable, and automatic reconnect would
have acted on it silently. Markers now carry the id of the launch that wrote
them, and only the launch immediately after may claim them. The launch id lives
in its own file with no backup mechanism, so it cannot roll back in step with
the markers it is proving adjacency for. Startup claims the previous launch's
markers into launch-scoped memory and deletes every durable copy in the same
step, so the durable fact dies at claim time rather than at use time. Both
halves fail closed: an unprovable predecessor and a clear that throws each
claim nothing.

Dispatch states. The send layer answers ok as soon as Orca owns the message;
the provider's own answer lives in the submission. Continuation read only the
envelope, so a rejected turn/start was reported as continued and stamped the
journal saying the agent had been asked to carry on. All four states are now
preserved, and only an accepted dispatch appends the attribution note.

Claude pre-echo sends. Claude cannot write a running turn until the SDK echoes
the user message back, which is seconds on a real journal, so a turn-id-only
marker dropped exactly the sessions that were working hardest. A send that has
not become a turn now carries its own identity, and the launch-side predicate
asks the journal about that submission's dispatch state instead.

* fix(native-chat): follow an accepted send to its turn, and settle before judging

Two defects found in QA, both reproduced twice.

Follow the submission forward. The launch-side predicate accepted a
submission-shaped marker only while its dispatch was pending or unknown, but the
window in which work is submission-shaped is precisely the window in which the
dispatch is about to be accepted: the send settles during teardown and the turn
it opened is then cut off as interrupted. Judgement was frozen at the moment the
marker was written, so the predicate refused the very sessions this was built
for and fired only when the send never reached the provider. An accepted
submission is now followed to the turn it opened -- matched through the user
item key a turn names and a submission is aliased by -- and that turn is judged
by the existing turn rule. Accepted alone still proves nothing: without the link,
or with a turn that completed, this refuses as before.

Settle before judging. A send resolves as soon as Orca owns the message, while
its dispatch is still pending; that is the ordinary successful path. Reading the
dispatch off the send result therefore reported every delivered continuation as
pending and never wrote the attribution note. The outcome is now decided on the
settled submission, through the host's existing settlement waiter, with the send
result as fallback when nothing settles in time.

The failed-note path no longer swallows its error. It stays best effort -- a
journal that refuses the note must not turn a delivered continuation into a
failure -- but the failure is reported through the host's error sink instead of
being discarded, so it cannot regress unseen again.

The surface's send is typed against the wire result rather than a hand-written
subset, which is what let a test assert a shape the host never returns. Binding
the surface to the host moves into its own file: the host was one line under the
line cap, and the bindings carry decisions that belong beside their consumer.

* feat(native-chat): show the reconnect offer the way the worktree sidebar does

The offer is a list of workspaces, so it should read like the one users already
know. Rows are now three tiers -- repo or project, then workspace, then the agent
sessions inside it -- and each agent carries a checkbox rather than its own
button, checked by default, with the footer acting on whatever is ticked.

Reused rather than rebuilt. The host chip is the sidebar's own: its markup lived
inline in the card's meta row, so it moves to a shared component both surfaces
render, and the label comes from getHostContextLabel, which is where "Local Mac"
has always come from. The repo glyph is RepoIconGlyph; a group with no repo uses
the FolderTree the sidebar's own project-group metadata uses. The agent row
reuses AgentIcon, the agent-type label helpers, formatShortTimeAgo and the same
model treatment.

Two things could NOT be reused, and both are deliberate. The sidebar's
CompactAgentRow needs a live pane, tab and status entry, and every chat here is
stopped by definition. And the sidebar has no git-worktree-vs-folder glyph
resolver at all -- both kinds render the same card, and the difference people
read is its status lane choosing GitBranch when a workspace has branch identity;
that single precedent is what the workspace glyph follows.

The model, the execution host and the workspace kind now travel with each
offered chat. All three are read off the durable record the predicate already
holds -- the model through the same normalizer the status feed uses -- so the
glyph is never inferred from a name and no new data source appears. They are
optional on the wire, so an older host still renders a row.

Selection changes which ELIGIBLE chats are acted on, never what is eligible. Ids
are seeded from the host's own answer and intersected back against it before any
call, and the host re-derives the predicate regardless of what it is sent.
Continuing still requires an explicit click, and the automatic path still calls
the reconnect method, which contains no send.

The badge's treatment becomes a variant instead of a pile of overrides, which is
what the design-system gate asks for once the markup is somewhere it can see it.

* fix(native-chat): title a folder workspace group with its project name

A folder workspace's synthetic worktree borrows the `repoId` slot to name the
project group it belongs to, so that field is NEVER null. The reconnect offer
read a non-null `repoId` as proof of a git repo, looked it up in the repos list,
found nothing, and rendered the raw `folder-workspace:<uuid>` string as the group
header. The project glyph written for the no-repo case was unreachable for the
one workspace kind it was meant for, and the string fallback behind it was dead
for the same reason.

The project group name was available all along and the sidebar already titles
these with it, which is what this list is meant to mirror.

Recognising the id now lives beside the code that mints it, so the two cannot
drift: there was no such helper, only forward constructions of the same prefix in
five places. The header choice itself moved into a pure resolver, so the branch
that was wrong is now the branch under test.

The dead fallback string is gone, along with its catalog entries.

* fix(native-chat): offer an accepted send the provider never opened a turn for

QA: a chat that was genuinely working was silently dropped from the offer. The
discriminator was how far the send had progressed -- it was the last chat
prompted before quitting, reachable by quitting a second or two after sending.

Mechanism, reproduced against the predicate. The marker was written while the
send was still pending, so it is submission-shaped. During teardown the dispatch
then settled to `accepted`, which took it out of the pending/unknown branch and
into the follow-forward branch. But the provider died before writing a turn row
for that send, so there was no turn to follow forward TO, and the branch demanded
a proved link before it would answer. Both the no-turn-at-all case and the
newest-turn-belongs-to-an-earlier-exchange case therefore refused.

An accepted send that never became a turn cannot be finished work, because
finishing writes a turn row. The marked send is also the newest work in the
session, so any turn it opened would be the newest turn.

That makes the link unnecessary to prove for a safe answer. When the newest turn
is interrupted or unverifiable the two readings agree: if the row really is this
send's under a key we failed to match, it was cut off; if it belongs to an
earlier exchange, this send opened no turn at all. Either way the work was
interrupted. A journal with no turn row at all is the same case with nothing to
disagree about.

The readings only diverge on a `completed` row, where an unmatched one might be
this very send's finished turn under a key we did not recognise. That stays
refused. Ambiguity resolves to no, because resuming finished work is the one
outcome never worth risking.

* fix: write the grouping separators as escapes so the files stay text

Five separators in the reconnect-offer redesign were written as raw NUL bytes
instead of the `\0` escape. The runtime strings were correct and the app behaved,
but git classifies a file containing a NUL as binary -- so the two central files
of that redesign rendered as "Binary file not shown" in review, and `rg` skipped
them silently, returning no matches rather than an error.

The escape produces the identical string, so the NUL separator is kept: the
previous separator was a space, and a workspace id containing one would corrupt
the join/split pair this grouping depends on.

Nothing could have caught this. Typecheck, lint, the quality gate, the
localization verifiers and the full suite all passed throughout, because none of
them look at file encoding. So this adds a check that does, wired into the
pre-commit hook where it costs nothing and catches the next one at the moment it
is written.

Two files already on main carry a raw NUL for the same reason -- one a template
separator, one a deliberately tricky test alphabet whose neighbours are all
written as escapes. They are grandfathered rather than fixed here, since they
belong to their own change, and the gate fails if the list ever grows or goes
stale.

* fix: parse markers into a domain type, and declare the four restart methods

Two CI failures, both ours.

Static analysis. `Reflect.get` was adopted to clear the casting audit, and the
anti-slop rule forbids it -- the two gates disagree, and the rule text says what
both want: parse dynamic input into a named type once, then read typed fields off
it. Markers re-enter from a file this process may not have written and decide
whether an agent is handed a provider child, so they now go through a single zod
parse. Unknown keys still pass, and a malformed marker is still dropped rather
than thrown, so a bad entry cannot make a user's sessions unreadable. The launch
stamp is parsed the same way, the resume-admission refusal becomes a named error
carrying a typed `owner` instead of a bag assigned onto `new Error`, and the test
harness gets a named journal type instead of reaching into `unknown`.

Cross-version wire. The four restart methods are added to the manifest rather
than the count being bumped, so the suite now exercises them in both skews. They
are bare additions, not capability-negotiated: an unknown RPC method answers
`method_not_found`, which is explicit and visible during negotiation, unlike a
stream opcode that is dropped in silence. The whole `agentSession.*` surface
already sits behind its runtime capability, so an old client is told it does not
exist and never reaches a host method.

The stub's spies stay a flat map because callers iterate it asserting each entry
is a spy that did not run; a composer reassembles the member the host really
exposes. The manifest and its params builders move to their own module, which is
what keeps the suite under its line cap as the surface grows.

* Prevent duplicate restart continuation and release reconnect holds

* fix: recheck interrupted work when admitting restart continuation

* fix(native-chat): invalidate restart offers after newer user work

* Consume restart recovery offers from an isolated advisory capsule

* Refuse completed restart work and report recovery outcomes

* fix(native-chat): honor queued completion and uncertain restart delivery

* fix(native-chat): preserve restart refusal and teardown evidence

* fix(native-chat): rederive recovery evidence before continuing

* Validate restart continuation at provider dispatch

* fix(native-chat): finish restart refusal and attribution delivery

* fix(native-chat): keep recovery teardown errors out of logs

* fix(native-chat): validate restart continuation at provider dispatch

* Revalidate restart continuation when Claude dequeues input

Check continuation authority after the SDK input queue wait and arm replay correlation only after authorization. Preserve typed pre-dispatch refusal, ordinary send behavior, and cleanup when the provider exits or capacity fills during authorization.

* Deduplicate settlement test import

* Keep merge update scoped to restart recovery

* Polish continuation popover spacing
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