Repository navigation
fix(native-chat): settle a structured send on admission, not on the provider echo - #19863
Conversation
…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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughThe change replaces dispatch timeouts with immediate Severity of issue fixed: Medium Merge Risk: 🟡 Moderate · up to 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)
✅ Passed checks (2 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
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 winClear 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 resetserror. The sequence is now a normal path under admission and late settlement:
- The RPC rejects, so
disposeStructuredAgentSessionSendFailuresetserrorto'Message delivery is unconfirmed'and the entry becomesunconfirmed.- The provider echo later settles the submission as
accepted.- This effect removes the entry, so
NativeChatStructuredSession.tsxno longer renders the notice or the Retry button.erroris still set, so the destructive line atNativeChatStructuredSession.tsxlines 307-311 keeps showing'Message delivery is unconfirmed'for a message that was delivered.The stale text persists until the next
sendorretryclears it. The new testuse-structured-agent-session-outbox.test.tsxlines 234-260 covers this exact sequence and does not asserterror.🐛 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 valueConsider an options object for
adapterFor.
adapterFornow takes nine positional parameters, andacquiredmust pass five consecutiveundefinedplaceholders 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 winTighten the assertion so it pins the new
pendingbehavior.With the new disposition, a
pendinganswer moves the entry todispatching, and the probe only re-queues entries in stateunconfirmed. 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 winConsider 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 })plusadvanceTimersByTimeAsync. 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 winAssert submission ordering in the migrated
sendtest.The test covers
acceptedand thrown-dispatchunknownoutcomes, but it only checks the history row aftersendcompletes. Add an assertion that the submission row exists whendispatchruns.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 8c874b81-22f9-43af-8d09-6837f69a3ce5
📒 Files selected for processing (30)
src/main/claude/claude-structured-compaction.tssrc/main/claude/claude-structured-dispatch-admission.test.tssrc/main/claude/claude-structured-dispatch-test-support.tssrc/main/claude/claude-structured-dispatch.test.tssrc/main/claude/claude-structured-dispatch.tssrc/main/claude/claude-structured-session-adapter-turns.test.tssrc/main/claude/claude-structured-session-adapter.test.tssrc/main/claude/claude-structured-session-adapter.tssrc/main/claude/claude-structured-session-close.tssrc/main/claude/claude-structured-session-state.tssrc/main/claude/claude-structured-session-test-support.tssrc/main/claude/claude-tui-resume-real-binary.integration.test.tssrc/main/codex/codex-structured-turn-start.tssrc/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.tssrc/main/native-chat/agent-session-journal/journal-pending-submission-recovery.tssrc/main/native-chat/agent-session-journal/journal-store.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-test-harness.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-send.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.tssrc/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.tssrc/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) |
There was a problem hiding this comment.
🗄️ 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.tsRepository: 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.
There was a problem hiding this comment.
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 —
dispatchClaudeTurnreturnsadmitted; waiters have no deadline and are retired only by child exit or a failed write. - Fail-closed retry allowlist —
journal-dispatch-doubt-reasons.tsgates redelivery onprovider_write_failedonly. - Late-settlement wiring —
settleWaiternow firesonSettledLate, settling the journal submission throughsettleLateDispatch. - Renderer outbox — Retry only targets the outbox head;
disposeStructuredAgentSessionSendResultcentralizes outcome → outbox-state folding.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
…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
There was a problem hiding this comment.
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 winPreserve existing non-delivery reasons.
When this filter selects an existing
unknownentry with aprovider_write_failed:reason,resolveDispatchoverwrites it withhost_restarted_before_acknowledgement.dispatchDoubtProvesUndeliveredrecognizes only the original reason, so a laterretryUnknowncannot redeliver a message the provider never received. Preserveentry.reasonfor existingunknownentries and use the recovery reason only forpendingentries.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 683a7061-672c-42fc-9e58-12d89ad290b2
📒 Files selected for processing (29)
src/main/claude/claude-agent-sdk-user-message-queue.test.tssrc/main/claude/claude-agent-sdk-user-message-queue.tssrc/main/claude/claude-stream-json-connection.tssrc/main/claude/claude-structured-compaction.tssrc/main/claude/claude-structured-dispatch-admission.test.tssrc/main/claude/claude-structured-dispatch.test.tssrc/main/claude/claude-structured-dispatch.tssrc/main/claude/claude-structured-session-adapter-turns.test.tssrc/main/claude/claude-structured-session-state.tssrc/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.tssrc/main/native-chat/agent-session-journal/journal-pending-submission-recovery.tssrc/main/native-chat/agent-session-journal/journal-reducer.test.tssrc/main/native-chat/agent-session-journal/journal-reducer.tssrc/main/native-chat/agent-session-journal/journal-row-builders.tssrc/main/native-chat/agent-session-journal/journal-row-schema.tssrc/main/native-chat/agent-session-journal/journal-store-contracts.tssrc/main/native-chat/agent-session-journal/journal-store.tssrc/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-send.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.tssrc/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-outbox.tssrc/renderer/src/lib/structured-agent-session-launch-prompt.test.tssrc/renderer/src/lib/structured-agent-session-launch-prompt.tssrc/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.
There was a problem hiding this comment.
✅ 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—performSendnow writesresolveDispatch({ state: 'pending' })on an admitted redelivery, so the submission comes backpendingandrefusedRedeliveryno longer drops the entry or surfaces the false refusal banner. - Fail-closed write disposition —
claude-agent-sdk-user-message-queue.tssplits failure intounwritten(queued/refused, provably never handed to the SDK) vswrite-outcome-unknown(in-flight, ambiguous); only the former maps to the redeliverableprovider_write_failed. - Durable echo settlement —
journal-reducer.tsgainsacceptSubmissionFromProviderItem, so apendingsubmission is accepted from the provider's echo item row even when the directsettleLateDispatchwrite fails. - Bounded active waiter queue —
MAX_ACTIVE_DISPATCH_WAITERS = 64rejects dispatch when full, preventing unbounded waiter accumulation. - Compaction carries no
clientMessageId— provider-control turns no longer settle a journal submission viaonSettledLate. - Recovery filter widened —
markPendingSubmissionsUnknownnow also covers liveunknownsubmissions (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
pendingmaps todispatchingin both the send disposition and the launch-prompt path.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ 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 —
performSendnow writesresolveDispatch({ state: 'unknown', reason: DISPATCH_DOUBT_RETRY_IN_PROGRESS })before a redelivery, so a crash mid-retry leaves a non-provable marker instead of a staleprovider_write_failedthat would green-light a third delivery. - Recovery preserves proven reasons —
markJournalPendingSubmissionsUnknownnow keeps a liveunknownentry'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/errorwhen the journal settles the accepted head, so a late transport failure can no longer resurrect the delivery-doubt banner. - Persisted
pendingrestored asdispatching— the shared reconciler maps apendingsubmission todispatching, preventing an already-admitted send from being re-dispatched on remount.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ 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.v1capability, advertised inRUNTIME_CAPABILITIES, the Electron remote/mobile client lists, and the desktop/mobile IPC lists. Capable clients receive the durablependingreply immediately; older clients keep the settled contract. - Compatibility wait —
StructuredAgentSessionSendSettlementplussendStructuredAgentSessionForClientblock an older client'sagentSession.senduntil the submission leavespending, without holding the mutation queue, and reject on transport abort, session close, host close, or an authoritative drop. - Client-delivery refactor —
StructuredAgentSessionClientDeliveryconsolidates the status feed, subscribers, and send settlement behind one owner; the host now delegates all publication edges (includingpublishStatusAndSettlementon the event-recovery path) through it. - Staleness guard —
journalItemRevisionIsStalestops a stale provider item behind its tombstone from settling apendingsubmission, 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— apendingoracceptedhead is treated as host-owned, ignoring a late transport failure while preserving an unrelated tail-save error.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
Superseded review statusThis 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: No merge was performed. |
…atch-admission-settlement # Conflicts: # src/main/ipc/runtime.ts # src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
…atch-admission-settlement
There was a problem hiding this comment.
✅ 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/closeAllresolve observers withundefined(yielding the durablependingreply) instead of surfacing a false "send failed" error for a message that was already admitted. - Proven-undelivered echo guard —
resolveJournalItemIdnow usesdispatchMayMatchProviderEcho, so a newer identical send — never the olderprovider_write_failedsubmission — is what a provider echo settlesaccepted. - Regression coverage — fake-timer timeout test, per-session and cross-session cap tests, the close-resolves test, and a reducer test pinning that
dispatchMayMatchProviderEchoskips the proven-undelivered submission.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
…atch-admission-settlement
…atch-admission-settlement
Correction and final review statusThe 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
The four previously uncovered modulesAll 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
Load-bearing versus defensiveLoad-bearing for correctness:
Compatibility/defensive/resource-safety:
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
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. |
There was a problem hiding this comment.
✅ 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 theisFocusedGroupprop intoNativeChatStructuredSession, plumbed through touseNativeChatComposerRevealFocus, and the test composer mock now attaches a realuseRefDOM node so the reveal-focus loop can verify focus ownership. - Test-fixture split reconciled — main's
test(native-chat): split structured question fixtures(#19924) introducedclaudeGroupedQuestionPromptItems, wired into the grouped-question test. - Independence confirmed — the
outboxHeadretry logic and the focus plumbing share no state and no conflicting resolution; no conflict markers, andisFocusedGroupflows correctly throughNativeChatStructuredViewProps.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
|
Final review status: clean at exact pushed head 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 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
…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>
* 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>

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
pendingstate until Claude’s item event settles itaccepted.pendingresult.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.
Settled after the first turn finished: the UI rendered
SECOND_PENDING_DONE; the journal recorded submission at1789039425059and acceptance at1789039506160, an 81.101-second delay.Testing
ORCA_BACKGROUND_LAUNCH=1and Playwright CDP exercised a real Claude session: a second message stayed neutral past 11 seconds, then settled after 81.101 seconds and renderedSECOND_PENDING_DONE.942c3e3e338163b4fdccee64d5dcceb124659b37: 323 focused tests across 22 files passed;pnpm tc:node, changed-code quality, andgit diff --checkpassed. A separate exact-production-head mobile direct/relay capability run passed 18 tests.AI Disclosure
Review
mainwas merged again before the final pass.942c3e3e338163b4fdccee64d5dcceb124659b37with no remaining in-scope findings.Agent skill upstream boundary
Notes
agent-session.pending-send-result.v1and receive durablependingimmediately. A legacy mobile client still has its own 15-second request budget and may fall back to transcript reconciliation before showing an unconfirmed warning.Checklist