Skip to content

fix(native-chat): resolve a conversation command against its terminal frame - #20658

Closed
brennanb2025 wants to merge 27 commits into
mainfrom
brennanb2025/native-chat-rpc-guard
Closed

brennanb2025 wants to merge 27 commits into
mainfrom
brennanb2025/native-chat-rpc-guard

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Native Chat conversation commands were treated as complete when their request returned. That is the wrong boundary for /compact and /clear: provider work can continue after transport settlement, so a client deadline could report failure while the original command was still live and allow a retry to race it.

The host now owns each command through its terminal provider outcome. It durably admits the operation, releases the serialized session lane before the long provider call, then re-enters under the same session/generation fence to commit the result before publishing it. The renderer observes that lifecycle and can replay the same operation after an unresolved request; it does not create a second provider command.

Implementation

  • A per-session host controller records admission and operation identity, runs provider work outside the mutation lane, and settles through fenced durable state.
  • Claude and Codex adapters correlate command completion to exact provider identities and terminal frames. Cancellation, close, restart, reacquisition, teardown, and late frames retire or quarantine the right generation.
  • /clear uses an explicit replacement-session lifecycle so the old session cannot publish after replacement.
  • The renderer keeps a bounded claim for the active operation, reconciles it from typed lifecycle snapshots, and clears it on terminal settlement, fence change, interrupt, or teardown.
  • Local and remote clients share the same lifecycle behavior. No RPC schema, stream opcode, required field, or generic runtime timeout behavior changed; legacy reply fallback remains available for older peers.

Scope

This PR is limited to structured Native Chat conversation-command ownership, provider correlation, recovery, and the supporting lifecycle tests. It does not change generic local RPC timeout behavior.

