Skip to content

fix(native-chat): settle a structured send on admission, not on the provider echo - #19863

Merged
brennanb2025 merged 12 commits into
mainfrom
brennanb2025/fix-dispatch-admission-settlement
Sep 10, 2026
Merged

brennanb2025 merged 12 commits into
mainfrom
brennanb2025/fix-dispatch-admission-settlement

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 24 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2800 $\color{#cf222e}{\Huge{\mathbf{−}}}$​853 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1947
Prod 36 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1188 $\color{#cf222e}{\Huge{\mathbf{−}}}$​232 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​956

ELI5

Structured native chat used to wait ten seconds for Claude to start a queued message. If Claude was still finishing the previous turn, Orca showed “Message delivery is unconfirmed” and offered Retry even though the message was already queued. Retrying could send the same prompt to the model again.

Orca now records its own admission immediately, waits for Claude’s later item event to attach provider identity, and shows delivery doubt only when evidence says the handoff failed.

What Changed

  • Persist the submission before dispatch and keep an admitted send in a durable neutral pending state until Claude’s item event settles it accepted.
  • Capability-gate the new pending response. Updated clients receive it immediately; legacy clients get bounded best-effort settlement observation for up to 30 seconds, with 64 observers per session and 1,024 per host, before the host returns the durable pending result.
  • Distinguish a provably unwritten frame from an ambiguous in-flight failure. Retry is allowed only for the provably unwritten case.
  • Settle and clean observers on item replay, abort, session close, host close, and provider exit; reject stale item revisions behind tombstones.
  • Bound active dispatch identity waiters, keep compaction outside user-submission correlation, and prevent stale renderer failures from resurrecting the warning or redispatching a host-owned send.

Why

Claude’s stdin/SDK handoff provides neither a durable provider receipt nor an immediate turn ID. The provider item arrives when Claude starts the turn, not when Orca queues it, so its delay includes the unbounded duration of the turn ahead of it. A fixed provider-echo timeout was measuring queue duration rather than delivery failure.

In the original journal sample, 44 of 173 delivered sends exceeded ten seconds and the longest clean delay was 105 minutes. The false Retry was not cosmetic: one prompt produced five provider items after repeated retries.

The durable boundary Orca can own is its journal admission. Provider replay supplies identity later; ambiguous writes remain non-retryable so Orca cannot silently duplicate a prompt.

Linked Issue

No linked issue — reported directly.

Visual Proof

The original failure was user-reported, so no direct pre-fix screenshot is available. These branch screenshots show both important post-fix states from one real Claude session.

Queued beyond the former timeout: the second message is still visibly working at 11+ seconds, with no “Message delivery is unconfirmed” text and no Retry button.

Queued send remains neutral past the former timeout

Settled after the first turn finished: the UI rendered SECOND_PENDING_DONE; the journal recorded submission at 1789039425059 and acceptance at 1789039506160, an 81.101-second delay.

Queued send settles after the previous turn

Testing

  • Hidden macOS Electron validation with ORCA_BACKGROUND_LAUNCH=1 and Playwright CDP exercised a real Claude session: a second message stayed neutral past 11 seconds, then settled after 81.101 seconds and rendered SECOND_PENDING_DONE.
  • Exact post-merge head 942c3e3e338163b4fdccee64d5dcceb124659b37: 323 focused tests across 22 files passed; pnpm tc:node, changed-code quality, and git diff --check passed. A separate exact-production-head mobile direct/relay capability run passed 18 tests.
  • Earlier branch validation: authenticated real-Claude integration, full mobile suite (4,258 passed, 3 skipped), node/web/mobile typechecks, targeted performance audit, 118 reliability gates, hooks, and formatting.
  • Exact-head GitHub CI passed all eight test shards, typecheck, static analysis, cross-version wire compatibility, Linux and Windows packaging, repository guards, and pullfrog.
  • Exact-head SSH and native mobile-emulator runs were not repeated after the final test-only main merge.

AI Disclosure

Review

  • Merge conflicts were resolved before review, and the latest main was merged again before the final pass.
  • The opening readiness pass found focused correctness, lifecycle, renderer, and mixed-version issues; all were fixed.
  • The same-model, same-effort adversarial review/fix loop returned clean after the final merge, including the settlement observer, queue ordering, journal reducer/replay, revision guard, mobile/wire behavior, cleanup, and performance.
  • Final readiness passed at exact head 942c3e3e338163b4fdccee64d5dcceb124659b37 with no remaining in-scope findings.

Agent skill upstream boundary

  • Not applicable; this change does not modify agent skill installation or sharing.

Notes

  • Security/supply chain: no dependency, lockfile, binary, shell, download, signing, secret, or network-sink change.
  • Backward compatibility: this does not preserve the former bounded settled-reply contract. It replaces it with bounded best-effort observation for legacy clients. Current desktop and mobile advertise agent-session.pending-send-result.v1 and receive durable pending immediately. A legacy mobile client still has its own 15-second request budget and may fall back to transcript reconciliation before showing an unconfirmed warning.
  • Architecture: Orca owns durable admission in its journal and attaches canonical provider identity later because the SDK exposes no synchronous provider receipt. That differs from systems able to establish canonical acceptance during target insertion, while retaining stable identity, target-owned admission, bounded resources, and explicit ambiguity.
  • Mobile: the production mobile change is capability advertisement, not layout. Direct/relay negotiation and RPC behavior are covered by focused tests; native emulator UI was not rerun because the visible flow is unchanged.
  • Cross-platform/SSH/remote: no platform-specific path, shell, process-spawn, or UI behavior was added. Settlement remains journal/event driven and does not infer provider exit from loss of client contact. Structured RPC is capability-gated on client paths that do not support it.
  • Performance: capable clients do not block on provider echo. Legacy observation is event-driven and bounded by a 30-second TTL plus 64/session and 1,024/host caps. Provider-identity retention is capped; no polling, subprocess fanout, render loop, or broad scan was added.
  • Scope: the separate Codex timeout remains unchanged because that path lacks equivalent identity correlation and waiter lifecycle. Changing it safely is follow-up work, not a speculative extension of this Claude fix.

Checklist

  • This PR is focused on the reported structured-send settlement failure and its required compatibility/lifecycle support.
  • I explained what changed and why (including ELI5).
  • State-transition screenshots are attached; the unavailable original failure screenshot is disclosed above.
  • Self-reviewed for correctness, security, and performance.
  • Cross-platform, SSH/remote, mobile, and backward-compatibility impact considered.
  • Lint/code quality, typecheck, and focused tests pass; remaining environment-specific gaps are disclosed above.

…rovider echo

Sending a message in structured native chat raised "Message delivery is
unconfirmed." with a Retry button on a message that had in fact been
delivered. Measured across 14 days of local journals: 44 of 173 delivered
sends (25.4%) tripped it.

The dispatch path wrote the message to the provider, then waited a fixed
10s for the provider to echo the message's uuid back. That echo is emitted
when the provider STARTS the turn, so a message queued behind a running
turn cannot be echoed until that turn ends. Echo latency is bounded by the
previous turn's duration, which is unbounded -- one send took 105 minutes.
The 10s constant sat at the p75 of real echo latency, with the slowest
clean send at 9.76s, a margin of 0.24s. No constant can work: the wait was
measuring the wrong event.

The false banner was not cosmetic. It invited a Retry, and Retry bypassed
the operation ledger to redeliver. One message reached the model five times
through that path.

Dispatch now returns as soon as the transport write completes and writes no
dispatch row; the submission stays `pending`, a neutral state, and the
provider's echo settles it `accepted` through the late-settlement channel
whenever the turn ahead of it ends. Delivery doubt is reachable only from
process facts -- a refused write, a dead child, a dead host -- never from
elapsed time.

Retry re-delivers only where the recorded reason proves the message never
reached the provider. The list is deliberately fail-closed: refusing a
legitimate retry costs the user a re-type, while allowing an illegitimate
one sends the model a second copy of their message. A refused entry now
leaves the outbox with an explicit notice instead of parking at the head,
where it would have wedged every message queued behind it.

The send-response classification moves to a pure module beside the existing
outbox reconciler, so both writers of an entry's state now live together and
the decision is unit-testable rather than reachable only through the hook.

Scope and known gaps:
- Codex carries the same 10s stopwatch. It has no late-settlement channel,
  matches waiters by queue order rather than identity, and has no waiter
  lifecycle at all, so there was no safe subset to land here. A marker
  constant records the debt and deletes itself when that lands.
- A message refused re-delivery loses its standing delivery notice and
  leaves only a transient error line. A passive "waiting to be accepted"
  affordance is the follow-up.
- The restart reconciler that would decide a dead child or a dead host on
  evidence rather than refusing them is fully written and has never had a
  production caller. Wiring it is the next change, and it removes the
  re-type cost above.
@coderabbitai

coderabbitai Bot commented Sep 10, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7810e2ca-2b9d-4e62-95e4-b2470cb0cbae

📥 Commits

Reviewing files that changed from the base of the PR and between 7061019 and 2bee1d7.

📒 Files selected for processing (1)
  • src/main/native-chat/agent-session-wire/structured-agent-session-send-settlement.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/native-chat/agent-session-wire/structured-agent-session-send-settlement.ts

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


📝 Walkthrough

Walkthrough

The change replaces dispatch timeouts with immediate admitted results and later provider-replay settlement. It distinguishes proven write failures from unknown write outcomes and limits active Claude waiters. Journal state now supports pending dispatches and provider-echo reconciliation. Structured host retries redeliver only proven-undelivered messages. Runtime capability negotiation preserves settlement behavior for older clients. Renderer outbox handling now applies shared dispositions for accepted, pending, rejected, and unknown results. Tests cover dispatch, recovery, host sends, runtime compatibility, and renderer delivery behavior.

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 2bee1

The change improves delivery admission and retry handling, but unresolved edge cases may still show false delivery warnings or prevent failed messages from being retried safely. Resolve these before merging.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 80 functions across 62 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description includes the required sections and detailed testing, compatibility, scope, and risk information. However, the required linked issue is explicitly missing. Add a valid issue reference in the Linked Issue section, such as Fixes #123``. Replace the current “No linked issue” statement.
Linked Issues check ⚠️ Warning The description states that no linked issue exists, although the template requires one for every pull request. Link the pull request to the relevant issue using a supported reference, such as Fixes #123``.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: structured sends settle on admission instead of waiting for the provider echo.
Out of Scope Changes check ✅ Passed The changes support admission-based settlement, retry safety, lifecycle cleanup, compatibility, renderer behavior, and required tests. No clearly unrelated production behavior is evident, and Codex ti…
✨ 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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts (1)

94-104: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear the delivery error when the journal settles a send the transport doubted.

This reconcile effect drops an entry once the journal reports it accepted, but it never resets error. The sequence is now a normal path under admission and late settlement:

  1. The RPC rejects, so disposeStructuredAgentSessionSendFailure sets error to 'Message delivery is unconfirmed' and the entry becomes unconfirmed.
  2. The provider echo later settles the submission as accepted.
  3. This effect removes the entry, so NativeChatStructuredSession.tsx no longer renders the notice or the Retry button.
  4. error is still set, so the destructive line at NativeChatStructuredSession.tsx lines 307-311 keeps showing 'Message delivery is unconfirmed' for a message that was delivered.

The stale text persists until the next send or retry clears it. The new test use-structured-agent-session-outbox.test.tsx lines 234-260 covers this exact sequence and does not assert error.

🐛 Proposed fix to clear the stale error
   useEffect(() => {
     const next = reconcileStructuredAgentSessionOutbox(outboxRef.current, submissions)
     if (
       next.some((entry, index) => entry !== outboxRef.current[index]) ||
       next.length !== outboxRef.current.length
     ) {
+      const settled = outboxRef.current.filter(
+        (entry) => !next.some((candidate) => candidate.clientMessageId === entry.clientMessageId)
+      )
+      if (settled.length > 0) {
+        setError(null)
+      }
       outboxRef.current = next
       setOutbox(next)
       writeOutbox(sessionId, next)
     }
   }, [sessionId, submissions])
🧹 Nitpick comments (4)
src/main/claude/claude-structured-session-test-support.ts (1)

237-247: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider an options object for adapterFor.

adapterFor now takes nine positional parameters, and acquired must pass five consecutive undefined placeholders to reach the last one. A future parameter insertion silently shifts every call site. An options object removes the placeholder chain and makes each call self-describing.

src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx (2)

568-568: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten the assertion so it pins the new pending behavior.

With the new disposition, a pending answer moves the entry to dispatching, and the probe only re-queues entries in state unconfirmed. The call count after the initial send is therefore exactly 1, not "at most 3". The current bound still passes if a regression re-dispatches twice, which is the exact failure this test guards.

💚 Proposed assertion change
-    expect(mocks.call.mock.calls.length).toBeLessThanOrEqual(3)
+    expect(mocks.call).toHaveBeenCalledOnce()

409-409: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider fake timers for the negative-assertion waits.

Four tests wait on real wall-clock time to prove that no further call happens: Line 409 (3000 ms), Line 493 and Line 497 (300 ms and 600 ms), Line 535 (3000 ms), and Line 566 (2500 ms). That adds roughly 10 seconds of real runtime and couples the assertions to machine speed. The last test in this file already shows the deterministic pattern with vi.useFakeTimers({ shouldAdvanceTime: true }) plus advanceTimersByTimeAsync. Applying it to these windows removes the sleeps and the 20-second per-test timeouts.

src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts (1)

11-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert submission ordering in the migrated send test.

The test covers accepted and thrown-dispatch unknown outcomes, but it only checks the history row after send completes. Add an assertion that the submission row exists when dispatch runs.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 8c874b81-22f9-43af-8d09-6837f69a3ce5

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf298d and b3a95ed.

📒 Files selected for processing (30)
  • src/main/claude/claude-structured-compaction.ts
  • src/main/claude/claude-structured-dispatch-admission.test.ts
  • src/main/claude/claude-structured-dispatch-test-support.ts
  • src/main/claude/claude-structured-dispatch.test.ts
  • src/main/claude/claude-structured-dispatch.ts
  • src/main/claude/claude-structured-session-adapter-turns.test.ts
  • src/main/claude/claude-structured-session-adapter.test.ts
  • src/main/claude/claude-structured-session-adapter.ts
  • src/main/claude/claude-structured-session-close.ts
  • src/main/claude/claude-structured-session-state.ts
  • src/main/claude/claude-structured-session-test-support.ts
  • src/main/claude/claude-tui-resume-real-binary.integration.test.ts
  • src/main/codex/codex-structured-turn-start.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.ts
  • src/main/native-chat/agent-session-journal/journal-pending-submission-recovery.ts
  • src/main/native-chat/agent-session-journal/journal-store.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter.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.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.ts
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts
  • src/shared/structured-agent-session-send-disposition.ts

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

setError(disposition.error)
outboxRef.current = disposition.entries
setOutbox(disposition.entries)
writeOutbox(sessionId, disposition.entries)

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Description: Compare writeOutbox result handling across the outbox hook and its history.
set -euo pipefail

fd -t f 'use-structured-agent-session-outbox.ts' src --exec rg -n -B2 -A4 'writeOutbox\('

# Prior handling of the send-settle branches before the disposition refactor.
git log -1 --format=%H -- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts
git show HEAD~1:src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts 2>/dev/null \
  | rg -n -B2 -A4 'writeOutbox\(' || echo 'previous revision not available'

Repository: stablyai/orca

Length of output: 4358


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts
printf '%s\n' '--- current hook ---'
sed -n '1,180p' "$file"
printf '%s\n' '--- storage helper and related symbols ---'
rg -n -B5 -A12 'function writeOutbox|const writeOutbox|writeOutbox|dispatchState' src/renderer/src/components/native-chat src/renderer/src -g '*.ts' -g '*.tsx' | head -240
printf '%s\n' '--- repository conventions for this scope ---'

Repository: stablyai/orca

Length of output: 34377


🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions

Length of output: 23346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- hook settle/finally path ---'
sed -n '145,330p' src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts

printf '%s\n' '--- disposition implementations ---'
fd -t f -e ts 'structured-agent-session-send-disposition' src --exec sed -n '1,280p' {}

printf '%s\n' '--- storage implementation ---'
sed -n '1,90p' src/renderer/src/components/native-chat/structured-agent-session-outbox-storage.ts

Repository: stablyai/orca

Length of output: 13789


Handle a failed settle write.

applyDisposition ignores the result from writeOutbox. If the write fails, storage keeps the entry as dispatching. On reload, readOutbox converts it to unconfirmed; the probe can then queue and send the entry again. The earlier settle branches had the same omission. Handle this failure and prevent redispatch.

@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

One correctness issue in the retry path: a successfully redelivered write-failure is misread as a refusal and dropped, re-introducing the false-banner harm this PR removes.

Reviewed changes

This PR replaces the 10s ack deadline in dispatchClaudeTurn with admission-time settlement: dispatch returns { state: 'admitted' } as soon as the transport write completes, and the provider's echo settles the pending submission accepted through the existing late-settlement channel. Delivery doubt is now reachable only from process facts, retry redelivery is gated by a fail-closed allowlist (provider_write_failed only), and the send-outcome classification was extracted to a pure module (structured-agent-session-send-disposition.ts) alongside the existing reconciler.

  • Admission-time settlement — dispatchClaudeTurn returns admitted; waiters have no deadline and are retired only by child exit or a failed write.
  • Fail-closed retry allowlist — journal-dispatch-doubt-reasons.ts gates redelivery on provider_write_failed only.
  • Late-settlement wiring — settleWaiter now fires onSettledLate, settling the journal submission through settleLateDispatch.
  • Renderer outbox — Retry only targets the outbox head; disposeStructuredAgentSessionSendResult centralizes outcome → outbox-state folding.

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

Merge Sim added 2 commits September 10, 2026 01:52
…atch-admission-settlement

# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-pending-submission-recovery.ts
#	src/main/native-chat/agent-session-journal/journal-store.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/native-chat/agent-session-journal/journal-pending-submission-recovery.ts (1)

15-16: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve existing non-delivery reasons.

When this filter selects an existing unknown entry with a provider_write_failed: reason, resolveDispatch overwrites it with host_restarted_before_acknowledgement. dispatchDoubtProvesUndelivered recognizes only the original reason, so a later retryUnknown cannot redeliver a message the provider never received. Preserve entry.reason for existing unknown entries and use the recovery reason only for pending entries.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 683a7061-672c-42fc-9e58-12d89ad290b2

📥 Commits

Reviewing files that changed from the base of the PR and between b3a95ed and 365c043.

📒 Files selected for processing (29)
  • src/main/claude/claude-agent-sdk-user-message-queue.test.ts
  • src/main/claude/claude-agent-sdk-user-message-queue.ts
  • src/main/claude/claude-stream-json-connection.ts
  • src/main/claude/claude-structured-compaction.ts
  • src/main/claude/claude-structured-dispatch-admission.test.ts
  • src/main/claude/claude-structured-dispatch.test.ts
  • src/main/claude/claude-structured-dispatch.ts
  • src/main/claude/claude-structured-session-adapter-turns.test.ts
  • src/main/claude/claude-structured-session-state.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.ts
  • src/main/native-chat/agent-session-journal/journal-pending-submission-recovery.ts
  • src/main/native-chat/agent-session-journal/journal-reducer.test.ts
  • src/main/native-chat/agent-session-journal/journal-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-row-builders.ts
  • src/main/native-chat/agent-session-journal/journal-row-schema.ts
  • src/main/native-chat/agent-session-journal/journal-store-contracts.ts
  • src/main/native-chat/agent-session-journal/journal-store.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts
  • src/renderer/src/lib/structured-agent-session-launch-prompt.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-prompt.ts
  • src/shared/structured-agent-session-projection.ts
💤 Files with no reviewable changes (1)
  • src/main/claude/claude-structured-compaction.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/claude/claude-structured-session-adapter-turns.test.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsx
  • src/main/native-chat/agent-session-journal/journal-store.ts

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

@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

This delta hardens the admission-settlement change from the prior review and addresses the one issue I raised (an admitted redelivery being misread as a refusal).

  • Admitted redelivery resets to pending — performSend now writes resolveDispatch({ state: 'pending' }) on an admitted redelivery, so the submission comes back pending and refusedRedelivery no longer drops the entry or surfaces the false refusal banner.
  • Fail-closed write disposition — claude-agent-sdk-user-message-queue.ts splits failure into unwritten (queued/refused, provably never handed to the SDK) vs write-outcome-unknown (in-flight, ambiguous); only the former maps to the redeliverable provider_write_failed.
  • Durable echo settlement — journal-reducer.ts gains acceptSubmissionFromProviderItem, so a pending submission is accepted from the provider's echo item row even when the direct settleLateDispatch write fails.
  • Bounded active waiter queue — MAX_ACTIVE_DISPATCH_WAITERS = 64 rejects dispatch when full, preventing unbounded waiter accumulation.
  • Compaction carries no clientMessageId — provider-control turns no longer settle a journal submission via onSettledLate.
  • Recovery filter widened — markPendingSubmissionsUnknown now also covers live unknown submissions (e.g. a mid-retry crash) and takes the reason as a parameter.
  • Renderer error clearing — the outbox hook clears the delivery error when the journal settles the accepted head, and pending maps to dispatching in both the send disposition and the launch-prompt path.

Pullfrog  | View workflow run | Using DeepSeek Pro (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

This delta (d4de21b) preserves dispatch recovery evidence across restarts and closes the one carried-forward gap: the recovery pass no longer overwrites a provable non-delivery reason.

  • Retry marker durably armed — performSend now writes resolveDispatch({ state: 'unknown', reason: DISPATCH_DOUBT_RETRY_IN_PROGRESS }) before a redelivery, so a crash mid-retry leaves a non-provable marker instead of a stale provider_write_failed that would green-light a third delivery.
  • Recovery preserves proven reasons — markJournalPendingSubmissionsUnknown now keeps a live unknown entry's existing reason (except the transient retry marker) rather than clobbering it with the host/provider-exit reason, so a provably-undelivered write stays retryable.
  • Outbox refs reset on an accepted head — the hook bumps its dispatch generation and clears dispatching/blockedIdRef/error when the journal settles the accepted head, so a late transport failure can no longer resurrect the delivery-doubt banner.
  • Persisted pending restored as dispatching — the shared reconciler maps a pending submission to dispatching, preventing an already-admitted send from being re-dispatched on remount.

Pullfrog  | View workflow run | Using DeepSeek Pro (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

This delta (7061019) preserves the pre-pending send contract for older clients by negotiating a new runtime capability, and hardens a few edges of the admission-settlement path.

  • Pending-send-result capability — new agent-session.pending-send-result.v1 capability, advertised in RUNTIME_CAPABILITIES, the Electron remote/mobile client lists, and the desktop/mobile IPC lists. Capable clients receive the durable pending reply immediately; older clients keep the settled contract.
  • Compatibility wait — StructuredAgentSessionSendSettlement plus sendStructuredAgentSessionForClient block an older client's agentSession.send until the submission leaves pending, without holding the mutation queue, and reject on transport abort, session close, host close, or an authoritative drop.
  • Client-delivery refactor — StructuredAgentSessionClientDelivery consolidates the status feed, subscribers, and send settlement behind one owner; the host now delegates all publication edges (including publishStatusAndSettlement on the event-recovery path) through it.
  • Staleness guard — journalItemRevisionIsStale stops a stale provider item behind its tombstone from settling a pending submission, on both the item row and lifecycle-batch mutation paths.
  • Provably-unwritten waiters forgotten — a refused write now fully drops its dispatch waiter (rather than retiring it) so it cannot block a later retry's replay correlation; compaction fails fast on a provably-unwritten /compact.
  • Renderer outbox hostOwnsHead — a pending or accepted head is treated as host-owned, ignoring a late transport failure while preserving an unrelated tail-save error.

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

@brennanb2025

brennanb2025 commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Superseded review status

This status was superseded after the PR author correctly identified that the legacy settlement observer was unbounded and that the compatibility wording overclaimed. The branch now has a 30-second observer TTL, 64/session and 1,024/host caps, deterministic cleanup, and wording that describes bounded best-effort observation rather than preservation of the former settled-reply contract.

The current exact-head review, direct answers, module coverage, scope classification, validation, and remaining gaps are recorded here:

#19863 (comment)

No merge was performed.

Merge Sim added 3 commits September 10, 2026 12:28
…atch-admission-settlement

# Conflicts:
#	src/main/ipc/runtime.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts

@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

This delta (cea9cb39, plus the 023b69a0 merge of origin/main) bounds the legacy send-settlement observation and closes one echo-matching gap — no behavioral changes from the unrelated main commits were in scope.

  • Bounded legacy settlement observation — the pre-capability compatibility wait now has a 30s unref'd timeout plus a 64-per-session / 1024-global waiter cap, so a legacy client that abandons the RPC at 15s can no longer leave an unbounded retained observer.
  • Close resolves rather than rejects — closeSession/closeAll resolve observers with undefined (yielding the durable pending reply) instead of surfacing a false "send failed" error for a message that was already admitted.
  • Proven-undelivered echo guard — resolveJournalItemId now uses dispatchMayMatchProviderEcho, so a newer identical send — never the older provider_write_failed submission — is what a provider echo settles accepted.
  • Regression coverage — fake-timer timeout test, per-session and cross-session cap tests, the close-resolves test, and a reducer test pinning that dispatchMayMatchProviderEcho skips the proven-undelivered submission.

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

@brennanb2025

brennanb2025 commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Correction and final review status

The concern was correct. The earlier implementation did not preserve the old bounded reply contract: a legacy client could leave the server waiting for provider settlement for the duration of the preceding turn, and transport timeout/cancellation was not guaranteed to retire that server-side wait. The prior status statement that old clients retained their settled-send contract was too strong.

That is fixed on the current branch. The compatibility path is now explicitly documented as “best-effort settlement observation,” bounded to 30 seconds, with bounded retained state and teardown on every host lifecycle path.

Direct answers

  1. Was the missing internal bound deliberate? No. Relying on the caller’s AbortSignal was insufficient because a known legacy mobile timeout abandons the request locally without necessarily cancelling the socket dispatch. The host now has its own 30-second TTL. A wait also ends on settlement publication, transport abort, session close, host close, or an authoritative provider-exit publication. Current desktop IPC returns pending immediately; current mobile direct and relay advertise agent-session.pending-send-result.v1 and also return pending immediately. A legacy direct/relay client uses the bounded observer. Structured RPC remains capability-gated on remote client paths that do not support it, and any supported remote path uses the same host-side bound rather than relying on network teardown.

  2. Could a legacy send RPC wait for the entire preceding turn? Before this correction, yes. Now the server-side compatibility wait cannot exceed 30 seconds. While a legacy desktop request is open, its optimistic outbox entry remains dispatching and preserves FIFO order; an observed acceptance removes it. If the 30-second observer expires, the host returns the durable pending result, which an old desktop may still render as unconfirmed. Legacy mobile has a tighter 15-second request budget: it clears the draft while sending, unlocks after its transport reports unknown, and then gives the transcript echo another 20 seconds to suppress the warning. If no echo lands, it shows “Delivery unconfirmed…” roughly 35 seconds after the send. This is better only as bounded best-effort compatibility: quick settlements avoid the old warning, while very long queued turns can still produce the legacy UX. The complete UX fix is the negotiated pending result used by current clients.

  3. Back-pressure and teardown: compatibility observers are capped at 64 per session and 1,024 per host. Overflow retains no new observer and immediately falls back to the original durable pending result. Timeout, session close, and host close remove the timer/listener/count and fall back to pending; abort removes them and rejects with the transport reason; an authoritative missing submission rejects; provider exit marks the submission unknown, publishes it, and resolves observers with that terminal state. If terminal publication itself fails, the 30-second observer TTL still retires the waiter. The separate Claude provider-identity state is also bounded at 64 active and 64 retired identities; new dispatch is refused when the active cap is full, and child exit retires/resolves active identity waits. No waiter leak remained in review.

  4. Contract wording: agreed—the contract changed; it was not preserved. The class comment and this PR description now say bounded best-effort legacy observation, not preservation of the former ~10-second settled-reply liveness contract.

The four previously uncovered modules

All four were reviewed by the same-model, same-effort adversarial reviewer to the same standard as the rest of the change, including a fresh pass after the latest main merge.

  • structured-agent-session-client-delivery.ts: centralizes all host-to-client journal/status publication, settlement notification, and close teardown. The extraction is organizational; routing every journal publication and lifecycle close through it is load-bearing so a parked compatibility observer cannot miss its wakeup or cleanup.
  • journal-reducer.ts: correlates a later provider item with the durable Orca submission during both live folding and replay, turns it into an accepted submission/receipt, and preserves the original bubble/order. This is load-bearing for late settlement and restart correctness. Review found and fixed one real defect here: an older identical but proven-unwritten submission could previously steal a newer provider echo.
  • journal-item-revision.ts: rejects stale or tombstoned provider revisions before alias adoption. Without that ordering, an obsolete provider row could falsely accept a pending submission. The guard is load-bearing; the small file boundary is organizational.
  • claude-agent-sdk-user-message-queue.ts: this is the write-admission boundary, not a second ordering queue. FIFO remains queued.shift() with exactly one inFlight frame, and the generator cannot pull the next frame until the SDK resumes it after awaiting the previous transport write. A frame still in queued is provably unwritten; a frame already pulled by the SDK has an ambiguous write outcome if the pump aborts. This distinction is load-bearing because it is what permits safe retry only before write while preventing duplicate sends after an ambiguous write. Ordering and fail/close paths were specifically tested and reviewed clean.

Load-bearing versus defensive

Load-bearing for correctness:

  • durable journal submission before provider dispatch;
  • SDK write-completion admission and queued-versus-in-flight provenance;
  • provider echo/history correlation, including live and replay settlement;
  • stale-revision/tombstone guard and repeated-identical-message ordering;
  • pending-aware desktop/mobile disposition and outbox FIFO ownership;
  • retry/correlation retention; and
  • capability negotiation so mixed-version clients do not misinterpret pending.

Compatibility/defensive/resource-safety:

  • the legacy settlement observer itself (needed only to give old clients a best-effort settled reply);
  • its 30-second TTL, 64/session and 1,024/host caps, and teardown paths;
  • the 64-active/64-retired Claude identity caps and associated diagnostics; and
  • the client-delivery and revision-guard module extractions as file organization (their wiring/guards remain load-bearing).

The dispatch-doubt allowlist is functionally load-bearing because it prevents unsafe redispatch; its more detailed reason names and diagnostics are defensive.

Architecture and validation judgments

  • Functional correctness: clean after the bounded-wait fix and the identical-message reducer fix.
  • Architectural fit: the root cause is removed by separating SDK-write admission from later provider identity settlement. Orca intentionally owns durable admission in its journal because this SDK offers no synchronous canonical provider receipt; systems that can establish canonical target acceptance during insertion do not need that two-stage boundary. Stable identity, target-owned admission, bounded retained resources, and explicit ambiguity still match the required mechanism.
  • Performance/resources: clean. Capable clients do not wait for provider echo; legacy observation is event-driven and capped, and no polling, broad scan, subprocess fanout, or render loop was added.
  • Validation: exact head 942c3e3e338163b4fdccee64d5dcceb124659b37 passed 323 focused tests across 22 files, pnpm tc:node, changed-code quality, and git diff --check; a separate exact-production-head mobile direct/relay capability run passed 18 tests. Earlier branch validation included the full mobile suite, authenticated real-Claude integration, hidden-CDP Electron proof, and broader reliability gates. Exact-head GitHub CI also passed all eight test shards, Linux and Windows packaging, static analysis, typecheck, cross-version wire compatibility, repository guards, and pullfrog. Exact-head SSH and native mobile-emulator runs were not repeated after the final test-only main merge; those gaps remain explicit in the PR body.

The requested orchestrated Grok Electron worker was attempted but blocked by its weekly quota and produced no evidence. The two hidden-CDP Electron screenshots already in the PR body are the valid visual proof; no desktop-control fallback was used.

No merge was performed.

@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

This delta is three merge commits of origin/main (17b9000, aeac9bf, 942c3e3) with no PR-authored commits; the PR's admission-settlement logic itself is unchanged. I verified the merge reconciliation is coherent, not just clean.

  • Composer-focus feature reconciled — main's feat(native-chat): focus the message box when a chat appears (#19868) brought the isFocusedGroup prop into NativeChatStructuredSession, plumbed through to useNativeChatComposerRevealFocus, and the test composer mock now attaches a real useRef DOM node so the reveal-focus loop can verify focus ownership.
  • Test-fixture split reconciled — main's test(native-chat): split structured question fixtures (#19924) introduced claudeGroupedQuestionPromptItems, wired into the grouped-question test.
  • Independence confirmed — the outboxHead retry logic and the focus plumbing share no state and no conflicting resolution; no conflict markers, and isFocusedGroup flows correctly through NativeChatStructuredViewProps.

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

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Final review status: clean at exact pushed head 942c3e3e338163b4fdccee64d5dcceb124659b37.

The legacy-wait concern was valid and is fixed: observation is bounded to 30 seconds, retained observers are capped at 64/session and 1,024/host, teardown is deterministic, and the code now describes this as bounded best-effort compatibility rather than preservation of the former contract. The independent review also covered structured-agent-session-client-delivery.ts, journal-reducer.ts, journal-item-revision.ts, and claude-agent-sdk-user-message-queue.ts to the same standard; no meaningful in-scope finding remains.

The end-of-session readiness checklist is clean. All applicable exact-head CI checks passed, including all eight test shards, typecheck, static analysis, cross-version wire compatibility, Linux and Windows packaging, repository guards, and pullfrog. The PR is OPEN, MERGEABLE, with merge state CLEAN; the worktree is clean and local/remote heads match.

Detailed answers and load-bearing-versus-defensive classification: #19863 (comment)

No merge was performed.

…atch-admission-settlement

# Conflicts:
#	mobile/src/transport/mobile-runtime-client-capabilities.ts
#	src/main/ipc/runtime.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
#	src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx
@brennanb2025
brennanb2025 merged commit 027acb4 into main Sep 10, 2026
28 of 29 checks passed
brennanb2025 added a commit that referenced this pull request Sep 13, 2026
…e meaning (#20133)

* refactor(native-chat): give each structured dispatch state exactly one meaning

`unknown` meant five different things. Only one of them was genuine
ambiguity.

A transport write that the provider's input pump never took is provably
undelivered -- which is what `rejected` already means. It was recorded as
`unknown` anyway, and a one-entry allowlist then existed solely to teach
Retry that this particular `unknown` was safe to re-deliver.

Collapsing that case into `rejected` deletes the allowlist and turns a
predicate into an invariant: Retry never re-delivers an `unknown`, with no
exception to reason about. The four states now each assert one thing --
`pending` written and awaiting, `accepted` the provider has it, `rejected`
provably did not happen, `unknown` genuinely cannot tell.

A fail-closed guard is the right default here because the asymmetry is
severe: refusing a legitimate retry costs the user a retype, while allowing
an illegitimate one sends the model a second copy of their message.

Also fixed, found while auditing every reader of `rejected`:

- The renderer printed `submission.reason` verbatim, so a broken pipe put
  the internal token `provider_write_failed: broken pipe` on screen in
  destructive red. The journal reason is unchanged -- it is the durable
  evidence and the transport-versus-content discriminator -- but the screen
  now gets copy that names the cause and says the message is safe to
  resend. Content rejections still show the provider's own words.
- The fallback copy "Message was not accepted" read as a content refusal.
  A null reason now yields "Message was not sent.", which asserts only what
  every rejection shares.
- A refused worker-start preamble threw a plain Error out of the dispatch
  path. It now throws `OrchestrationError('dispatch_preamble_undelivered')`
  so a coordinator can tell "we could not send it" from "we sent it and
  something else broke" without parsing prose. Retain/discard behaviour is
  unchanged; only the verdict's legibility improves.

Two behaviours improve as a consequence rather than by design: a provably
undelivered message no longer blocks conversation commands, and no longer
leaves the session reading as "working" in chat and in every session list.

Not addressed here, and named rather than implied: a message left `unknown`
by a dead child or a host restart still has no recourse but retyping. The
restart reconciler that would decide those on evidence is written and has
never had a production caller. Parking the refused entry instead would
reintroduce the head-of-queue wedge removed in #19863, so it is not an
option.

Note for whoever edits `journal-reducer.ts` next: it sits at 297 of its 300
counted lines. The next statement added there needs a split, not a shave.

* fix(native-chat): close two gaps review found in the rejection taxonomy

Both are narrow and both were real.

A journal written before a refused write became `rejected` still holds that
submission as `unknown` with the transport marker. The predicate this change
replaced excluded exactly that shape from provider-echo matching; the
state-only check that replaced it does not, so on replay such a row could
claim the echo of a later, genuinely delivered send of the same text and
attach the delivery to the wrong message. Fail-closed still prevented any
re-delivery, so nothing duplicated — but the wrong submission was credited.
Replay now excludes the legacy shape too.

And the content-versus-transport split had a third case neither side covers:
a local capacity refusal is neither the provider explaining itself nor a
frame that failed to leave. It fell through to the verbatim branch, so
`claude structured dispatch queue is full` reached the screen — the same
class of leak this change set out to fix, one reason short of being caught.
Internal reasons now get copy; only a provider's own words are shown as
written.

Each is pinned by a test that fails with its guard reverted and passes with
it restored.

* fix(native-chat): preserve dispatch refusal across clients

* fix(native-chat): rotate immediately rejected retries

* docs(native-chat): correct rejection taxonomy reference

* docs(native-chat): align mobile retry comment

* docs(native-chat): clarify unknown replay semantics

* fix(native-chat): keep a mobile send's operation id when delivery is unknown

Mobile released the retained operation id whenever a send came back
`unknown`, so the user's next send of the same text went out under a fresh
id. A fresh id has no ledger row, so the host treats it as a first delivery
and dispatches it -- even though `unknown` is the one answer that says the
provider may already have the message. That is the duplicate this branch
exists to remove, reintroduced on the client that has no outbox.

Which case that was matters. Mobile only ever sees `unknown` from ack-loss
(`isRpcDeliveryUnknown`: "the host may have processed it and only the ack
was lost"), because the mapper reported every `ok` result as `accepted`
without reading `dispatchState`. So the rotation fired exclusively where
delivery was ambiguous and never where it was provably refused, which is
the inverse of the rule this branch establishes.

Retaining the id is what makes a retry safe, and it costs no liveness:
`performSend` answers a second request under a recorded id from the journal
and never puts it back on the wire, so a reused id delivers when nothing
landed and replays when something did. Rotating can only ever add a second
copy. The retention stays bounded by the host's admission window, which
`retainStructuredSessionOperationId` already enforces.

`retryUnknown` goes with it: the host ignores it for delivery, and all it
does is skip the cached answer to re-read the same row.

Keeping the id exposes what the rotation was hiding, so fix that too: a
replayed `unknown` comes back `ok`, and mobile called it `accepted` and
cleared the composer as if the message had landed. `dispatchState` now
decides, in one pure function:

  accepted/pending  sent, and the id is spent
  rejected          provably did not happen and terminal in the reducer, so
                    reusing the id could only replay that rejection: spent,
                    and the next attempt is a first delivery under a new id
  unknown           keeps its id

Reading `dispatchState` at all is a pre-existing defect, fixed here because
the false "sent" cannot be removed without it, and scoped to the send path.
`mutate`'s rotation for prompt/option/cancel plans is untouched. The
rejection copy is the desktop's notice, so an internal reason
(`provider_write_failed: ...`) still never reaches a person.

Tests: the hook test that was flipped to assert a rotated id now pins the
opposite -- one id across an ack-loss and two `unknown` replays, each
reported `unknown` rather than `accepted`. The send fixture grew the durable
submission row a real host returns; without it every send test asserted
against a shape that cannot express the bug.

* fix(native-chat): enforce fail-closed structured send replay

* fix(native-chat): align retry and mobile RPC contracts

* fix(native-chat): keep transient admissions retryable

* test(tab-bar): expand nested create menu in harness

---------

Co-authored-by: Merge Sim <sim@local>
salvadorgu7 added a commit to OrbittechIA/orca that referenced this pull request Sep 23, 2026
* feat(work-items): port structured Start to Orca 1.4.203

Work Item Start opens exactly one structured agent session for a linked work item,
delivers its prompt once, and refuses before creating anything when it cannot.

A squash-port of the proven 1.4.201 checkpoint (05ac9d8)
onto the official 1.4.203 base (tag v1.4.203, commit 776e424,
tree df262ab). 1.4.203 ships no Work Item Start surface, so
the whole contract still applies; the substrate it lands on changed in eight files and the
delta was re-decided there instead of replayed.

Contract (unchanged from the checkpoint)
- `agent-session.work-item-start.v1` is a negotiated runtime capability; an older host does
  not advertise it and the route refuses instead of half-opening a session.
- `launchOrigin: 'work-item-start'` admits one launch to the structured route and
  `launchAuthority` records who admitted it; both are host-derived, never wire-accepted.
- Strict (`submit-after-ready`) launches declare no `legacyFallback`: a definitive refusal
  settles as `failed` and no terminal writer opens behind it.
- The strict preflight runs BEFORE `git worktree add` and before `createFolderWorkspace`.
- A structured session is created on the runtime that owns its workspace; an ambiguous
  owner fails closed.
- `worktree ps` projects the structured `sessionId` and provider handle for Core adoption.
- Packaged builds embed version/commit/tree/buildId; `status.get` publishes it and
  `runtime.buildAttestation` hashes the running executable on demand.

Re-decided on 1.4.203
- Unknown send refusal: 1.4.203 (stablyai#20133) retains the operation id and replays the same
  envelope through the host's durable send plan, and names parking the entry as the
  head-of-queue wedge stablyai#19863 removed. The checkpoint's `unconfirmed`/-1 parking hunk in
  `requeueStructuredAgentSessionSendRefusal` is dropped; the official function is kept
  verbatim. The Start path still reports `deliveryUnknown` from the refusal code so no
  second writer is opened, and its test now asserts the official same-envelope replay
  (`retryUnknown` is compatibility-only on 1.4.203 and is no longer sent).
- RPC params type parity (new in 1.4.203) required `runtime.buildAttestation` in the
  generated catalog; regenerated with the official script.
- Mobile Tasks parity hashes recomputed over the merged 1.4.203 source.
- Localization catalog: the Start strings now have `en.json` entries, which the 1.4.201
  checkpoint lacked; `verify:localization-*` passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Re-decided on 1.4.209 (port of this commit onto stablyai/orca v1.4.209)
- 1.4.209 (stablyai#20502) removed the renderer terminal fallback and made structured launches open
  a provisional chat tab that owns its own retry. Strict Start no longer needs to withhold a
  `legacyFallback`: none exists. Strict worktree/direct/folder Starts now await the launch
  settlement and prompt delivery on top of the provisional handle; non-strict launches keep
  the upstream fire-and-forget behaviour.
- The pending-creation recovery lane (`worktree-creation-structured-recovery.ts`) that
  1.4.209 deleted is kept for strict Start only; its retry re-enters the launch registry,
  which restarts an unknown launch under the same intent instead of minting another.
- The shared launch-prompt dispatch (stablyai#20502) returns a boolean; `deliveryUnknown` is carried
  beside it, and read back from the outbox entry when the mounted chat ran the shared send.
- stablyai#20944 stopped routing on terminal arguments (`requiresTuiLaunchCommand`); the strict
  preflight and planners follow it, so they predict the same route as the post-create plan.
- `launchOrigin` and the owning runtime target now survive the intent retry/restore split
  and the reload-persisted launch record (the create fingerprint covers `launchOrigin`).
- Mobile: the createSupport probe uses the typed RPC operation; the new-worktree route
  builder carries the combined setup + session warning.

* fix(work-items): scope every status event per subscriber; certify against 1.4.203

Two blockers the independent review found on 4e098c4, each with a regression that fails
on the previous code.

Status feed access scope (P1)
- `subscribe(includeSession)` filtered only the opening snapshot; `broadcast` looked for a
  top-level `sessionId`, which a `status` event never has (it names its session at
  `event.session.sessionId`). A paired runtime denied a Work Item Start session therefore
  received every later publish and ownership revoke for it.
- Every session-bearing event now passes the subscriber's filter (`scopeStatusEvent`):
  `status` is withheld, `snapshot` is filtered, `end` always arrives. The filter is dropped
  together with its subscriber on direct `unsubscribe`, on the disposer, and on emit failure;
  a re-subscribe under the same id never inherits an earlier filter.
- Regressions: the feed itself (denied subscriber gets an empty snapshot and zero status
  events across publish and revoke, the allowed session still flows, `end` exactly once,
  filter cleanup on both removal paths) and the RPC boundary through
  `agentSession.subscribeStatus` with `canAccessWorkItemStartStructuredSession`, for a
  paired runtime and a mobile client on their own connections while the owner's connection
  receives both events. The RPC fixture now shares one real feed per test over a journal the
  test can advance, and exposes the live reply list of a streaming call.

Certification baseline
- `WORK_ITEM_START_E2E_BASELINE_VERSION` was still `1.4.201`, so this candidate's own
  evidence validator would have rejected a correct 1.4.203 paired run. It is `1.4.203` now,
  fixed rather than derived from the manifest: the manifest says what was packaged, the
  constant says what the owner certifies, and a self-consistent 1.4.201 pair must still be
  refused. Fixtures bind to the official v1.4.203 base (776e424 / df262ab, buildId
  7bbdd813f784, as `build-provenance.mjs` derives it). Matched 1.4.199 and 1.4.201 pairs are
  rejected on both sides even when they match their own manifest; a 1.4.201 side behind a
  1.4.203 side is rejected by baseline, identity, and version mismatch. The Windows-client,
  distinct-artifact, commit/tree/buildId, capability, single-executor, one-prompt, and
  zero-terminal checks are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(work-items): close the remaining 4e098c4 review findings (A–H)

Each finding has a regression that fails on 8b87c61 and passes here; each was proved by
reverting only that finding's implementation.

A  Strict quick-create keeps `launchOrigin: 'work-item-start'` and declares no
   `legacyFallback`; a definitive refusal settles as `structured-refused` (workspace kept,
   no writer, no terminal, Retry allowed).
B  Every launch exposes `recovery = { intent, clientMessageId }`. The pending creation
   persists it on an unknown launch/delivery and Retry re-enters with `recover`: the exact
   intent (same session id and create envelope, replayed through `reconcileUnknownLaunch`)
   and the staged prompt looked up by operation id. A lost staged entry is unknown, never
   success. Prompt staging moved to `structured-agent-session-launch-staging.ts`.
C  A definitively refused launch prompt is deleted from the outbox instead of requeued
   under a fresh id; only unknown / not-yet-admitted refusals retain and replay the same id.
D  Mobile admission is tri-state (`terminal` / `structured` / `refused` / `unknown`) with a
   10 s status timeout; both Start entry points stop BEFORE `worktree.create` on refused or
   unknown and never fall back to the terminal.
E  The mobile send envelope is persisted in the operation journal before dispatch;
   `agent_session_operation_unknown` on `agentSession.send` is unknown; unknown outcomes
   replay the same operation id a bounded number of times and stay persisted until settled.
F  `prepareStructuredAgentSessionCreateForWorktree` passes `expectedWorktreeTarget` to the
   resolver, where the full record comparison (path, instance, identity, host, creator) runs.
G  `worktree ps` merges the host's projection with the hook-store row that shares its pane
   key: one row per session, with `sessionId` and provider handle.
H  Build provenance is fail-closed: any working-tree change yields no identity, an
   `ORCA_BUILD_COMMIT/TREE` that contradicts (or cannot be confirmed by) the repository
   throws, the packaged `app.asar` bundle is verified in `afterPack`, the candidate manifest
   is written after every artifact, `build:*` run `verify:build-provenance`, and the paired
   spec requires `ORCA_CANDIDATE_MANIFEST`. `ORCA_BUILD_UNCERTIFIED=1` is the only escape and
   embeds nothing certifiable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Re-decided on 1.4.209
- B lands on the 1.4.209 launch registry: a recovery re-enters the registered state when it
  still exists (a restart that finds, rather than skips, the staged operation) and otherwise
  rebuilds it from the persisted intent through `reconcileUnknownLaunch`. Publication is
  verified with the authoritative tab snapshot (stablyai#20502 removed the renderer-tab shortcut).
- A: 1.4.209 has no terminal fallback, so a refused strict create simply settles `failed`
  and maps to `structured-refused`.
- `recovery` is optional on hand-built settlements; the settle loop always sets it.
- D: the mobile admission read goes through a typed `status.get` RpcOperation, as the
  1.4.209 raw-request-port ratchet requires.

* fix(build): do not count electron-vite's transient config bundle as a dirty build input

The first fail-closed package refused itself: electron-vite bundles its config into
`electron.vite.config.<timestamp>.mjs` in the working directory while the config loads —
which is when `readBuildProvenanceLiteral` runs — and deletes it afterwards. It is derived
from the tracked config, not an input. Ignored in git and excluded by exact pattern in
`dirtyBuildInputs`; a modified config or any other stray file still yields no identity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(work-items): strict quick-create never reads missing delivery evidence as success

Re-review counterexample on e7567c6: a structured settlement that resolves with a session
but carries no prompt-delivery state completed the strict creation, because
`!delivery || delivery.delivered` took the success branch. Missing evidence is not evidence
of delivery. In strict mode an absent delivery result now settles as `promptDeliveryUnknown`
with the launch's `recovery`, so Retry reconciles the same session and operation; nothing
completes, nothing mints a second session, and no terminal fallback exists on this path.

Regression: the counterexample itself, asserting durable unknown with the recovery intent,
one launch, and only the structured chat's own activation (never a startup terminal).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(build): bind provenance to the real embed site and keep the transient bundle out by timing

Two re-review holes in the provenance gate.

P1 — a name-based whitelist admitted ANY `electron.vite.config.<digits>.mjs` as clean, so an
unverifiable file could hide under a numeric transient name. No name is exempt now: the build
wrapper (`run-electron-vite-build.mjs`) reads the identity BEFORE electron-vite writes its
transient config bundle and hands it to the config through
`ORCA_ELECTRON_VITE_BUILD_PROVENANCE`; the config accepts it only if its commit and tree are
what git reports at load time (malformed, contradicted or unconfirmable hand-overs throw), and
falls back to the strict read without it. The gitignore entry is gone, so a lookalike is a
dirty input like any other and the identity is null. Regression: the exact counterexample
(`?? electron.vite.config.123456789.mjs` → dirty → null) beside the genuine path (hand-over
accepted while the transient is on disk).

P2 — the bundle verifier accepted any bundle in which the expected strings occurred somewhere,
so `value:null` plus a comment passed. The runtime read site now carries an exact sentinel
(`site:'orca:build-provenance:embed', value:<substituted>`, checked at runtime so no minifier
drops it) and the verifier parses the value right after that sentinel: exactly one site, a
well-formed identity, equal to the expected one; null, an unreplaced identifier, a malformed
object, two sites, or another build's identity all refuse, whatever else the bundle contains.
Regressions: null + expected strings in a comment; forged identity at the site + expected
strings elsewhere; duplicate sites; unreplaced identifier; sentinel byte-equal on both sides.

Packaging hooks: `artifactBuildCompleted` validates the AppImage synchronously again and
returns the manifest write; `ORCA_BUILD_UNCERTIFIED` is read per call. The electron-builder
hook tests broken since b52da80 are fixed (layout tests run as throwaway packages) and two
deterministic refusals added: a manifest under a contradicted override, and an afterPack
whose packaged bundle embeds null.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(work-items): settle mobile M1 only on ledger settlement, hold the workspace lifecycle through attach, full sha for mac local builds

Final source round of the independent review (R1–R3), each with a regression that fails on
the previous implementation.

R1 — mobile durable journal. `deliverWorkItemStartPrompt` cleared the persisted M1 for every
non-unknown mutation result, so a lost reply followed by an `agent_session_operation_capacity`
("try later") replay deleted M1 and reported undelivered while the host may hold it. The
canonical `agentSessionRefusalOperationState` now decides: `unknown` and `pending-admission`
(and any code the ledger does not classify) replay the SAME operation id a bounded number of
times and, still undecided, stay persisted and are reported `unconfirmed` with the exact
envelope; only `settled-rejected` (and a pre-handler failure that never reached the host)
clears. M2 is never minted.

R2 — worktree target TOCTOU. The full expected-target comparison ran in the resolver, then
launch preparation awaited, then `attach` ran with nothing keeping the record what it was.
The runtime now has a workspace lifecycle hold (a second instance of the existing
shared/exclusive `WorktreeTerminalMutationLock`, its own key space): a structured create
holds the shared side from authoritative resolution through attach — scoped creates before
resolving, generic ones once resolved — and `removeManagedWorktree` (every removal branch,
therefore every replacement) takes the exclusive side around the whole removal, refusing as
`worktree_lifecycle_busy` past a bounded wait rather than being granted later. Belt to that
brace: `commit` re-resolves the exact full target (path, instance, identity, host, creator)
under the hold immediately before attach and refuses on any difference. Deterministic
regressions with the real `OrcaRuntimeService`: A attaches once; A replaced by B during a
paused preparation → attach never called; a removal waits for the in-flight create; a create
arriving during a removal sees B and is refused; a removal past its deadline is busy. The
terminal-only lock is not used: removal never took it.

R3 — mac local build. `getLocalBuildIdentity` passed `git rev-parse --short=12` as
`ORCA_BUILD_COMMIT`, which the provenance gate compares byte-for-byte with `rev-parse HEAD`,
so the local mac build refused itself. It now hands over the full sha (and refuses anything
else); only `createLocalBuildVersion` abbreviates it for the human version suffix. The
verifier is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(work-items): strict quick-create fails closed on a thrown launch or a generic failed settlement

Last material blocker of the independent review on 0fd77da: with a strict (`submit-after-ready`)
non-empty prompt, `launchStructuredWorktreeSession` still answered `accepted: true` when the
launch threw during launch-intent creation (an ambiguous runtime owner) or when the settlement
was a generic `{ kind: 'failed' }`; the caller then completed and removed the pending creation
with no structured session and no delivery proof.

Both now settle as `failure: 'structured-launch'` in strict mode — the same discipline as a
host refusal: the workspace exists with no writer, no legacy terminal opens, the creation is
marked as a retryable error instead of completed, and nothing mints a second session. The
non-strict path is unchanged.

Regressions (the strict suite moved to `worktree-creation-structured-session-strict.test.ts`
to stay under max-lines): a thrown `worktree_runtime_owner_ambiguous` from the launch's sync
prologue, and a generic failed settlement, each asserting `accepted: false`,
`failure: 'structured-launch'`, no fallback claim, no startup terminal, no after-wake terminal,
no trust preflight; and, at the flow level, that such a result is kept on the retry surface
and never completes or removes the pending creation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Re-decided on 1.4.209: the strict lane also fails closed when the provisional chat surface
could not open (reveal failed) and the creation was not cancelled; the strict suite runs the
real plan, settle loop and provisional tab, and asserts the only activation is the chat's own.

* fix(work-items): strict quick-create fails closed when the launcher itself throws

The same fail-open one level above e1d9c5e's fix: the caller of
`launchStructuredWorktreeSession` in `worktree-creation-flow-execute.ts` caught any exception
that escaped the launcher, logged it, and fell through to `completeWorktreeCreation` with
`structuredLaunchAccepted` still true — no session, no delivery proof, pending creation removed.

For a strict (`submit-after-ready`) Work Item Start an escaped exception now marks the
existing workspace with the retryable `structured-launch` error and returns before completion:
no legacy terminal, no pending removal, no second session. The non-strict path keeps its
historical behaviour (treated as a failed-but-accepted launch).

Regression at the caller level: `launchStructuredWorktreeSession` itself is mocked to reject
(not the plan inside it); `completeWorktreeCreation` never runs, the pending entry is a
retryable `structured-launch` error bound to the worktree, and no startup / activation
terminal is seeded. Fails on e1d9c5e's file, passes here; the helper-level throw and generic
failed-settlement tests are kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(work-items): clear the changed-code quality gate (83 type-assertion findings) without behaviour change

`check:code-quality:changed` against the official v1.4.203 base reported 83
`typescript/consistent-type-assertions` findings across the port (9 production sites, 66 in
tests). It now passes with 0 findings.

Production: checked narrowing instead of casts. A tiny shared guard (`isUnknownRecord`) narrows
untyped input in the build-provenance parser, the launch-authority parser and the mobile
admission reader; the attestation stream is typed as an `AsyncIterable<Buffer | string>`; the
pre-create preflight declares the detected-agents promise type the routing already requires;
the session-tab projection spreads the base projection over the change instead of asserting;
the IPC dispatch response needed no assertion at all. One production cast is unavoidable and
carries the repository's line-specific SAFETY rationale: the `globalThis.ORCA_BUILD_PROVENANCE`
read, which the packager's define substitutes textually and which the cli/mobile tsconfigs
(no `build-constants.d.ts`) also compile. The inherited web settings API `satisfies … as`
carries one too (the web preload implements a checked subset).

Tests: `vi.hoisted` state is declared with types instead of asserted; RPC/mock params are read
through `isUnknownRecord`; `JSON.parse` results are assigned to typed bindings; DOM elements are
narrowed with `instanceof`; worktree-target fixtures are typed with `Pick<Worktree, …>`.
Fixtures that deliberately pin a subset of a large interface (runtime stores, structured hosts,
protected resolvers, GlobalSettings) keep a single SAFETY-annotated cast each, usually behind a
small helper so one rationale covers the repeats. No rule is disabled and no allowlist grows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(work-items): recompute Mobile Tasks parity hashes over the merged source

The refactor-parity guard pins hashes of the Mobile Tasks source; the port
onto v1.4.205 merges the Work Item Start changes into upstream's RpcOperation
migration of the same files, so the screen-hook, statement, declaration and
semantics fingerprints (and the semantics line count, 3452 -> 3458) are
recomputed over the merged tree, exactly as the 1.4.203 port did. Render tree,
style and diff-hook fingerprints are unchanged. No behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Re-decided on 1.4.209: recomputed over the Work Item Start changes merged into the 1.4.209
Mobile Tasks source (semantics line count 3274 -> 3280); render tree, style and diff-hook
fingerprints unchanged.

* fix(work-items): scope the workspace lifecycle hold per execution host on 1.4.205

Upstream v1.4.205 runs same-id workspace removals on different hosts
concurrently (a local row and an `ssh:*` row under the migrated spelling),
and its integration suites build partial runtime fakes. The reviewed R2 hold
(0fd77da) keyed the lifecycle lock by worktree id alone, so on the 1.4.205
base the remote removal waited on the local one until `worktree_lifecycle_busy`
and three upstream integration suites failed with
`holdWorktreeLifecycle is not a function`.

- `runtimeWorktreeLifecycleKey` keys the hold per execution host; no host is
  the local machine, which is what an unscoped hold always meant.
- The create path holds the host the admitted target names (or the resolved
  location's host); the removal path holds the host it routes to, resolved
  from the explicit host or the owner repo before the hold is taken.
- The removal hold helper receives the scope it needs instead of a bare id.
- Upstream's three integration fixtures declare the hold, as the reviewed
  fixtures already did.
- Regression: a remote exclusive hold neither waits for nor blocks the local
  create, and the local scope is still the one the create held.

Verified on this tree: `pnpm tc`; the previously failing suites plus their
neighbours (orca-runtime, orca-runtime-tests, structured-session
integration/replay/claude, create-lifecycle, precommit refusal, worker seed
options, worktree ps: 8 files, 1320 tests) pass; oxlint on changed files;
`check:code-quality:changed` against 11aba8b: 0 findings; `git diff --check`.
The one remaining failure in the targeted desktop run,
`hourly-build-version.test.mjs` "stays on the already-shipped hourly base",
fails identically on pristine v1.4.205 (upstream's own stale expectation) and
is not part of this delta.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: repair v1.4.205 port CI and Windows provenance paths

Re-decided on 1.4.209: the legacy adapter freeze hunk is dropped — 1.4.209 (stablyai#20872) already
reads own-property descriptors, which is the lint fix this worked around; the added freeze
assertions are kept and pass. The mobile admission read keeps its typed operation.

* test: check Windows target watcher in architecture packaging guard

* test: use retrying cleanup for Windows package fixtures

* fix(work-items): preserve scoped Start authority at production callers

* fix(work-items): retain strict delivery across incomplete settings refreshes

* fix(work-items): preserve legacy settings wire behavior outside strict Start

* fix(codex): preserve yolo semantics in structured app-server

Re-decided on 1.4.209: stablyai#20944 deleted `codex-structured-app-server-args.ts` and stopped
passing configured CLI arguments to app-server. The Yolo posture now travels as the
thread's explicit `approvalPolicy: never` / `sandbox: danger-full-access` on thread/start
and thread/resume, derived from the bypass flag wherever it sits in the arguments, and
conflicting `-c approval_policy` / `--sandbox` tokens never reach app-server. So the patch's
`-c` overrides are dropped with the deleted file, and its guarantee (Yolo is final regardless
of argument order) is pinned against `codexStructuredPermissionPolicyForSettings`.

* fix(mobile): restore the 1.4.209 web client context seam

The 1.4.205 CI repair moved the RPC client context into a native-only
rpc-client-context.ts and pointed host-client-hooks at it. 1.4.209 ships a
client-context.web.tsx with its own context, so on the web page every
useHostClient read a context the web provider never fills: 27 bridge-parity
goldens failed (base: 793/793). Back to upstream's import layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(work-items): adapt direct and composer Start to 1.4.209

Split the strict submit-after-ready half of launchWorkItemDirect into
launch-work-item-direct-strict-start.ts and the mobile composer Start into
composer-work-item-start.ts, with unchanged behaviour. The strict pre-create
preflight keeps reading the settings snapshot taken at launch rather than
re-reading the store after the awaits.

1.4.209 keeps structured launches with configured agent arguments and refuses
only a command override, so the unavailable copy now says "custom launch
command" and the folder composer test pins arguments as non-blocking. Test
fixtures follow 1.4.209 APIs (retired-name registry, agentLaunchSupported,
repoName, provisional-tab store fields, cancelStructuredAgentLaunch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mobile): read a missing support method as a verdict

The Start's probeFailed marks an unanswered createSupport probe so a strict
Start reports unconfirmed instead of refused. It also swallowed
method_not_found, which is a host answering that it has no structured route:
structured-launch-support-refused diverged from its golden. Definitive codes
(isDefinitiveAgentSessionCreateRefusal) are plain unsupported again; other
ok:false replies stay probeFailed, so the two reply-matrix goldens whose
generic refusal partitions now settle probeFailed are re-recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(work-items): pin the closures and capabilities Start changes on 1.4.209

- The structured RPC module imported the native-chat write floor from
  mobile-native-chat-send, dragging terminal-write families into the Tasks
  and Files pages. The constant moves to a leaf module.
- The layout's composer now opens the Start's structured session, so C1
  (and C5, which inlines it) pins agentSession.structured-create/launch with
  their replay verdicts; C2/C3/C5 censuses move by +2 families, +9 goldens.
- The session route closure is 4329 modules (+6 Start modules).
- The desktop renderer advertises the structured capabilities to paired
  remotes too, which the port added so a paired Start is not refused; the
  divergence test records that decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(daemon): reap terminal descendants during shutdown on 1.4.209

Port of upstream 632ae13 ("fix(daemon): reap terminal descendants
during shutdown (stablyai#22232)") onto the v1.4.209 tree. The release's pr.yml
already runs config/scripts/run-daemon-shutdown-descendants-docker.mjs,
but the harness was never shipped and the leak it guards was live: a
setsid'd, SIGTERM-ignoring child of a live PTY shell survived daemon
exit.

Host dispose now captures each live session's descendants, escalates
them with identity-checked SIGTERM/SIGKILL and waits for verification
(bounded to 2.5s inside the daemon's 5s budget) before force-killing the
shell and releasing its PTY. In-flight teardowns are upgraded to
immediate and awaited instead of being disposed a second time.

Carried as-is: the teardown/verification runtime, the per-pid partial
read verifier, the process-table parser split, tests, and the Docker
oracle plus its PR-scope classification. Not carried: the
daemon-cgroup-scope busctl hunk (the module was removed in 1.4.209) and
the pr.yml step (already present in the release).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(work-items): scope Start session reach to its owner on 1.4.209

- hold, reveal, restartResumable/Resume/Continue authorize each concrete
  sessionId: a Work Item Start session is reachable by its owner alone,
  even with global structured chat off; cross-owner access is refused.
- Add the Start-only client capability agent-session.work-item-start.client.v1.
  Paired desktop and web clients advertise it instead of the generic
  structured-session capability, so Start no longer grants reach into
  ordinary structured chats. Hosts still accept the generic capability.
- Ordinary structured tab restore stays behind the v1.4.209 setting; with
  it off, a Start caller restores only its own visible Start tabs, latched
  per record so owners never spend each other's sweep.
- worktree.ps drops sessionId and provider thread ids from structured rows
  the caller cannot reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(work-items): close the remaining 1.4.209 Start review findings

Settings wire: settings.update accepts the optional workItemStartPromptDelivery
('draft' | 'submit-after-ready') that paired web clients already send; legacy
payloads are unchanged and an unknown value is refused.

Prompt refusal: only a strict Work Item Start (submit-after-ready) settles a
definitively refused prompt, and it names one fresh retry id. Every other
structured launch keeps the v1.4.209 requeue.

Desktop strict retry: a recovery whose staged prompt is gone restages it under
the SAME operation id, so the host ledger replays an accepted send instead of
delivering twice. A strict refusal leaves one retry id and keeps Retry enabled.
A refused create retries with its never-dispatched prompt, and a thrown
unadmitted refusal is definitive rather than unknown.

Quick create: the strict preflight checks the host the workspace runs on
(ephemeral VM or run target), not the source repo's, before any worktree exists.

Mobile: a strict Start refuses SSH, runtime-hosted and WSL repos before
worktree.create. A durable attempt journal, written after worktree.create and
before the session request, makes every retry re-enter the same session id and
create operation. The composer returns the created workspace with a warning
instead of an error that left Create armed for a second workspace; the workspace
screen offers Retry Start; the Tasks create path has a synchronous in-flight
guard. Support probes are bounded to 10 s on mobile and desktop.

Build provenance: the hourly, daily and adhoc mac version scripts emit the full
commit sha (display names still slice seven), and provenance refuses any
ORCA_BUILD_COMMIT/ORCA_BUILD_TREE that is not a full 40-hex id.

Certification evidence: the 1.4.203 pin is gone and evidence binds to the
candidate manifest's version, commit, tree and build id, with an optional
ORCA_CANDIDATE_EXPECTED_COMMIT pin. The runtime attestation adds optional
appContent (the app.asar hash) and attestationId, and packaging records the
asar hash per artifact in the manifest. The paired spec runs its contract steps
in PR CI against a new inert Codex app-server fixture; candidate evidence runs
only with ORCA_WORK_ITEM_START_CERTIFICATION=1, where nothing may skip.

Also: the terminal-paste path is fenced from strict Start and pinned by a test,
mobile/pnpm-lock.yaml is restored to its v1.4.209 bytes (a format-only rewrite
with no package.json change), and three parity pins plus six mobile RPC goldens
are refreshed for the intended probe-timeout and create-guard changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(work-items): close the 1.4.209 Start CI findings outside the RPC pin

- runtime-executable-attestation: read the original-fs shape and
  process.resourcesPath through typed property access, not Reflect.get.
- write-candidate-manifest: rpm names x64 `x86_64`, which the bare `x86`
  match read as ia32; the package job refused the manifest for want of an
  ia32 app-content sidecar. Covered by a linux x64 binding test.
- Session route closure 4329 -> 4333: the four Start modules added in
  8ecea75 (attempt journal, prompt delivery, retry banner, execution host).
- Web runtime capability tests assert the Start-scoped structured capability
  is advertised and the generic structured capabilities are not.
- The e2e workflow contract finds its Windows launcher by listing rather than
  naming a batch-shim literal; it only reads the file, never spawns it.
- en.json gains the retryable prompt-refusal copy the recovery path reads.

The RPC recording pin stays red: the 9 structured goldens record the
support probe's new 10s budget, which the pinned baseline tree does not
send. Only a repin plus full re-record satisfies both replays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(rpc): repin recordings to final 1.4.209 Start candidate

* fix(work-items): refuse unsupported strict Start repos before worktree.create

A strict Work Item Start now asks the host for the source repo's
structured-session verdict (agentSession.createSupport with a repo
selector) after admission and before any setup or worktree.create.
A remote/wsl/agent refusal, an older host that rejects the repo params,
or an unanswered probe stops the Start with nothing created. Draft mode
never sends the probe.

The host derives the would-be location from the repo row, the project
runtime's configured WSL distro and a WSL UNC path, so a C:\ repo whose
project runs in WSL is refused before a workspace exists.

Also binds mac zips to the macos app-content sidecar in the candidate
manifest, refusing zips whose platform is ambiguous.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mobile): expect the pre-create repo probe in the Start submit hook

ef2ff81 made a strict Work Item Start ask agentSession.createSupport with a
repo selector before worktree.create, and updated the Tasks-side tests, but
not the new-workspace submit hook test. Its expected call sequence still
omitted the repo probe, failing Mobile Checks deterministically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(work-items): deliver strict quick-create Start as submit-after-ready

Quick-create derived its structured prompt delivery as `auto-submit` whenever
no draft prompt existed, which is always the case for a strict Work Item
Start. The launch carried `launchOrigin: work-item-start` but not
`submit-after-ready`, so the strict send guard never applied and a refused
send was requeued into the legacy chat outbox behind the Start.

The route resolver now owns the delivery decision: a strict Start always
delivers `submit-after-ready`, legacy quick-create keeps `draft` /
`auto-submit`. The worktree-creation launch boundary also pins strict delivery
from `workItemStartPromptDelivery`, so a stale request cannot reach the legacy
requeue. A strict Start with an empty prompt is refused before any workspace
exists instead of completing or reconciling on an empty send. Direct and
folder Start are unchanged; both already sent `submit-after-ready`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(work-items): report managed Claude account ineligibility as agent, not wsl

`agentSession.createSupport` answered a managed Claude account mismatch with
reason `wsl`, even for a native repo, so mobile told the user the repository
runs inside WSL and orchestration downgraded with "this workspace runs under
WSL". The verdict now uses the existing `agent` reason, which every client
already maps, so no schema or wire change is needed. Mobile's `agent` copy,
before and after create, now names the selected agent or its active account.
Real WSL workspace / project-runtime refusals keep reason `wsl`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(work-items): add strict Start empty-prompt localization

* fix(work-items): single-flight strict Start and Retry per workspace

A mobile Start and a Retry Start (or two Retry taps) for the same workspace
could overlap. The attempt and send journals only make a sequential
re-entry safe: the first flow cleared the send record when it settled, the
overlapping one minted a new send operation, and the prompt reached the
session twice.

Start and Retry now share one module-level in-flight promise per workspace.
Every overlapping caller awaits that same promise, so there is one create,
one session and one prompt. The slot is released only by the promise that
holds it, on success or on a throw, so a later Retry after a settled
outcome runs normally. Retry reads the attempt journal inside the flight,
so a stale banner tap after a settled Start finds nothing and sends
nothing. The banner also guards double taps with a ref.

A phone paired with mobile scope is now told that the refusal is about its
pairing scope instead of being blamed on an older host. Pairing authority
is unchanged.

The pre-create route decision moves unchanged to work-item-start-route.ts
(plus its tests) to keep both files under max-lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(work-items): pin strict Start mobile route closure

* chore(work-items): preserve reviewed mobile Start sources

---------

Co-authored-by: salvadorgu7 <206022846+salvadorgu7@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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