Files Added Deleted Net
Test 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1835 $\color{#cf222e}{\Huge{\mathbf{−}}}$​12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1823
Prod 26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1668 $\color{#cf222e}{\Huge{\mathbf{−}}}$​463 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1205

Validation

  • 15 focused test files, 231 tests passed after the latest-main merge
  • pnpm tc
  • pnpm run check:code-quality:changed with zero findings
  • pnpm run verify:localization-runtime-catalog
  • Hidden Electron validation over CDP; final-head evidence will be linked in the review status comment

@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds a claim-based lifecycle for structured conversation commands. Claims wait for terminal journal frames, track unresolved deadlines, and reset with session fences. Session close and cancellation now use control lanes when a command parks the main lane. Local runtime calls now enforce optional deadlines through a shared helper, while structured conversation commands rely on terminal frames and remote calls still receive forwarded timeouts. Tests cover command claims, control lanes, session close, and runtime deadlines.

Priority: ➖ Normal

Merge Risk: 🟠 High · up to 7d17a

This change reworks how /compact and /clear commands are tracked and how session close/cancel interact with them. Two credible defects remain: a delayed but eventually successful command reply can leave the UI permanently believing a command is still pending (requiring a restart or manual interrupt to recover), and a failed cancellation can let the app allow a new command while the previous one is still running on the backend, risking overlapping mutations. A minor, previously flagged stale-error display issue also appears to persist. These should be addressed before merging.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 20 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description explains the implementation, rationale, scope, and validation, but it omits most required template sections. It does not include an ELI5 section, a linked issue, visual proof or an exp… Complete the required template sections. Add an ELI5 summary, explain why this approach was chosen, provide the issue reference under “Fixes #”, add before/after visual proof or state N/A with a reason, complete the Testing and checklist it…
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the primary change: conversation commands now resolve against terminal frames. It is concise and related to the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 20 files. (1 skipped: 1 unsupported.)

Full details: Description check

Explanation

The description explains the implementation, rationale, scope, and validation, but it omits most required template sections. It does not include an ELI5 section, a linked issue, visual proof or an explicit N/A, the required testing checklist, AI disclosure, review section, agent skill boundary section, notes, or the final checklist.

Resolution

Complete the required template sections. Add an ELI5 summary, explain why this approach was chosen, provide the issue reference under “Fixes #”, add before/after visual proof or state N/A with a reason, complete the Testing and checklist items, provide AI disclosure if applicable, complete Review and Agent skill upstream boundary, and document security, cross-platform, SSH, mobile, compatibility, and performance considerations.

  • Fix all pre-merge checks with AI
✨ 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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7317bc29-6cb5-48ad-ad1e-f7384b6f3747

📥 Commits

Reviewing files that changed from the base of the PR and between c6a7216 and 9b5bba0.

📒 Files selected for processing (8)
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/renderer/src/runtime/local-runtime-call-deadline.ts
  • src/renderer/src/runtime/runtime-rpc-client-local-deadline.test.ts
  • src/renderer/src/runtime/runtime-rpc-client.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.

Important

The local-deadline fix is sound and correctly scoped, but the new stall watchdog reports into a channel that production never supplies, so the observability half of this PR has no runtime effect. Details inline.

Reviewed changes

  • Local RPC deadline applied on desktop. New callLocalRuntimeWithDeadline races the local window.api.runtime.call against a timer only when the caller declared a timeoutMs, with no default; runtime-rpc-client routes its local branch through it. This is the real fix — runtime:call in src/main/ipc/runtime.ts never accepted timeoutMs, so the declared 195s conversationCommand guard never reached main.
  • agentSession.send deliberately stays unbounded. No deadline is passed for send, preserving the delivery-unknown classification. Verified: every other callRuntimeRpc(..., { timeoutMs }) site is gated on target.kind === 'environment', so conversationCommand is genuinely the only local-reachable caller that declares one.
  • Per-session task-queue stall watchdog. StructuredAgentSessionTaskQueue wraps each serialized task in a watch that reports once past a 240s threshold (derived from the 180s compaction bound) without cancelling or reordering, exposed as stalledTasks() and wired through onSessionTaskStalled → deps.onError.

ℹ️ Nitpicks

  • The test titled reports a rejected parked task only for the session that stalled exercises a never-settling task, not a rejected one; the schema and ordering assertions are fine, only the name misleads.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Made the stall report reach a production sink: onSessionTaskStalled now falls back to console.error when deps.onError is absent, so a parked session is observable on desktop and orcad instead of disappearing into an unset optional dep. This closes the one open finding from the prior review.
  • Hardened the queue's reporter: StructuredAgentSessionTaskQueue.report wraps onStalled in try/catch, so a throwing reporter cannot escape the timer callback or perturb the parked chain.
  • Added coverage for both: a queue test asserting a throwing reporter is contained without releasing the chain, and a runtime test asserting the no-reporter fallback logs the scoped error. Both fail with the production lever reverted.

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

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Visual proof / Electron smoke (PR #20658)

Hidden-renderer CDP validation of the desktop app. This change is timeout and stall-diagnostic plumbing: screenshots prove the app at this branch renders, not that a deadline or queue watchdog fired.

SHAs

Role SHA
Head when this validation began 5ef096ef6c7e08becd7e36e65a707759d471a7ca (fix(runtime): keep session stall reporting observable)
PR / worktree head at comment time cf94910a19916bc186fc93b73a59e521c966697b (refactor(runtime): extract session stall reporting)

The extra commit landed during the run. It is a main-process extract of stall reporting (structured-agent-session-stall-reporter.ts) with no renderer/UI change. Electron was launched against the begin SHA; targeted tests were re-run at the later head.

Commands

git rev-parse HEAD
# begin: 5ef096ef6c7e08becd7e36e65a707759d471a7ca

ORCA_BACKGROUND_LAUNCH=1 pnpm test \
  src/renderer/src/runtime/runtime-rpc-client-local-deadline.test.ts \
  src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.test.ts \
  src/main/runtime/structured-agent-session-runtime.test.ts
# 3 files, 31 tests, all passed (also re-run at cf94910a19: 31 passed)

# Isolated throwaway profile + folder workspace only (not the user profile).
ORCA_BACKGROUND_LAUNCH=1 REMOTE_DEBUGGING_PORT=9333 \
  ORCA_DEV_USER_DATA_PATH=/Users/brennanbenson/.orca-qa/pr20658-profile \
  node config/scripts/run-electron-vite-dev.mjs

# CDP identity before any interaction:
# window.api.app.getIdentity()
#   name: Orca: brennanb2025/native-chat-rpc-guard
#   isDev: true
#   devBranch: brennanb2025/native-chat-rpc-guard
#   devWorktreeName: native-chat-rpc-guard
#   devRepoRoot: /Users/brennanbenson/orca/workspaces/orca/native-chat-rpc-guard
# CDP target: page "Orca" http://localhost:5173/

Launch used ORCA_BACKGROUND_LAUNCH=1 (window stays off-screen). Interaction was Playwright CDP only. No computer-use / OS UI automation. No product files edited or committed. pnpm test lockfile drift was reverted and not committed.

Screenshots

Empty isolated app (this branch renderer):

01-empty-app.png

Throwaway folder workspace with a live terminal pane (folder workspace, not a git worktree):

02-workspace-terminal.png

New-tab menu showing Claude/Codex/Grok structured-chat launch entries (not launched):

03-new-tab-agent-menu.png

Experimental settings: Chat UI on, default view Chat UI, Use updated structured native chat on:

04-structured-chat-settings.png

What was visually verified

  • CDP attached to this worktree’s dev instance, not another Orca.
  • Hidden renderer painted the real app chrome at this branch.
  • Isolated folder workspace opened; Terminal 1 ran in ~/.orca-qa/pr20658-workspace.
  • Structured-chat entry path is present: New Tab lists Claude/Codex, and Experimental settings show Chat UI + structured native chat enabled.
  • No user profile data was used. Settings/repo writes stayed in the throwaway profile/folder.

What a screenshot cannot prove (gaps)

  • No live NativeChatStructuredSession transcript. Opening a real Codex/Claude structured session would start an agent against home credentials. That was skipped as unsafe for a no-mutation smoke.
  • The 195s conversationCommand local deadline was not observed in the running app. It is an explicit timeoutMs race in callLocalRuntimeWithDeadline; elapsed time is not evidence of non-delivery, and there is no default local deadline. Proven by unit tests, not pixels.
  • The 240s per-session queue stall watchdog was not observed. It reports once per parked task and never cancels, releases, or reorders host work. Proven by unit tests (structured-agent-session-task-queue.test.ts), not pixels.
  • Switching a shell tab’s viewMode to chat did not mount native-chat chrome (expected: this tab is not an agent session).

Tests (vitest, not e2e)

At both 5ef096ef6c and cf94910a19:

  • runtime-rpc-client-local-deadline.test.ts — 5 tests
  • structured-agent-session-task-queue.test.ts — 9 tests
  • structured-agent-session-runtime.test.ts — 17 tests

31 passed, 0 failed. These are the guards; this Electron pass is rendered-app smoke only.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Visual proof (anchored to final head)

Hidden-renderer CDP recapture after coordinator SHA cf94910a19916bc186fc93b73a59e521c966697b. This is timeout/stall-diagnostic plumbing: screenshots are rendered-app smoke only. They do not prove the 195s conversationCommand deadline or the 240s per-session stall watchdog.

Exact SHA

cf94910a19916bc186fc93b73a59e521c966697b (refactor(runtime): extract session stall reporting)

Worktree was clean and matched origin/brennanb2025/native-chat-rpc-guard at recapture. Electron was stopped and relaunched against this head with ORCA_BACKGROUND_LAUNCH=1.

CDP identity (verified before interaction)

{
  "name": "Orca: brennanb2025/native-chat-rpc-guard",
  "isDev": true,
  "devLabel": "native-chat-rpc-guard",
  "devBranch": "brennanb2025/native-chat-rpc-guard",
  "devWorktreeName": "native-chat-rpc-guard",
  "devRepoRoot": "/Users/brennanbenson/orca/workspaces/orca/native-chat-rpc-guard"
}

CDP target: page Orca at http://localhost:5173/ on port 9333. Isolated throwaway profile only (~/.orca-qa/pr20658-profile); no user-profile writes.

Commands

git rev-parse HEAD
# cf94910a19916bc186fc93b73a59e521c966697b

ORCA_BACKGROUND_LAUNCH=1 pnpm test \
  src/renderer/src/runtime/runtime-rpc-client-local-deadline.test.ts \
  src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.test.ts \
  src/main/runtime/structured-agent-session-runtime.test.ts
# 3 files, 31 tests, all passed at this SHA

ORCA_BACKGROUND_LAUNCH=1 REMOTE_DEBUGGING_PORT=9333 \
  ORCA_DEV_USER_DATA_PATH=/Users/brennanbenson/.orca-qa/pr20658-profile \
  node config/scripts/run-electron-vite-dev.mjs

Playwright CDP only. No computer-use / OS UI automation. No agent session launched. No product files edited or committed.

Screenshots (this head)

Throwaway folder workspace with Terminal 1:

final-01-workspace.png

New-tab menu with Claude/Codex structured-chat launch entries (not launched):

final-02-new-tab-agents.png

Experimental settings: Chat UI on, default view Chat UI, Use updated structured native chat on:

final-03-structured-chat-settings.png

Visually verified

  • This worktree’s hidden renderer is the live app at the final head.
  • Isolated folder workspace (not a git worktree) opens with a terminal.
  • Structured-chat entry path is present: New Tab lists Claude/Codex; Experimental settings show structured native chat enabled.

Not proven visually (gaps)

  • No live NativeChatStructuredSession transcript — launching Claude/Codex would start an agent against home credentials; skipped for no-mutation smoke.
  • Local RPC deadline applies only when timeoutMs is explicit; there is no default local deadline; elapsed time is not delivery evidence.
  • Per-session queue reports a stall and never cancels, releases, or reorders host work. Watchdog behavior is unit-tested, not screenshot-tested.

Tests at cf94910a19

31 passed / 0 failed across the three targeted vitest files. Those tests are the timeout/watchdog guards; this Electron pass is UI smoke.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: clean at cf94910a19916bc186fc93b73a59e521c966697b

The review/fix loops found and pushed two meaningful reliability fixes beyond the original change:

  • Stall reports now have a production fallback sink; the desktop runtime did not provide the optional reporter, so the watchdog was otherwise silent.
  • A throwing diagnostic observer is contained at the timer boundary and cannot crash the timer turn, release the lane, cancel host work, or reorder later mutations.
  • Stall reporting was extracted into a named domain module so the current-main merge stays below the max-lines gate; no lint disable or threshold bump was added.

Architecture remains intentionally constrained:

  • Local RPC deadlines apply only when a caller explicitly supplies timeoutMs.
  • agentSession.send remains unbounded; elapsed time is not treated as delivery evidence.
  • The per-session queue reports once and remains ordered; it never cancels or releases the running task.
  • Cost is one unref'd one-shot timer/map entry per actively running session task, cleared on settlement; queued followers add no timers and there is no polling or fanout.

Validation:

  • Focused Vitest: 3 files, 31/31 passed at the final head.
  • pnpm run check:code-quality:changed: clean across all 10 changed files.
  • Branch pnpm tc: exit 0.
  • Current-main merge-ref typecheck and static analysis each report only the pre-existing unused NativeChatMessage import in NativeChatMessageList.windowing.test.tsx; this PR does not touch that file.
  • Package, Windows package, cross-version wire compatibility, and 7/8 test shards pass. The remaining shard contains only the three known terminal-handle-links.test.ts failures; this PR touches no terminal-pane files.
  • Final-head Electron/CDP proof and screenshots: fix(native-chat): resolve a conversation command against its terminal frame #20658 (comment). It verifies rendered desktop/folder-workspace/structured-chat entry smoke only; timeout/watchdog semantics are proven by Vitest, not pixels.
  • Current-main synthetic merge is conflict-free.

Final ablation was rerun immediately before this comment at cf94910a19916bc186fc93b73a59e521c966697b. Removing only the three production levers caused 8 targeted failures while 23 controls passed, including the no-default-deadline and remote-forwarding cases. The worktree was then restored and verified clean at the same SHA.

No remaining in-scope code issues.

… frame

A conversation command runs on the host long after the request that started
it, so the reply is a receipt rather than a completion. The client raced the
transport promise against a 195s timer, which reported an outcome nobody had
established and left no residue: the next attempt silently raced a live first
one.

The client now holds one claim per session and resolves it against the frame
the host revises when the command finishes, carried on the session's own
journal stream; the reply is one way that frame can be learned, not the answer.
When neither arrives inside the deadline the claim becomes a marker that
refuses the next attempt and says the command may still be running, and the
marker retires on a late frame, a session restart, or an interrupt.

Also:

- callRuntimeRpc dropped a declared timeoutMs on the desktop branch while
  forwarding it on the remote one, so every local caller that asked for a
  deadline silently got none. The local path now honours it, and stays a
  synchronous pass-through when no deadline is declared: an extra async frame
  inserts a microtask that reorders calls the caller sequences.

- Cancel already escaped to its own lane while a compaction held the session's
  mutation lane. close() did not, so closing a session queued behind the very
  wait that closing ends. Both now take one named control lane, chosen by
  re-deriving from the durable record whether a command still parks the main
  lane.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/native-chat-rpc-guard branch from cf94910 to ebf408c Compare September 16, 2026 19:08
@brennanb2025 brennanb2025 changed the title fix(runtime): apply the declared RPC deadline on the desktop path fix(native-chat): resolve a conversation command against its terminal frame Sep 16, 2026

@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

The terminal-frame resolution has two gaps: the host's own "unconfirmed" compaction frame is read as success, and an orphaned /clear has no recovery. Details inline.

Reviewed changes

  • Re-keyed user controls onto a control lane. New structured-agent-session-control-lane.ts re-derives from the durable record whether a prepared/unknown conversation command parks the session's main lane, and close() plus cancelStructuredAgentSessionTurn now serialize on session-control:<id> while it does.
  • Added a client-side conversation-command claim. StructuredConversationCommandClaim holds one command per session, settles compact on the host's terminal journal frame and clear on its committed reply, arms a refusal marker past the 195s deadline, and retires on a late frame, a fence change, or an interrupt.
  • Rewired the renderer mutation seam. runConversationCommand now mints its own operation id and feeds the claim the journal stream; useStructuredAgentSessionMutate takes { operationId, onUnresolved } so a host refusal is distinguishable from a missing reply.
  • Dropped the transport deadline for structured-session commands and deleted the previous revision's stall watchdog and structured-conversation-command-send.ts.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

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

201-201: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Assert that attach proceeds before resolving the parked command.

runStructuredConversationCommand() holds the main sessionId queue while it awaits compact(). The current test resolves compact() through finish({}) after close() and does not attempt attach() before that resolution. It would not detect a regression that leaves the main queue blocked. Start attach() before calling finish({}), then assert that it completes.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: d02c5d9c-1013-4f6e-8ce1-c18ceed21b59

📥 Commits

Reviewing files that changed from the base of the PR and between cf94910 and ebf408c.

📒 Files selected for processing (15)
  • src/main/native-chat/agent-session-wire/structured-agent-session-control-lane.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-control-lane.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-claim.test.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-claim.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-send.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/renderer/src/components/native-chat/use-structured-conversation-command.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/runtime/local-runtime-call-deadline.ts
  • src/renderer/src/runtime/runtime-rpc-client-local-deadline.test.ts
  • src/renderer/src/runtime/structured-agent-session-client.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/native-chat/structured-conversation-command-send.ts

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

const message = error instanceof Error ? error.message : 'Request was not sent'
if (stateRef.current.fence === targetFence) {
setWriteError(error instanceof Error ? error.message : 'Request was not sent')
setWriteError(message)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,110p' src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts
rg -n "setWriteError|writeError" src/renderer/src/components/native-chat -A3 -B3
sed -n '1,75p' src/renderer/src/components/native-chat/use-structured-conversation-command.ts

Repository: stablyai/orca

Length of output: 12586


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- claim implementation ---'
sed -n '1,280p' src/renderer/src/components/native-chat/structured-conversation-command-claim.ts
printf '%s\n' '--- session command and mutation call sites ---'
rg -n "useStructuredConversationCommand|mutate<|mutate\\(|applyStreamSnapshot|writeError|setWriteError" src/renderer/src/components/native-chat -g '*.ts' -g '*.tsx' -A5 -B5

Repository: stablyai/orca

Length of output: 32151


Do not persist an error when onUnresolved owns the result.

The transport-error path sets writeError before calling onUnresolved. A later terminal frame settles StructuredConversationCommandClaim through applyStreamSnapshot; it does not call setWriteError(null). Only a later successful mutate call clears the error. The terminal-frame path can therefore leave a stale error visible.

Set writeError only when options?.onUnresolved is absent, or clear it when the claim settles.

Proposed fix
-        if (stateRef.current.fence === targetFence) {
+        if (stateRef.current.fence === targetFence && !options?.onUnresolved) {
           setWriteError(message)
         }

…hat-rpc-guard

# Conflicts:
#	src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts
#	src/renderer/src/components/native-chat/use-structured-agent-session.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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: c9a218f5-5b30-4235-8623-baf7b3c7124a

📥 Commits

Reviewing files that changed from the base of the PR and between ebf408c and 7d17a91.

📒 Files selected for processing (16)
  • src/main/native-chat/agent-session-wire/structured-agent-session-control-lane.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-control-lane.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-claim.test.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-claim.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-send.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/renderer/src/components/native-chat/use-structured-conversation-command.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/runtime/local-runtime-call-deadline.ts
  • src/renderer/src/runtime/runtime-rpc-client-local-deadline.test.ts
  • src/renderer/src/runtime/runtime-rpc-client.ts
  • src/renderer/src/runtime/structured-agent-session-client.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/native-chat/structured-conversation-command-send.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/i18n/locales/en.json

Included review availability: Your plan provides up to 10 included reviews per hour; 7 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 critical issues — both prior findings are addressed; one minor suggestion inline and one open question below.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (ebf408c): the current head rebuilds conversation-command ownership on top of the main merge.

  • Reconciled live command ownership with the durable record. requestControl reports whether a pending command actually holds the main lane; only a compact parks it, and a compact admitted before its provider call is now cancelled by close rather than parking close.
  • Gave /clear a recovery path. The hook retains a clear's operation id across the deadline and a fence change, and the host adopts a prepared clear left under a previous fence (interruptedClear, rerunWhenReplayMissing, settleSupersededOperation).
  • Made the claim read the terminal frame, not a timestamp. The host's unconfirmed item is excluded from terminalFrameOutcome (by text and now by a running lifecycle), and provider failures are carried through instead of reading as success.
  • Threaded refusal-vs-missing-reply through the mutation seam with clearWriteError/onReconciled, scoped to the reconciled operation id.

ℹ️ close() now parks behind an in-flight clear

requestControl returns false for any pending clear, so close() runs on the session's main lane and waits for the clear's replacement attach to complete. That ordering is deliberate — the replacement must commit before the source is retired, and it is covered by the new finishes clear before a concurrent close test — but it is the same shape of wait this PR removed for compaction: closing can no longer escape the work it is closing. Raising it only because the invariant stated in structured-agent-session-control-lane.ts is that closing must not queue behind the operation it stops, and for clear the thing it waits on is a provider acquire.

Technical details
# Close blocks on a clear's replacement attach

## Affected sites
- `src/main/native-chat/agent-session-wire/structured-conversation-command-controller.ts:28-44` — `requestControl` returns `false` for a pending `clear` regardless of `providerCallStarted`, and arms no cancellation.
- `src/main/native-chat/agent-session-wire/structured-agent-session-host.ts:194-209` — `close()` therefore serializes on `sessionId`.
- `src/main/native-chat/agent-session-wire/structured-agent-session-acquisition.ts` — no deadline found on the acquire path.

## Open questions for the human
- Is the replacement attach bounded? If it is, close is bounded by it and this is fine; if it can hang, close is unbounded on this path.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the two follow-up commits. Both are targeted, tested fixes; posting this as non-approving only because the earlier inline note on the refusal message is still open in its thread.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (000aa7d).

  • Preserved the newest mutation's error. useStructuredAgentSessionMutate stamps each request with an increasing sequence and only lets the newest settled mutation set or clear writeError, so a late reply from an older command can no longer wipe a newer mutation's error.
  • Admitted a cancel before it may interrupt a command. cancelStructuredAgentSessionTurn picks its lane from the new read-only mainLaneParked (so a stale or conflicting cancel no longer takes the control lane) and signals the pending command only inside the admitted plan; the renderer retires a claim only when the host confirms cancelled, so a failed cancel leaves the command's claim intact.
  • Retired an expired obligation on its late reply. The claim's applyReply hands a late definitive reply to onLateReply, which drops the retained operation id so the next attempt gets a fresh identity.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues — a small host/client compatibility alignment.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (9f3cd91).

  • Stopped the unconfirmed compaction row from reading as an active turn. The host now writes { kind: 'status', text: 'Compaction completion is unconfirmed.' } with no turnLifecycle, so activeStructuredAgentSessionTurnId no longer treats the row as a live turn; the claim still excludes the row by its 'Compaction completion is unconfirmed.' text, and the tests now assert the lifecycle is absent.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new abandon/claim design has two gaps: a host refusal now permanently retains the operation id, and abandon can orphan a clear replacement session.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (81ad1c2): 59aaeae replaces the control-lane approach with an execution module that releases the session lane during provider work.

  • Removed the control lane and released the session lane during commands. StructuredConversationCommandExecution runs the provider call outside the session lane, and close()/cancel now call abandon() on the lane instead of taking a separate session-control:<id> lane.
  • Split durable admission from execution. admitAgentSessionMutationRequest admits and returns a run decision; prepareStructuredConversationCommand persists the prepared record before any provider work starts.
  • Gave both commands a typed lifecycle frame. publishConversationCommandLifecycle writes running/completed/interrupted/unverifiable status items under <command>:<opId>, and the claim reads those instead of a wall-clock deadline.
  • Dropped the transport deadline for conversation commands and deleted structured-conversation-command-send.ts and the control-lane module.

ℹ️ The PR description no longer matches the implementation

The body still describes the ebf408c revision: it names structured-agent-session-control-lane.ts (deleted), a 195s client claim deadline (removed), and an onUnresolved distinction between a refusal and a missing reply that the code does not actually make (see the inline finding). It also states a clear has no stream frame to wait for, but the host now writes a lifecycle frame for clear too. Worth rewriting so the next reader is not led to the old design.

Technical details
# Stale PR description

## Affected sites
- PR body — "Change" and "Deliberately not done" sections reference the removed control lane, the removed 195s deadline, and the `clear`-has-no-frame claim.

## Required outcome
- The description should describe the current abandon/lifecycle design.

ℹ️ Nitpicks

  • components.native-chat.conversationCommand.mayStillBeRunning (src/renderer/src/i18n/locales/en.json:17384) has no remaining reference — the claim's unresolvedMessage was removed in this commit.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — the follow-up commit strengthens the abandon/interrupt escape paths. Two threads from the prior review remain open and still apply, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (59aaeae): 12e2a7a preserves the conversation-command escape paths once a command is abandoned or interrupted.

  • Broadened the host's command guard to every lane mutation except cancel. mutate() now refuses send, setOption, and respondToPrompt while the durable record holds a prepared/unknown conversation command, and cancel only abandons the command when the provider confirms cancelled.
  • Gave the compaction latch a confirmed-interrupt path. StructuredSessionCompaction.interrupted() deletes the pending attempt and rejects it; the Claude and Codex cancel paths call it on a confirmed cancel, and a provider failure that may still settle late now publishes a running lifecycle instead of retiring the claim.
  • Settled the client claim on a retired host lifecycle. An unverifiable (or legacy unconfirmed-text) frame now finishes the claim with restart guidance, and the hook keeps a clear's operation id across a fence change while resetting on a session switch.

ℹ️ The cancel/interrupt settle path is only covered through mocks

Two new mechanisms now meet in one race — a cancel that abandons the command on the session lane, and a provider compaction rejection that tries to publish a running lifecycle — and no test exercises them together. The compaction unit test drives interrupted() directly and the host test mocks adapter.cancelTurn/adapter.compact, so the ordering argument that abandon always wins against the queued markUnknown is reasoned rather than pinned.

Technical details
# Cancel/interrupt integration is untested

## Affected sites
- `src/main/native-chat/agent-session-wire/structured-session-compaction.test.ts:82-95` — drives `interrupted()` against a real tracker, but with no command and no lane.
- `src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts` — mocks `adapter.cancelTurn` and `adapter.compact`, so the real `StructuredSessionCompaction` never participates.
- `src/main/native-chat/agent-session-wire/structured-conversation-command-execution.ts:211-259` — `markUnknown`/`complete` are the other half of the race.

## Required outcome
- One case that runs a `compact` through a real pending `StructuredSessionCompaction` while a cancel abandons the command, asserting exactly one durable `conversationCommand` record, exactly one terminal lifecycle, and a command promise that resolves once.

## Open questions for the human
- Is the lane-ordering guarantee documented anywhere, or should it be a comment on `markUnknown`'s `ownsExecution` guard?

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — the two prior pullfrog threads (host refusal permanently retaining the operation id, and an abandoned clear orphaning its replacement) remain open and were not touched by 707d21e, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (12e2a7a): 707d21e bounds conversation-command recovery.

  • Re-added a client-side completion deadline. StructuredConversationCommandClaim arms a 195s timer; on expiry the live claim becomes an unresolved obligation that refuses the next attempt with restart guidance and keeps the operation id, retiring on a matching terminal frame, a late definitive reply, or a session/fence reset.
  • Settled out-of-lane commands during whole-host teardown. A new abandon-conversation-commands teardown phase runs abandonAll over every pending session on its lane, so a command still running when the host tears down resolves instead of hanging.
  • Bounded a missing provider request receipt. StructuredSessionCompaction.run now races the completion window against the invocation, so its 180s timeout applies even when the compact() call itself never returns, while a late provider notification can still settle it.
  • Discarded a late mutation result from a previous request scope. useStructuredAgentSessionMutate now guards every late settle on a requestScope (target + session) in addition to the fence, and resets the operation ids, error owner, and newest-sequence watermark when the scope changes.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

8316448 makes recovery replay-based, but two paths discard the retained operation id exactly where the UI tells the user to retry, so the promised replay becomes a fresh id the host refuses. Details inline.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (707d21e): 8316448 makes conversation-command recovery replayable.

  • Released interaction at the deadline and kept the operation id as a bounded observer. StructuredConversationCommandClaim now hands the id back to the hook via retrySameOperation so a retry replays the same durable operation, instead of holding a refusing marker with restart guidance.
  • Typed the mutation reply. mutateWithDisposition distinguishes completed/refused/unresolved; a definitive refusal settles without retrySameOperation and releases the id, closing the prior refusal-retains-id finding.
  • Gated the abandoned clear's replacement close on canSettle, closing the prior orphaned-replacement finding.
  • Reworded the recovery copy to "Retry the command to check its status."

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

Comment on lines +118 to +124
.then(({ retrySameOperation, ...outcome }) => {
if (!retrySameOperation && operationIds.current.get(command) === operationId) {
operationIds.current.delete(command)
onReconciled(operationId)
}
return outcome
})

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.

A retry after finishUnconfirmed/expire usually arrives while blocked is true: the host's own markUnknown(retire=false) publishes a turnLifecycle: { state: 'running' } item for the unconfirmed compact, so activeStructuredAgentSessionTurnId reports compact:<opId> and blocked is set. StructuredConversationCommandClaim.run then returns the pendingWork outcome before calling send and without retrySameOperation, so this .then deletes the retained id. The next retry mints a fresh id, which the host refuses (conversationCommandBlocked → "The previous conversation operation is unconfirmed.") — even though the user was just told to "Retry the command to check its status."

Technical details
# A blocked retry discards the retained operation id

## Affected sites
- `src/renderer/src/components/native-chat/use-structured-conversation-command.ts:118-124` — deletes the retained id whenever the outcome lacks `retrySameOperation`.
- `src/renderer/src/components/native-chat/structured-conversation-command-claim.ts:112-122` — the `blocked` early return never calls `send` and never sets `retrySameOperation`.
- `src/renderer/src/components/native-chat/use-structured-agent-session.ts:78-83` — `blocked` includes `transportState.turnId`.
- `src/main/native-chat/agent-session-wire/structured-conversation-command-lifecycle.ts:43-55` — every lifecycle frame carries `turnLifecycle`, so a compact that may settle late reads as the active turn.
- `src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts:93-126` — a same-id replay is resolved before `conversationCommandBlocked` runs, so the host would accept the replay even while its own turn is "running".

## Required outcome
- An unresolved command must keep its operation id across a `blocked` attempt so the eventual retry replays the same durable operation, instead of minting a fresh id the host refuses.

## Suggested approach (optional)
- In `run`, prefer the retained id over the `blocked` gate (attempt the replay and let the host answer), or have the claim report `retrySameOperation` when it short-circuits without sending while an operation is still outstanding.
- The hook harness hardcodes `blocked: false`; add a case that expires a command and then retries while `blocked`.

Comment on lines +140 to +147
applyStreamSnapshot(items: readonly AgentJournalRenderItem[]): string[] {
const settledOperationIds: string[] = []
if (this.live) {
const outcome = terminalFrameOutcome(items, this.live)
if (outcome) {
settledOperationIds.push(this.live.operationId)
this.finish(this.live, outcome)
}

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.

An unverifiable frame settles the command without retrySameOperation, so the hook's stream effect deletes the id — yet the copy tells the user to "Retry the command to check its status." A fresh id is then refused by the host ("The previous conversation operation is unconfirmed."), while the retained id would at least replay the durable unknown outcome.

Technical details
# An unverifiable terminal frame retires the replay id

## Affected sites
- `src/renderer/src/components/native-chat/structured-conversation-command-claim.ts:65-70` — `terminalFrameOutcome` returns a flag-less outcome for `unverifiable`.
- `src/renderer/src/components/native-chat/structured-conversation-command-claim.ts:140-147` — `applyStreamSnapshot` calls `finish` with that outcome; `finish` (224-231) cannot set `retrySameOperation`.
- `src/renderer/src/components/native-chat/use-structured-conversation-command.ts:42-50` — the stream effect deletes every operation id the claim reports settled, and the observer branch drops it the same way.
- `src/main/native-chat/agent-session-wire/structured-conversation-command-admission.ts:20-25` — a fresh command is refused while the durable record is `prepared`/`unknown`.

## Required outcome
- Retain the id on an `unverifiable` terminal frame (settle like `finishUnconfirmed`), or reword the copy so it does not promise a retry the host will refuse.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. ce54738, 67e5164, and 799faa1 harden command framing and clean up the compaction generation rather than change the recovery contract; the two prior pullfrog threads (a blocked retry discarding the retained id, and an unverifiable frame retiring it) are untouched, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (8316448): the three follow-up commits fence stale command state.

  • Fenced stale command replies by generation. StructuredConversationCommandClaim stamps each live claim with a generation, bumps it in reset(), and drops any reply from an earlier generation, so a pre-reset reply can no longer settle a same-operation replay.
  • Keyed the controller's coalescing on the whole request. run's pending key now includes expectedRuntimeFence and payloadFingerprint, and send no longer short-circuits on a pending command, so a legitimate send replay is served from the durable ledger while a mismatched duplicate is refused.
  • Tightened the clear replacement's authority. executeClear commits only when the replacement lease is live and this host actually holds the session; a bare reservation now surfaces as unverifiable instead of a false commit.
  • Quarantined an interrupted compaction generation. StructuredSessionCompaction retains the interrupted generation until the provider's terminal frame, refuses a new compaction with The interrupted compaction is still settling., and ownsTurn no longer reports the interrupted generation as owning its turn.

ℹ️ A quarantined compaction generation has no deadline

StructuredSessionCompaction.run keeps an interrupted generation in pending until the provider's terminal frame, but the 180s timer cannot remove it (its callback only rejects a promise that already rejected) and run's finally clears it anyway, so the quarantine has no time bound. That is fine while the provider eventually emits result/turn/completed (or ended fires), but a provider that confirms the cancel and then goes silent leaves the session unable to compact again — every retry returns The interrupted compaction is still settling. — while hasPending keeps suppressing compaction content from the translator.

Technical details
# Quarantined compaction generation has no expiry

## Affected sites
- `src/main/native-chat/agent-session-wire/structured-session-compaction.ts:62-70` — `interrupt()` rejects the waiter but leaves the generation in `pending`.
- `src/main/native-chat/agent-session-wire/structured-session-compaction.ts:73-101` — the timeout only rejects; the `finally` clears it, and `finish` (the terminal-frame path) is the sole removal.
- `src/main/claude/claude-structured-compaction.ts:46-54` — `hasPending` suppresses compaction content while a generation is retained.

## Open questions for the human
- Once the provider confirms the cancel, is a terminal frame (or `ended`) guaranteed? If not, should the quarantined generation carry its own expiry, or be cleared on re-attach, so a silent provider cannot wedge compaction permanently?

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

@pullfrog

pullfrog Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — 846fcb7 makes transport-level doubt non-terminal so the claim always defers to the host's lifecycle. One informational note inline. The two prior threads (a blocked retry discarding the retained id, and an unverifiable frame retiring it) are untouched by this commit, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (799faa1): 846fcb7 makes a conversation command await the remote lifecycle rather than its transport reply.

  • Stopped settling a conversation command on transport doubt. StructuredConversationCommandClaim no longer treats a rejected send or an unresolved reply as a terminal outcome, so completion is decided only by the host's terminal journal frame or the 195s deadline.
  • Kept the host-confirmed unknown reply on its old path. A reply whose result.state is unknown still routes through finishUnconfirmed, retaining the operation id for same-operation replay.
  • Reconciled the transport error on the unresolved path. useStructuredConversationCommand now clears the write error as soon as a disposition is unresolved, leaving the claim's own outcome (or the lifecycle frame) to own the messaging.
  • Updated the guards for the new contract: the claim test now asserts it keeps waiting when the transport returns no reply, and a new case pins same-operation replay for a host-confirmed unknown.

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

// Transport doubt cannot settle host-owned work; the session lifecycle or claim deadline does.
void input.send().then(
(reply) => this.applyReply(claim, reply),
() => undefined

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.

run no longer lets a rejected send settle the claim, and applyReply no longer settles on an unresolved reply, so a command whose request failed at the transport layer now stays live until the host's terminal frame or the 195s deadline. For a request that never reached the host there is no frame to wait for, and use-structured-conversation-command.ts:98-101 clears the transport error while use-structured-agent-session.ts:104 gates sends on isRunning(), so the user sees neither the error nor a usable composer until the deadline. Confirm this is the intended trade-off for a fast local transport failure (the first hook test now asserts exactly this state).

Technical details
# A transport failure with no lifecycle frame holds the composer for the full deadline

## Affected sites
- `src/renderer/src/components/native-chat/structured-conversation-command-claim.ts:137-139` — a rejected `send` is swallowed; nothing settles the claim early.
- `src/renderer/src/components/native-chat/structured-conversation-command-claim.ts:201-203` — an `unresolved` reply no longer calls `finishUnconfirmed`.
- `src/renderer/src/components/native-chat/use-structured-conversation-command.ts:98-101` — the `unresolved` disposition calls `onReconciled`, clearing the write error `mutateWithDisposition` just set.
- `src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts:118-129` — a thrown call maps to `{ status: 'unresolved' }` and sets `writeError`.
- `src/renderer/src/components/native-chat/use-structured-agent-session.ts:104` — `send` is gated on `!conversationCommand.isRunning()`.

## Required outcome
- Decide whether a transport-level failure with no durable host record should surface immediately (as before `846fcb7`) or hold interaction for `CONVERSATION_COMMAND_DEADLINE_MS` (195s).

## Open questions for the human
- For a local `window.api.runtime.call` rejection the request may never have reached the host; is the full 195s wait intended there, or should the error and retry affordance return earlier?
- If the wait is intended, should anything tell the user the command is still outstanding rather than merely blocking the composer?

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — 258917a is a targeted retirement fix with guards that can fail. No inline comments. The earlier threads (a blocked retry discarding the retained id, an unverifiable frame retiring it) and the transport-failure latency note are untouched by this commit, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (846fcb7): 258917a retires interrupted compaction ownership instead of letting it hold a waiter or suppression.

  • Resolved a waiter whose execution lost ownership. markUnknownInLane no longer returns early when the journal/fence has moved on; if the entry is still current it finishes it with an unknown result, so a command superseded by provider recovery resolves instead of hanging. The clear error is sliced to 4096 before it is published or returned.
  • Released an interrupted compaction generation on the next dispatched turn. StructuredSessionCompaction.claudeTurnStarted finishes an interrupted generation (and now runs its onLateResult, which was previously only wired to expiry) when a later Orca-dispatched turn starts, while a /compact replay keeps transcript suppression.
  • Wired the Claude release through the observation path. observeClaudeCompaction now calls claudeTurnStarted for turn-opening messages before deciding whether to translate, and accepts a narrower translator shape.
  • Added guards: a concurrency case where provider recovery advances the session generation, plus Claude cases for the later-turn release and for keeping a /compact replay suppressed.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — e9690a7 closes the remaining silent-return holes with guards that can fail. No inline comments. The earlier threads (a blocked retry discarding the retained id, an unverifiable frame retiring it) and the transport-failure latency note are untouched, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (258917a): e9690a7 retires stale command completions instead of dropping them.

  • Resolved a completion that lost its generation. complete now calls markUnknownInLane(entry, STALE_COMMAND_COMPLETION, true) when canSettle is false, so a provider result that lands after recovery advanced the fence resolves the waiter as unknown rather than returning silently.
  • Retired a superseded clear replacement. executeClear marks the command unknown before returning when its execution can no longer settle, instead of leaving the waiter to the next attempt.
  • Added guards for both paths: a compact whose provider result arrives after recovery, and a clear whose replacement attach finishes after recovery, each asserting the old command resolves unknown and a fresh command under the recovered fence completes.

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

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Visual evidence (commit 274a9deeb18939fe08f9f39c60adf112d8f2469f)

Hidden Electron launch from this worktree only (ORCA_BACKGROUND_LAUNCH=1, isolated user-data, CDP 127.0.0.1:9334, renderer 127.0.0.1:5174). window.api.app.getIdentity(): name=Orca: brennanb2025/native-chat-rpc-guard, devBranch=brennanb2025/native-chat-rpc-guard, devRepoRoot=/Users/brennanbenson/orca/workspaces/orca/native-chat-rpc-guard. Renderer console: 0 errors (React DevTools / React Grab / tooltip controlled-uncontrolled / xterm deadline warnings only).

What was exercised

  1. Isolated profile + throwaway git folder workspace. Native chat flags on (experimentalNativeChat, experimentalStructuredNativeChat, openAgentTabsInChatByDefault).
  2. Seeded Claude transcript fixture, toggled the terminal tab to Chat, opened the / command menu, selected /compact, and sent it. Chat recorded Ran /compact.
  3. New tab → Claude Chat (contentType: agent-session). Typed /compact in the structured composer and sent it. Host settled immediately with Not enough messages to compact.

Identity / empty home:

01-home-empty.png

Native Chat / menu with /compact selected:

05-slash-compact-selected.png

After send on the transcript fixture (settled Ran /compact):

07-compact-ran-settled.png

Structured Claude Chat idle composer:

09-structured-claude-idle.png

Structured /compact command picker:

10-structured-compact-picker.png

Structured /compact settled (host: Not enough messages to compact.):

12-structured-compact-settled.png

What passed

  • Branch identity matched this SHA/worktree.
  • Native Chat command surface rendered (/clear, /compact) and accepted a /compact send.
  • Structured Claude Chat opened and agentSession.conversationCommand for /compact returned a host decision that the UI displayed.

Limitation

Empty structured session: compact refused immediately, so this run did not capture a long-running compaction, a 195s unconfirmed marker, or a successful compact summary after a prior user turn. The transcript-fixture /compact is the PTY chat command marker, not the structured RPC path.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — 274a9de releases a provider's compaction state when close or replacement ends the child, and re-checks command currency against the durable lease. No inline comments. The earlier threads (a blocked retry discarding the retained id, an unverifiable frame retiring it) and the transport-failure latency note are untouched by this commit, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (e9690a7, with 48dd3fb/f65ac0c read in the same pass): 274a9de retires compaction state when the provider ends and fences command execution on the durable lease.

  • Released compaction ownership when the provider ends. The Claude adapter now routes the closeSession/releaseAcquisition onEvent through its own emit, so observeClaudeCompaction sees the ended frame; the Codex adapter added forwardProviderEvent to call compactions.ended before forwarding to deps.onEvent, wired into teardown and acquire.
  • Fenced command execution on the durable lease, not just the command record. conversationCommandExecutionIsCurrent now also requires record.lease.runtimeFence === fence, and the extracted retireStale retires a command whose lease advanced before the provider call or during the event flush.
  • Extracted retireStale from the four duplicated canSettle/markUnknown pairs, adding a check after compact() returns.
  • Guards added: a concurrency case pinning that provider work never starts after the lease advances, plus Claude and Codex cases for compaction ownership releasing on close or replacement.

Both levers were ablated after checkout: removing record.lease.runtimeFence === fence fails the new concurrency case, and reverting either provider's ended routing makes its releases compaction ownership test time out. The store's lease renewal preserves the existing fence, so the new lease check does not trip during normal operation.

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

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Visual evidence (post-merge, current head 44cd46c6b4)

Hidden Electron CDP recapture of PR #20658 at exact SHA 44cd46c6b43458f3c50968672561f8c56f844be5. Launch used ORCA_BACKGROUND_LAUNCH=1, an isolated user-data directory, CDP 127.0.0.1:9334, renderer 127.0.0.1:5174. window.api.app.getIdentity(): name=Orca: brennanb2025/native-chat-rpc-guard, devBranch=brennanb2025/native-chat-rpc-guard, devRepoRoot matches this worktree.

Steps exercised

  1. Isolated throwaway folder workspace at /tmp/orca-pr20658-44cd46c6/workspace (not a git worktree). Native Chat flags on.
  2. New tab → Claude opened a structured Claude Chat tab (Start a chat with Claude).
  3. Typed /compact in the structured composer, opened the command picker, selected /compact, and sent it.
  4. Host settled immediately with Not enough messages to compact.

Empty isolated app:

01-home-empty.png

Throwaway folder workspace with Terminal 1:

02-workspace-folder.png

New-tab menu with Claude / Codex structured-chat launch entries:

03-new-tab-menu.png

Structured Claude Chat idle composer:

04-structured-claude-idle.png

Command picker with /compact selected:

05-slash-compact-picker.png

Structured /compact settled (host: Not enough messages to compact.):

07-structured-compact-settled.png

Limitation

This session was empty, so compact refused immediately. The run verifies the picker, dispatch, and resulting terminal UI, but did not capture a long-running compaction or a successful compact summary after a prior user turn.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — eedf606 scopes the transport change back down by deleting the now-vestigial local deadline module. One documentation note below; the three prior threads remain untouched, which is why this is not an approval.

Reviewed changes

Re-reviewed the delta since the prior pullfrog review (c571bc4): eedf606 keeps runtime timeout behavior scoped.

  • Deleted the local deadline helper. local-runtime-call-deadline.ts and runtime-rpc-client-local-deadline.test.ts are gone, so the PR no longer bounds a local call that declares timeoutMs.
  • Reverted the local branch to the plain transport call. runtime-rpc-client.ts:85 again calls window.api.runtime.call({ method, params }) directly, restoring the documented contract that the local branch ignores timeoutMs (use-native-chat-skills.ts:232 already supplies a renderer-side backstop for exactly that).
  • No dangling references. The only exporter and importers of the helper were the two deleted files plus runtime-rpc-client.ts; a repo-wide grep finds none remaining, and no live test pins a local timeout.

ℹ️ The description still claims the removed transport fix

The PR body's "Problem" item 1, its "Change → Transport (1)" paragraph, its "Tests" list, and two ablation-table rows all describe local-runtime-call-deadline.ts and the runtime-rpc-client.ts deadline forwarding. That code is deleted at head eedf606, so the description now advertises a fix the diff does not contain. Worth trimming so the next reader is not misled about what this PR changes.

Technical details
# The PR body describes a transport change the diff no longer contains

## Affected sites
- PR body, "Problem" item 1 — "A declared deadline that never applied. `callRuntimeRpc` forwarded `timeoutMs` on the remote branch only. The local branch ... dropped it."
- PR body, "Change" → "Transport (1)" — "`local-runtime-call-deadline.ts` applies an explicitly requested `timeoutMs` on the local branch ..."
- PR body, "Tests" — "Guards in `runtime-rpc-client-local-deadline.test.ts` ...", plus the ablation rows keyed on `runtime-rpc-client.ts` (deadline forwarding) and on re-adding the `async` frame to `local-runtime-call-deadline.ts`.
- `src/renderer/src/runtime/local-runtime-call-deadline.ts` and `src/renderer/src/runtime/runtime-rpc-client-local-deadline.test.ts` — deleted at head `eedf606`; neither appears anywhere in the current full diff.

## Required outcome
- Problem/Change/Tests should drop the local-deadline fix (and its guard file and the two ablation rows), since `conversationCommand` already declares no transport deadline and the local branch is back to ignoring `timeoutMs`.

## Open questions for the human
- Is the local `timeoutMs` gap tracked elsewhere? The body's "Deliberately not done" only rules out a default local deadline for `agentSession.send`; it does not note that no local-reachable caller honors a declared `timeoutMs` at all (e.g. `automation-host-client.ts` passes `timeoutMs: 15_000` to a target that can be local).

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

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Visual evidence (exact final head fb38fa3be1)

Hidden Electron CDP recapture of PR #20658 at exact SHA fb38fa3be16b8a0b08f0a11a508513165f5d74b5. Launch used ORCA_BACKGROUND_LAUNCH=1, an isolated user-data directory, CDP 127.0.0.1:9334, renderer 127.0.0.1:5174. window.api.app.getIdentity(): name=Orca: brennanb2025/native-chat-rpc-guard, devBranch=brennanb2025/native-chat-rpc-guard, devRepoRoot matches this worktree.

Steps exercised

  1. Isolated throwaway folder workspace at /tmp/orca-pr20658-fb38fa3be1/workspace (not a git worktree). Native Chat flags on.
  2. New tab → Claude opened a structured Claude Chat tab (Start a chat with Claude).
  3. Typed /compact in the structured composer, opened the command picker, selected /compact, and sent it.
  4. Host settled immediately with Not enough messages to compact.

Empty isolated app:

01-home-empty.png

Throwaway folder workspace with Terminal 1:

02-workspace-folder.png

New-tab menu with Claude / Codex structured-chat launch entries:

03-new-tab-menu.png

Structured Claude Chat idle composer:

04-structured-claude-idle.png

Command picker with /compact selected:

05-slash-compact-picker.png

Structured /compact settled (host: Not enough messages to compact.):

07-structured-compact-settled.png

Limitation

This session was empty, so compact refused immediately. The run verifies the picker, dispatch, and resulting UI, but did not capture a long-running compaction or a successful compact summary after a prior user turn.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Final review status: ready

Reviewed exact head fb38fa3be16b8a0b08f0a11a508513165f5d74b5 through repeated independent review and a final readiness audit. The last review loop returned clean, and the readiness audit found no P0, P1, or P2 issues.

  • Functional: /compact and /clear now remain host-owned through their provider terminal outcome. Admission is durable, same-operation replay cannot start a duplicate provider command, and cancel/close/restart/clear/reacquisition/teardown paths fence stale work.
  • Architecture and performance: long provider work runs outside the serialized session lane; durable settlement precedes lifecycle publication; renderer state is a bounded observer with timer/teardown cleanup. No polling or unbounded state was added.
  • Scope and compatibility: the final diff is limited to Native Chat conversation-command lifecycle, correlation, recovery, localization, and tests. Generic local RPC timeout behavior matches main. No RPC opcode/schema or required field changed; legacy reply fallback remains. Cross-version wire CI and Windows packaging passed.
  • Validation: 15 focused files / 231 tests, pnpm tc, changed-code quality with zero findings, localization runtime-catalog verification, and diff checks passed locally. All required GitHub checks are green, and GitHub reports MERGEABLE/CLEAN.
  • Visual proof: exact-head hidden Electron/CDP results show the structured command picker, /compact dispatch, and the immediate empty-session terminal result.

Remaining coverage gaps are a live SSH run, live Windows interaction, and visual capture of a successful long-running compaction or the 195-second unresolved state. The platform-neutral implementation, focused lifecycle tests, Windows/package CI, and wire-compatibility CI make these non-blocking for this PR. Mobile emulator QA is inapplicable because no mobile UI or protocol surface changed.

@OrcaWin

OrcaWin commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Closing as superseded by the newer command/message architecture on main. I reviewed the full production diff, its changed test intents, and the current receipt, provider-terminal and clear paths; this is not based on the old timeout/watchdog comments, branch age, or conflicts.

  • /compact now enters the durable ordinary-submission/queued-card path introduced by fix(native-chat): a /compact sent while the agent works waits in line instead of being refused #25704 and subsequently strengthened by fix(native-chat): accept sends through their own receipt #26672. conversationCommandPlan uses the operation ID as the client message ID and replays the existing submission/card. The RPC reply means handover, while the command's journal turn remains owned by provider frames. The long-running command therefore no longer needs this PR's separate prepared-command execution controller and renderer claim/deadline observer.
  • Main's Claude command dispatch binds a sent UUID to the command turn; terminal interpretation rejects an explicitly different input UUID and requires compaction-boundary evidence before reporting success. Codex carries the command under its claimed provider turn and ends that journal turn from the matching completion, with compaction/Stop/failure evidence. These replace the old StructuredSessionCompaction path this PR extends.
  • Keep native chat /clear in the same conversation #26579 changed /clear to an atomic context boundary within the same conversation. The divider, boundary and operation receipt commit together; replay after a lost acknowledgment does not create another divider. This invalidates the proposed replacement-session attach, redirect and abandonment lifecycle.
  • The renderer mutation layer also now captures the issuing visit for late presentation and separates write outcomes from toasts. The PR's old shared writeError ownership/scope repairs do not need to be transplanted into that replaced design.

Evidence is pinned to main 77130982faf44abe3e4fb02354e79b61cdad5358; the relevant command/turn/mutation production files are unchanged at the currently fetched main 4e0f391c382f0189e70b57e8fc2848f02d39d157. I also ran the actual pinned provider command-turn modules against synthetic frames: Claude ignored a different UUID, reported failure without compaction evidence and success with it; Codex ignored an unrelated provider turn, required compaction evidence and ignored a settled late turn. That probe is mechanism evidence, not a full translator/journal/restart/Electron/SSH test. Current clear rollback and lost-ack assertions were inspected, not rerun.

The original reliability goal remains valid and is carried by the current durable-message and in-place-clear paths. This is architectural supersession, not a claim that the two implementations have identical internal lifecycle rules. Branch preserved.

@OrcaWin OrcaWin closed this Oct 27, 2026
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.

2 participants