Skip to content

fix(native-chat): honor saved agent Command and Arguments - #25721

Merged
brennanb2025 merged 19 commits into
mainfrom
brennanb2025/experimental-chat-agent-launch
Oct 6, 2026
Merged

brennanb2025 merged 19 commits into
mainfrom
brennanb2025/experimental-chat-agent-launch

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 42 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1007 $\color{#cf222e}{\Huge{\mathbf{−}}}$​126 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​881
Prod 39 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​604 $\color{#cf222e}{\Huge{\mathbf{−}}}$​55 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​549

ELI5

After #25720, updated structured native chat opens regardless of the Command saved in Settings > Agents, but always runs the stock CLI. Saved Arguments are also ignored without this PR. Users who choose a custom executable or supply supported flags therefore get a different launch from the one they configured.

Claude and Codex structured chats now use a resolvable saved executable and supported Arguments on start and resume. A shell-line Command, missing executable or workspace-relative path still opens structured chat with the stock CLI. Settings stays the same, with no extra notice.

What Changed

  • The execution host resolves one executable Command for both a chat and its model-catalog probe. Absolute paths, home-relative paths and executable names found on that host can select a custom binary; other values use the stock resolver.
  • Each process start reads current saved Arguments. Editing Settings applies to the next retry or resumed child. Claude translates supported flags into Agent SDK options and preserves every additional directory; Codex places supported root options before app-server.
  • Chat keeps control of transport, session identity and permissions. Unsupported Arguments use existing failure/retry surfaces to name the option and point to Settings > Agents > Arguments, without exposing values or prompts.
  • Environment, Agent Permissions, terminal launches and terminal-backed chat keep their existing behavior. Main's routing and registered-agent setup are retained.

Why

Structured chat already runs the user's installed CLI. Reusing the existing settings supports custom executables without adding another configuration surface. Sharing resolution keeps model discovery consistent with the chat. Stock fallback preserves #25720's decision that Command must not prevent structured chat from opening, while chat retains the flags needed for a reliable transport and session identity.

Linked Issue

Maintainer-requested fix; no pre-existing issue was provided.

Visual Proof

Before (earlier build): structured Codex omitted saved Arguments; the idle stand-in failed startup.

Before structured Codex

After, exact head 3f70f4ef2b3: the custom Codex binary opens a structured chat with its model catalog and draft composer.

Custom Codex structured chat on final head

Actual process logs and matching selected chat-tab records prove all four launches:

Saved Command Actual executable Saved Arguments observed
Absolute custom Claude executable Custom stand-in, PID 22834 --model rig-model, all three --add-dir values
Absolute custom Codex executable Custom stand-in, PID 22879 -c model_reasoning_effort=high --enable unified_exec before app-server
npx claude Stock rig Claude, PID 22915 Same saved model/directories
wrapper --arg Stock rig Codex, PID 22929 Same config/feature arguments before app-server

All four actual session children carry the structured-session and fake-environment markers. The selected tabs are agent sessions and show chat composers; neither shell-line row opened a terminal.

Remaining final-head chat panes and existing Settings

Custom Claude structured chat with expected stand-in startup error

Shell-line Claude Command keeps structured chat

Shell-line Codex Command keeps structured chat

Existing Command and Arguments for the custom Claude binary

Existing shell-line Command and saved Claude Arguments

The hidden macOS proof uses isolated homes, an empty launch environment and stand-in CLIs. It verifies actual launches and rendered chat surfaces, not completed conversations. Claude's stand-in intentionally exits during startup; Codex implements only the minimal launch protocol. No real credentials or seeded conversation were used. Earlier unsupported-Arguments feedback uses the existing retry surface:

Existing Arguments refusal surface, earlier build

Testing

  • I manually tested the final merged head locally
  • Automated tests added/updated

On merge head 7aac4f9b052 (main 4c355161500 merged in, including floating-workspace launch folders from #22302):

  • 723 tests across 75 explicit files: every PR test file, every test file in the merge conflicts, tests of modules that import the conflicted files or main's changed delivery loop and Claude connection, and the reopened-tab-close end-to-end unit test. 722 pass; one Windows-only test is skipped on macOS. Floating-workspace launch-folder tests and saved-Arguments tests pass side by side in the Claude and Codex launch resolvers.
  • Fresh node and renderer typechecks pass through the shared queue. Changed-file oxlint, formatting, changed-code-quality, line-count ratchet and localization catalogs/extraction/coverage pass.
  • The two required real-Claude files pass 10/10 once (run on the merge with main 975e732ce3e, before main's CI-fix commits). Claude's settings.json and Codex's config.toml and hooks.json byte-match their copies taken just before the run.

On earlier head 3f70f4ef2b3:

  • 724 tests pass across 58 explicit files: all PR test files, all tests changed by fix(native-chat): ignore the terminal launch command when starting a native chat #25720, structured-session import tests and the release-checkout importer. Coverage includes session/probe executable consistency, shell/missing/relative stock fallback, renderer/host routing, current Arguments on start/resume, repeated directories, SDK argv and actionable refusals.
  • Fresh node and renderer typechecks pass sequentially through the shared queue. Changed-file oxlint, formatting, changed-code-quality and localization catalogs/extraction/coverage pass.
  • The two required real-Claude files pass 10/10 once sequentially, with unchanged assertions/timeouts and fresh protected-file copies. All three files byte-match immediately afterward. No failing real-Claude assertion was rerun on this head.
  • Hidden Electron on macOS: all four custom-executable/shell-line rows have matching actual argv/environment logs and rendered structured chat panes. Settings remains the existing Agents page; no new notice is displayed.

Earlier installed Codex 0.159.0 parser checks used an isolated home without credentials or a turn: config/features before app-server start successfully; --profile fails there and is dropped. Historical real-Claude evidence remains disclosed in the local QA reports: an intermediate parallel run passed 8/10, serial confirmation passed 10/10, and previous head 521a8319973 passed 10/10 once. A launcher on superseded 038b76988e4 duplicated --config and failed before test collection; no real Claude child ran. None of those runs substitutes for current-head evidence.

Linux, Windows, SSH, WSL, paired-host and folder-workspace rendered flows, a real Codex conversation and rendered Claude resume remain locally unverified. Session-less probe matching is covered by automated tests; the live rig rows prove sessions. Terminal launch behavior is covered by retained tests, not a new terminal UI flow.

Review

  • Intended: Command never forces terminal chat. An executable selects the binary; shell lines, missing commands and workspace-relative paths use the stock CLI. Session-less probes have no workspace from which to resolve relative paths.
  • Intended: Codex Arguments precede app-server so supported config/feature options apply. Root model/search options do not configure app-server; the composer remains the model surface. Profiles are dropped rather than translated into new behavior.
  • Intended: Chat drops permission, transport, identity and SDK-owned flags. Agent Permissions remains the permission source. Unsupported repeated Claude scalar options are refused; directory options preserve all values.
  • Intended: Saved-Arguments errors reuse existing failure surfaces so users can correct the setting and retry.
  • Temporary: A composer permission picker remains a separate follow-up; this change uses existing Agent Permissions.

Agent skill upstream boundary

  • Not applicable; no skill source was changed or copied.

Notes

Merged main 4c355161500 includes #25720, registered-agent setup, readable Claude thinking and floating-workspace launch folders. The execution host reads its own settings; SSH and WSL retain terminal chat. No RPC parameter or stream operation is added. Existing failures carry an optional saved-Arguments cause; older readers ignore it and keep their wording. The legacy stored Arguments field remains compatible and is not read by structured launches.

CI: on merge head 7aac4f9b052, every workflow finished green: PR Checks (static analysis and typecheck, all five unit shards, relay integration, cross-version wire, Codex index-heal contract, packaging on Linux and Windows), Mobile Checks, Headless Node server and the test-size check. Jobs that do not apply to this change were skipped by the workflows' own change detection. The intermediate head 6c3b2a453d2 failed only main's own line-count lint and typecheck break, which main fixed in #25949 and the final merge includes. This PR does not touch pnpm-lock.yaml or package.json.

Checklist

  • This PR focuses on saved agent launch settings
  • I explained the problem, user-facing change, mechanism and alternatives
  • Before/after proof attached, with current-head evidence distinguished
  • Self-reviewed for correctness, security and performance
  • Cross-platform, SSH/remote and path impact considered
  • Validation complete and every pre-readiness final-head CI result classified

@brennanb2025
brennanb2025 marked this pull request as ready for review October 6, 2026 06:09
@brennanb2025
brennanb2025 marked this pull request as draft October 6, 2026 06:11
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7cf6ae99-fafa-4120-b65f-ac8c13948b09
📥 Commits

Reviewing files that changed from the base of the PR and between 6c3b2a4 and 7aac4f9.

📒 Files selected for processing (3)
  • src/main/claude/claude-model-catalog-probe.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts
  • src/main/runtime/structured-agent-session-codex-interrupt-order.test.ts

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


📝 Walkthrough

Walkthrough

Structured Claude and Codex launches now resolve configured executable commands and use configured saved Arguments. Provider-specific parsers apply supported options and reject or omit other argument forms. The runtime requires a launch-argument resolver. Argument validation details pass through session failure and refusal data to native-chat messages, with localized correction guidance. Tests cover launch resolution, argument handling, and safe failure details.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 7aac4

Saved Arguments for structured Claude and Codex chats are now honored. Two minor issues from earlier review remain open. A refusal on a resumed session may be worded as a first start. Claude flag parsing may pass stray positional tokens to the CLI. Neither is likely to cause serious failures, but both are worth resolving or accepting before merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 62 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: structured native chat now honors saved agent Command and Arguments.
Description check ✅ Passed The description covers the user-facing change, implementation, rationale, visual proof, testing, review notes, and checklist. It also explains that no issue was linked because this was a maintainer-re…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0f295a6c-7259-4bb5-91eb-7861232fbf20
📥 Commits

Reviewing files that changed from the base of the PR and between 5f7c848 and 521a831.

📒 Files selected for processing (85)
  • src/main/agent-launch/agent-launch-executor.test.ts
  • src/main/agent-launch/agent-launch-executor.ts
  • src/main/agent-launch/agent-launch-mode.ts
  • src/main/claude/claude-agent-sdk-contract-pins.test.ts
  • src/main/claude/claude-model-catalog-probe.test.ts
  • src/main/claude/claude-structured-auth-parity.test.ts
  • src/main/claude/claude-structured-launch-args.test.ts
  • src/main/claude/claude-structured-launch-args.ts
  • src/main/claude/claude-structured-launch-resolution.test.ts
  • src/main/claude/claude-structured-launch-resolution.ts
  • src/main/claude/claude-structured-turn-resume-point.test.ts
  • src/main/codex/codex-model-catalog-probe.test.ts
  • src/main/codex/codex-structured-launch-args.test.ts
  • src/main/codex/codex-structured-launch-args.ts
  • src/main/codex/codex-structured-launch-resolution.test.ts
  • src/main/codex/codex-structured-launch-resolution.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-agent-start.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-refusal.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts
  • src/main/native-chat/structured-agent-arguments-error.ts
  • src/main/native-chat/structured-agent-command-resolution.test.ts
  • src/main/native-chat/structured-agent-command-resolution.ts
  • src/main/native-chat/structured-agent-configured-args.test.ts
  • src/main/native-chat/structured-agent-configured-args.ts
  • src/main/native-chat/structured-agent-session-create-support.test.ts
  • src/main/native-chat/structured-agent-session-create-support.ts
  • src/main/runtime/claude-structured-session-integration.test.ts
  • src/main/runtime/orca-runtime-get-worktree-ps.ts
  • src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts
  • src/main/runtime/structured-agent-launch-args-wiring.test.ts
  • src/main/runtime/structured-agent-session-codex-interrupt-order.test.ts
  • src/main/runtime/structured-agent-session-codex-settlement-order.test.ts
  • src/main/runtime/structured-agent-session-codex-turn-end-settlement.test.ts
  • src/main/runtime/structured-agent-session-failure-logging.test.ts
  • src/main/runtime/structured-agent-session-held-signal.test.ts
  • src/main/runtime/structured-agent-session-host-refusal.test.ts
  • src/main/runtime/structured-agent-session-install-failure.test.ts
  • src/main/runtime/structured-agent-session-integration.test.ts
  • src/main/runtime/structured-agent-session-record-import-install.test.ts
  • src/main/runtime/structured-agent-session-runtime-exit.test.ts
  • src/main/runtime/structured-agent-session-runtime.test.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts
  • src/main/runtime/structured-agent-session-startup-reconcile.test.ts
  • src/main/runtime/structured-agent-session-startup-tab-restore.test.ts
  • src/main/runtime/structured-chat-coordinator-mail-rig.test-fixture.ts
  • src/main/runtime/structured-child-records-switch.test.ts
  • src/main/runtime/structured-claude-pending-rewind.test.ts
  • src/main/runtime/structured-claude-runtime-adapter.ts
  • src/main/runtime/structured-claude-scripted-runtime-test-support.ts
  • src/main/runtime/structured-codex-child-work-runtime.test.ts
  • src/main/runtime/structured-codex-session-rpc-test-harness.ts
  • src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx
  • src/renderer/src/components/native-chat/agent-session-failure-words-text.test.ts
  • src/renderer/src/components/native-chat/agent-session-failure-words-text.ts
  • src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts
  • src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/agent-launch-route-input.test.ts
  • src/renderer/src/lib/agent-launch-routing.test.ts
  • src/shared/agent-launch-intent.ts
  • src/shared/agent-session-argument-problem.ts
  • src/shared/agent-session-failure-copy.ts
  • src/shared/agent-session-failure-fact-schema.ts
  • src/shared/agent-session-failure-words.ts
  • src/shared/agent-session-failure.test.ts
  • src/shared/agent-session-failure.ts
  • src/shared/agent-session-refusal-details.ts
  • src/shared/agent-session-refusal-notice.test.ts
  • src/shared/agent-session-refusal-notice.ts
  • src/shared/agent-session-write-failure.ts
  • src/shared/node-cli-command-resolution.ts
  • src/shared/tui-agent-launch-command-override.test.ts
  • src/shared/tui-agent-launch-command-override.ts

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

Comment on lines +65 to +68
const values = separator === -1 ? [] : [arg.slice(separator + 1)]
while (args[index + 1] !== undefined && !args[index + 1]!.startsWith('-')) {
values.push(args[++index]!)
}

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:

git diff --no-ext-diff --unified=50 5f7c8483e9c0e7701356d37e3341adaa6018585b 521a8319973f65bf6c1c868ceacb81694c1b828c -- src/main/claude/claude-structured-launch-args.ts src/main/claude
printf '\n--- reviewed parser ---\n'
nl -ba src/main/claude/claude-structured-launch-args.ts
printf '\n--- direct references and tests ---\n'
rg -n -F -- 'claudeStructuredLaunchArgs' src/main/claude
printf '\n--- launch argument consumption ---\n'
rg -n -- 'extraArgs|additionalDirectories|positionalPrompt|StructuredAgentArgumentsError' src/main/claude/claude-structured-*

Repository: stablyai/orca

Length of output: 41677


🏁 Script executed:

printf '%s\n' '--- launch-args bindings ---'
rg -n -F -- 'resolveLaunchArgs' src
printf '%s\n' '--- parser tests ---'
nl -ba src/main/claude/claude-structured-launch-args.test.ts
printf '%s\n' '--- resolver dependency and call ---'
nl -ba src/main/claude/claude-structured-launch-resolution.ts | sed -n '35,115p;275,330p'
printf '%s\n' '--- Codex parser candidates ---'
rg -n -- 'LaunchArgs|launchArgs|positionalPrompt|multipleValues|additionalDirectories' src/main/codex src/main/native-chat
printf '%s\n' '--- changed argument source/callers ---'
rg -n -- 'claudeStructuredLaunch|createClaudeStructuredLaunchResolver|resolveLaunchArgs' src/main

Repository: stablyai/orca

Length of output: 32208


🏁 Script executed:

printf '%s\n' '--- configured argument resolver ---'
nl -ba src/main/native-chat/structured-agent-configured-args.ts
nl -ba src/main/runtime/orca-runtime-get-worktree-ps.ts | sed -n '145,170p'
nl -ba src/main/runtime/structured-agent-session-runtime.ts | sed -n '100,115p;258,305p'
printf '%s\n' '--- Claude adapter SDK call ---'
rg -n -- 'query\\(|launch\\.options|pathToClaudeCodeExecutable' src/main/claude/claude-structured-session-adapter.ts src/main/runtime/structured-claude-runtime-adapter.ts
nl -ba src/main/runtime/structured-claude-runtime-adapter.ts | sed -n '70,100p'
printf '%s\n' '--- Codex parser contract ---'
nl -ba src/main/codex/codex-structured-launch-args.ts | sed -n '65,125p'
printf '%s\n' '--- SDK argv contract test assertions ---'
rg -n -- 'produces a matching CLI flag|extraArgs|spawn.*args|PRE_SDK_ARGV' src/main/claude/claude-agent-sdk-contract-pins.test.ts

Repository: stablyai/orca

Length of output: 11628


🏁 Script executed:

printf '%s\n' '--- Claude adapter query binding and handoff ---'
rg -n -F -- 'query(' src/main/claude/claude-structured-session-adapter.ts
rg -n -F -- 'openConnection' src/main/claude/claude-structured-session-adapter.ts
nl -ba src/main/claude/claude-structured-session-adapter.ts | sed -n '1,155p'
printf '%s\n' '--- SDK package declaration ---'
rg -n -- '"@anthropic-ai/claude-agent-sdk"|claude-agent-sdk' package.json pnpm-lock.yaml

Repository: stablyai/orca

Length of output: 8788


Restrict values to flags that take values.

The parser binds each following non-dash token to the current flag. Thus --chrome prompt becomes extraArgs.chrome = 'prompt', and --model -foo sets model to null and skips -foo. The resolver includes these values in the structured SDK options.

Use a Claude-specific allowlist for value-taking flags. Keep bare positionals on the existing ignore path, preserve repeated --add-dir values, and handle dash-prefixed values explicitly instead of silently dropping them.

Comment on lines +91 to +93
failure: { kind: 'startFailed', argumentProblem },
surface: 'rejection',
context: { ...context, agentName: argumentProblem.agent }

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 60,120p src/shared/agent-session-refusal-notice.ts
rg -n 'restartFailed|startFailed' src/shared/agent-session-failure-words.ts src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-refusal.ts

Repository: stablyai/orca

Length of output: 3029


🏁 Script executed:

printf '%s\n' '--- refusal notice references ---'
rg -n -F -- 'agentSessionRefusalCauseParts' src || test "$?" -eq 1
printf '%s\n' '--- refusal and argumentProblem producers/types ---'
rg -n -F -- 'argumentProblem' src/shared src/main/native-chat | head -100
rg -n -F -- 'agent_session_operation_invalid' src/shared src/main/native-chat | head -100
printf '%s\n' '--- acquisition/restart and accept-deliver wording references ---'
rg -n -i 'accept.*deliver|deliver.*accept|couldNotRestart|restartFailed|newSession' src/shared src/main/native-chat | head -160
printf '%s\n' '--- refusal notice module with line numbers ---'
nl -ba src/shared/agent-session-refusal-notice.ts | sed -n '1,150p'

Repository: stablyai/orca

Length of output: 43442


🏁 Script executed:

printf '%s\n' '--- launch retry caller ---'
nl -ba src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx | sed -n '1,110p'
printf '%s\n' '--- generic write notice callers ---'
rg -n -F -- 'agentSessionWriteNoticeParts(' src/renderer src/main
printf '%s\n' '--- refusal argument details schema and parser ---'
nl -ba src/shared/agent-session-refusal-details.ts | sed -n '125,155p;240,275p'
printf '%s\n' '--- send preparation complete declaration ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.ts | sed -n '1,150p'
printf '%s\n' '--- delivery-loop argument refusal block ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts | sed -n '235,290p'
printf '%s\n' '--- failure text conversion block ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts | sed -n '85,130p'
printf '%s\n' '--- accept-then-deliver failure assertions ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts | sed -n '325,380p'

Repository: stablyai/orca

Length of output: 21601


🏁 Script executed:

printf '%s\n' '--- launch retry caller ---'
nl -ba src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx | sed -n '1,110p'
printf '%s\n' '--- generic write notice callers ---'
rg -n -F -- 'agentSessionWriteNoticeParts(' src/renderer src/main || test "$?" -eq 1
printf '%s\n' '--- refusal argument details schema and parser ---'
nl -ba src/shared/agent-session-refusal-details.ts | sed -n '125,155p;240,275p'
printf '%s\n' '--- send preparation complete declaration ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.ts | sed -n '1,150p'
printf '%s\n' '--- delivery-loop argument refusal block ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts | sed -n '235,290p'
printf '%s\n' '--- failure text conversion block ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts | sed -n '85,130p'
printf '%s\n' '--- accept-then-deliver failure assertions ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts | sed -n '325,380p'

Repository: stablyai/orca

Length of output: 21601


🏁 Script executed:

printf '%s\n' '--- launch retry caller ---'
nl -ba src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx | sed -n '1,110p'
printf '%s\n' '--- generic write notice callers ---'
rg -n -F -- 'agentSessionWriteNoticeParts(' src/renderer src/main || test "$?" -eq 1
printf '%s\n' '--- refusal argument details schema and parser ---'
nl -ba src/shared/agent-session-refusal-details.ts | sed -n '125,155p;240,275p'
printf '%s\n' '--- send preparation ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.ts | sed -n '1,150p'
printf '%s\n' '--- delivery-loop argument refusal ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts | sed -n '235,290p'
printf '%s\n' '--- failure text conversion ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts | sed -n '85,130p'
printf '%s\n' '--- accept-then-deliver assertions ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts | sed -n '325,380p'

Repository: stablyai/orca

Length of output: 21560


🏁 Script executed:

printf '%s\n' '--- NativeChatLaunchRetry production call sites ---'
rg -n -F -- 'NativeChatLaunchRetry' src/renderer/src
printf '%s\n' '--- launch lifecycle / failure prop derivation ---'
rg -n -F -- 'StructuredAgentSessionLaunchLifecycle' src/renderer/src

Repository: stablyai/orca

Length of output: 11991


🏁 Script executed:

nl -ba src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx | sed -n '250,315p'
rg -n -F -- 'NativeChatStructuredSession' src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx

Repository: stablyai/orca

Length of output: 3678


🏁 Script executed:

nl -ba src/renderer/src/components/native-chat/use-native-chat-provisional-launch.ts | sed -n '1,180p'
nl -ba src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx | sed -n '1,90p'

Repository: stablyai/orca

Length of output: 10069


🏁 Script executed:

printf '%s\n' '--- launch failure API and registry declarations ---'
rg -n -F -- 'useStructuredAgentSessionLaunchFailure' src/renderer/src/lib
rg -n -F -- 'failure:' src/renderer/src/lib/structured-agent-session-launch-registry.ts
nl -ba src/renderer/src/lib/structured-agent-session-launch-registry.ts | sed -n '1,240p'
printf '%s\n' '--- argument-problem refusal producer ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-refusal.ts | sed -n '1,175p'

Repository: stablyai/orca

Length of output: 18935


🏁 Script executed:

printf '%s\n' '--- launch failure API and registry declarations ---'
rg -n -F -- 'useStructuredAgentSessionLaunchFailure' src/renderer/src/lib
nl -ba src/renderer/src/lib/structured-agent-session-launch-registry.ts | sed -n '1,240p'
printf '%s\n' '--- argument-problem refusal producer ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-refusal.ts | sed -n '1,175p'

Repository: stablyai/orca

Length of output: 18935


🏁 Script executed:

printf '%s\n' '--- failed-acquisition refusal callers ---'
rg -n -F -- 'failedAcquisitionRefusal(' src/main
printf '%s\n' '--- launch failure assignments and failure-bearing state updates ---'
rg -n -F -- 'failure:' src/renderer/src/lib/structured-agent-session-launch-callers.ts src/renderer/src/lib/structured-agent-session-launch.ts src/renderer/src/lib/structured-agent-session-launch-registry.ts src/main
rg -n -F -- '.failure =' src/renderer/src/lib src/main/native-chat/agent-session-wire

Repository: stablyai/orca

Length of output: 44136


🏁 Script executed:

printf '%s\n' '--- failed-acquisition refusal callers ---'
rg -n -F -- 'failedAcquisitionRefusal(' src/main
printf '%s\n' '--- launch failure assignments ---'
rg -n -F -- 'failure:' src/renderer/src/lib/structured-agent-session-launch-callers.ts src/renderer/src/lib/structured-agent-session-launch.ts src/renderer/src/lib/structured-agent-session-launch-registry.ts src/main
rg -n -F -- '.failure =' src/renderer/src/lib src/main/native-chat/agent-session-wire

Repository: stablyai/orca

Length of output: 44102


🏁 Script executed:

nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts | sed -n '175,225p'
nl -ba src/renderer/src/lib/structured-agent-session-launch-outcome-tracking.ts | sed -n '1,90p'

Repository: stablyai/orca

Length of output: 5183


🏁 Script executed:

nl -ba src/renderer/src/lib/structured-agent-session-launch-failure.ts | sed -n '1,180p'
rg -n -F -- 'class StructuredAgentSessionCreateRefusalError' src/renderer/src/lib

Repository: stablyai/orca

Length of output: 2841


🏁 Script executed:

printf '%s\n' '--- refusal-reference reader and failure conversion ---'
rg -n -F -- 'function agentSessionRefusalFailure' src/shared/agent-session-write-failure.ts
rg -n -F -- 'function readAgentSessionRefusalReference' src/shared/agent-session-wire-refusals.ts
nl -ba src/shared/agent-session-write-failure.ts | sed -n '120,205p;225,260p'
nl -ba src/shared/agent-session-wire-refusals.ts | sed -n '1,120p'

Repository: stablyai/orca

Length of output: 9350


Preserve restart wording for resumed launch refusals.

When a resumed acquisition fails because of saved Arguments, its refusal can include argumentProblem while the host words the failure as a restart. This notice always formats that detail as startFailed, so NativeChatLaunchRetry can tell the user the agent “couldn't start.” Carry the acquisition’s start-versus-restart kind into this formatter and use restartFailed for restart failures. The accept-then-deliver path is separate and already shows “couldn't restart.”

@brennanb2025
brennanb2025 marked this pull request as ready for review October 6, 2026 10:53
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Status: brought up to date with main; still ready for review.

What this PR changes for you. In the updated structured chat for Claude and Codex, the Command and Arguments you save in Settings > Agents now take effect. If Command names a single program (an absolute path, a home-relative path, or a name found on your PATH), the chat and its model list run that program. If Command is a shell line such as npx claude, or the program can't be found, the chat still opens and runs the normal Claude or Codex CLI. It never falls back to a terminal. Saved Arguments are read again on every start and resume, so an edit applies on the next retry. If an argument can't be used in chat, the existing retry message names it and points you to Settings > Agents > Arguments.

Merge. Latest main (842c6c667a8) is merged in as 57538155340. Three files had conflicts, and each one kept both sides. Main's new readable-thinking option for Claude and your saved Arguments now go into the same launch. A new test checks that the two combine, with the thinking display staying under Orca's control. One test that main added was missing the launch-arguments input this PR requires, and that input was added.

Verified on this head:

  • 579 tests across 51 named files. 578 pass, and one Windows-only test is skipped on macOS.
  • Node and renderer typechecks, lint, formatting, the changed-code quality gate and the localization checks all pass.
  • The real Claude CLI tests pass 10/10 in one run. Your Claude and Codex settings files were byte-identical before and after.
  • CI finished green on every workflow: PR Checks, Mobile Checks, Headless Node server and the test-size check.
  • The four launch rows and screenshots in the PR body come from the earlier head 3f70f4ef2b3. They cover a custom Claude program, a custom Codex program and a shell-line Command for each, and every row opened a structured chat. The launch code those rows exercise is unchanged by this merge, apart from the readable-thinking combination described above.

Not verified: screens on Linux, Windows, SSH, WSL or a paired host. Also not verified: a full conversation with the real Codex CLI, or a rendered resume of a Claude chat.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Main merged again (head 7aac4f9b052); still ready for review.

Main's floating-workspace change (#22302) now chooses the folder a chat runs in, so a floating chat keeps the folder it started in. That change touched the same launch code as this PR. Both are kept: a structured Claude or Codex chat starts in the folder main chooses, and it still uses your saved Command and Arguments. The Codex options still go before app-server. One file went one line over the size limit, so it was tightened.

Re-verified on this head:

  • 723 tests across 75 named files: 722 pass, and one Windows-only test is skipped on macOS. The floating-folder tests and the saved-Arguments tests now run side by side.
  • Node and renderer typechecks, lint, formatting, the changed-code quality gate and the localization checks pass.
  • The real Claude CLI tests pass 10/10 in one run. They ran on the first merge of main (975e732ce3e), before main's later CI fixes. Your Claude and Codex settings files were byte-identical before and after.
  • CI finished green on every workflow. An intermediate head failed only on main's own lint and typecheck break, which main fixed in fix(ci): restore main's typecheck and lint after #25751 and #24369 #25949. This head includes that fix.

The earlier "Not verified" items are unchanged.

@brennanb2025
brennanb2025 merged commit 0a26f3e into main Oct 6, 2026
32 checks passed
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…t-launch-instant-tab

Conflicts resolved against #24934's rebuild (82544cb), which this branch was built on, so each
keeps main's squashed #24934 plus this branch's change. agent-launch-floating-workspace.test.ts takes
main's #22302 routing (a structured preference opens a Codex chat) with this branch's host-derived
viewMode on the terminal branch. The record-store slot test gets the resolveLaunchArgs dep #25721 made
required, the same line as main's fix.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
The PR was conflicting with main, so CI could not run.

- structured-agent-session-agent-start.ts: kept D3's `aborted` and main's
  `argumentProblem` (#25721) on the failed start outcome.
- en.json: kept D3's `sessionNotRestored` and main's saved-Arguments strings.
- structured-agent-session-delivery-loop.ts: main's growth plus D3's
  aborted-start guard went over the 300-line budget; named the refused-start
  type once so refusedStart's signature fits on fewer lines.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
… typecheck passes

Main's typecheck fails here too: #25721 made resolveLaunchArgs required and
this test (#25051) predates it. Same line, same place as the open main fix
(#25977), so merging main after it lands is a no-op.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
… Claude Arguments add

Main (#25721) now builds additionalDirectories from the user's --add-dir arguments; this branch's attachment-store grant replaced that list instead of adding to it. Both are granted now. Moves the Claude session-id derivation to its own module to keep the resolver under the line limit.
@brennanb2025
brennanb2025 deleted the brennanb2025/experimental-chat-agent-launch branch October 6, 2026 22:20
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
… Command

After #25721 native chat runs a runnable Command, and this PR refuses one it can't run; four comments still said the Command applies to terminal launches only. The pre-spawn test fixture also drops the saved value from its error message, matching the resolver.
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
… running the stock CLI (#25667)

* fix(native-chat): honor custom Claude and Codex launch commands

* fix(native-chat): sync launch failure localization catalogs

* feat(native-chat): run Settings → Agents → Command as the native chat program

Native chat ignored the Command setting, so a user who pointed it at a
custom Claude or Codex build still got the stock CLI. The host now reads
the Command per acquisition and per model-catalog probe, resolves it on the
execution host as a program (an absolute path, ~ path, or a name on PATH),
and spawns it with the normal structured arguments. A set Command that is
not a runnable program refuses the start with a new agentCommandNotRunnable
failure that names the setting; it never falls back to the stock CLI.

* fix(native-chat): find a configured Claude Command on the launch PATH; clearer copy

A Claude Command given as a bare name was looked up on Orca's own PATH,
not the PATH the Claude child launches with (login shell plus Settings →
Agents environment), so a name only that environment provides was refused.
Codex already resolved against its launch environment. Claude now reads
its launch env first and resolves a configured name against its PATH and
home; the stock lookup with no Command is unchanged.

The failure copy now states the rule (a program path or name, no
arguments or variables) and names the Settings control (Reset). The
catalog fingerprint doc notes that the program is not keyed, so a chat on
an older program keeps refreshing the shared entry until it ends.

* fix(native-chat): one PATH key for the Claude lookup and the child on Windows

On Windows, an inherited `Path` and a Settings → Agents env `PATH` both
reached the Claude child. The configured-program lookup read the first
inserted twin (inherited), while Node's child_process keeps the
lexicographically first (`PATH`, the user's), so a bare-name Command found
only on the Settings PATH was refused.

The Claude launch env now drops inherited case-twins of the overlay's
variables (the rule the structured shell env already applies), so the
lookup and the child read one PATH. pathEnvOf follows Node's win32 key
rule, and Codex's lookup uses it too, so both agents read PATH one way.
es/fr/ja copy now says the Command must be, as the English does.

* fix(native-chat): refuse a Command that can't run instead of running the stock CLI

A saved Settings → Agents → Command that did not resolve to a program (a
command line such as `codex --profile work`, `FOO=1 claude` or `npx …`, a
missing or relative path, a non-executable file) silently ran the stock
Claude/Codex in native chat and its model-catalog probe. The user's choice
was ignored with no sign of it.

A set Command that does not resolve now refuses the start with a new
agentCommandNotRunnable failure naming the setting, beside the existing
Retry; the catalog probe refuses the same way and never lists the stock
CLI. A blank Command keeps the stock lookup. On Windows a resolved file
without a spawnable extension (.exe/.com/.cmd/.bat) refuses too, instead
of failing to spawn with no word about the setting.

* chore(native-chat): correct comments that say native chat ignores the Command

After #25721 native chat runs a runnable Command, and this PR refuses one it can't run; four comments still said the Command applies to terminal launches only. The pre-spawn test fixture also drops the saved value from its error message, matching the resolver.
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
…otocol (#25225)

* Leave a stopped turn's running tools to the agent's own end

When another writer settles the open turn (a person's Stop), the assembler
now only stops that turn's text and cancels its pending prompts. Running tool
calls stay the agent's: a progress update or completion it reports after the
Stop lands as reported, and whatever is still running settles at the agent's
turn end for that turn, the next turn's open, or the session's end.

An agent's end for an earlier turn while a newer one is open no longer clears
the open turn's activity line or ends its anonymous reply. An unnamed end right
after a Stop ends the stopped turn instead of being dropped. The test rig's
restart no longer writes the dead assembler's window text, matching dispose.

* Pin that a stopped turn's running tools hold budget until the agent's end

* Type the stopped turn's tool progress update as a tool body

* List every event the assembler hands to the decision step

The type-aware lint requires an exhaustive switch with no default case.
Also retitle a Stop test to say what it asserts.

* End a running call as its turn's journal row ends

A call still running when its turn ends takes the state of that turn's
row: a row another writer settled first (a person's Stop) stands, so its
calls read interrupted whatever the provider's later end reports. The
no-ending path that settled calls from the Stop row is gone, since a Stop
now leaves running calls to the provider. Adds the two Spanish strings.

* Say why a Grok turn failed, and keep task rows in Grok's own words

A failed Grok turn ended with no reason on screen: the translator dropped
every copy of Grok's message. The failed turn now gets one status row in
Orca's existing "provider did not accept this message" words with Grok's
reason, read from whichever copy arrives first (the given-up retry, the
turn's end, the prompt's completion notice, or the prompt's error answer);
later copies only fill a reason the row still lacks.

A running background command no longer reads "Background task <id>
started": a task's summary is mapped only once it has settled. A monitor
stays a monitor when the agent reads its output: a frame that names no
kind keeps the known one, and a "[monitor" command is a monitor.

A prompt's turn is marked started, so a late frame for an ended prompt
neither reopens it nor becomes the active turn. A tool's turn is held in
one place at a time.

* Read a monitor from Grok's exact output prefix

* Word a failed Grok turn in Grok's own text, not as a refused message

A turn that started and then failed was told "The provider did not accept
this message", Orca's sentence for a message refused before its turn. The
row now reads as a Codex turn-ending error does: an error status row with the
provider's own words. With no words, the dialect names the failure ("Grok
ended this turn with an error." / "Grok usage limit reached."), else the
agent's display name does.

* Settle a stopped turn's running call as its turn row ended after a restart too

The restart sweep ended every running call by the death evidence alone, so after
a person's Stop with no proof the child died the call read failed under a turn
that read interrupted. The sweep and the live dead-generation settlement now ask
the same rule the assembler does: a call in a turn already settled ends as that
row ended; only a turn still running leaves its calls to the evidence.

* Keep the dead-generation settlement under the line cap

* Register the ACP schema verify step in the PR preflight phase test

* refactor(agent-session): one required agent registry; declarations admit what they claim

A StructuredAgentRegistry, built once from the {definition, adapter}
registrations, is now a required host dependency and the router routes with
it. The adapter interface loses its optional router-only capabilities?() and
definition?(); every reader (options at rest, thread goal, rewind, the
/compact handover) asks the registry. A live session still narrows rewind
through the adapter, and the host combines declared and narrowed in one
helper. The registry refuses a registration that declares compact, a thread
goal or rewind without the adapter method behind it.

/compact is admitted by the declared capability, so an agent that declares
compact:false gets the commandRefused fact instead of a thrown error.

Also: the cut-turn notice names the agent from the catalog, create-support
builds the account home through agentSessionAccountHome, and the turn
status text older clients read names the session's agent instead of
defaulting to Codex (Claude/Codex text unchanged).

* Read ACP permissions, session events and prompt errors through the protocol client's own types

The translator now reads a permission request with the client's lenient reader, a session update
with its session-event reader, and takes only the agent's own error answer as a failed prompt's
reason, so an Orca-side error never reads as the provider's words. Tests cover protocol values
newer than this build.

* refactor(agent-session): the router applies the declared rewind itself

The router holds the registry, so its rewindSupport answers the owner's
declared rewind narrowed by the adapter, in one place no reader can bypass.
The host-side combining helper is gone; readers ask the adapter they hold.

* test(agent-session): register the agents the merged-in tests now need

The adoption replay test builds its host over this build's registered agents
instead of an adapter definition method, and the stored-form test passes the
stored agents the record guard now requires.

* chore(agent-session): the registry is the one lookup; drop the test-only empty capability record

* fix(agent-session): keep "dismiss all" restart offers dismissed for the desktop

The desktop renderer calls the restart methods as a paired runtime client
without the registered-agents capability, so it was handed a Claude/Codex
audience and its "dismiss all" took the scoped path: no dismissal fence and
no clearing of unwritten teardown witnesses, so a late teardown write could
bring a dismissed offer back.

The audience is now derived against this host's registry: a client that can
show every agent the host registered gets none, and dismisses exactly as
before (one fence for every offer). A client that truly cannot show some
agent still gets a final dismissal for what it sees: each cleared session is
fenced on its own (bounded, superseded by a dismiss-all fence) and only the
witnesses of agents it sees are dropped.

One predicate now answers which agents a client renders, for both tab
projection and restart offers; the Claude capability rule is folded in.

* fix(agent-session): a changed agent definition never hides that agent's chats

A record was readable only if every handle used the transport its agent's
definition declares today, and its account variable was declared by some
registered agent. A later build that drives the same agent over another
protocol, or under a renamed variable, would have set every existing chat of
that agent aside: no tab, no history, no error.

Readability now asks only that the record agree with itself: its agent is
registered, every handle shares one namespace owned by that agent, and the
account variable is a well-formed name. Whether this build can drive it (the
chain's transport is the one the agent speaks, and the account variable is
that agent's own, since it becomes the child's environment) is decided in
performAttach, the one admission every start of every agent passes through,
and refused there as hostUnsupported. A record pinning another agent's
variable is refused the same way rather than passing because some agent
declares it.

* refactor(agent-session): each agent's registration says where it runs and which account it pins

createSupport, create and the model catalog's account read still chose a
location rule and an account resolver by name (Claude, Codex, else none),
so a new agent would have needed a third branch beside the registration
list that already decides storage, routing and publication.

Each runtime registration now carries supportsLocation(location) and
resolveAccountHomePath({ launchEnv, location, purpose, workspacePath }),
and Claude's and Codex's rules move into their entries unchanged: a read
still syncs no home and starts no bridge, a Codex launch still trusts the
workspace first, and WSL locations resolve as before. The list exists
before the host is built, so none of these reads installs the host, and an
agent the list does not hold is answered no without opening the journal.

* test(agent-session): drop a duplicate registry key; keep fences out of the capsule state

A spread input already carries the registry, so the explicit key was
overwritten (TS2783). The session-fence helper now returns only the fence,
not the capsule state it was given.

* fix(agent-session): a scoped dismiss-all persists no per-session fence

The per-session dismissal fence added a forever-persisted capsule field
with an arbitrary cap that served no caller: only the local desktop calls
the restart methods, and on a host whose agents it all shows it gets no
audience and takes the unscoped, fenced dismissal. A scoped dismiss-all
(a caller that cannot show some registered agent) keeps that agent's
offers, clears only its audience's unwritten witnesses, and writes no
fence; a late write from this process is serialized behind it. The field
never left this unreleased branch.

* fix(agent-session): refuse an attach whose agent is not the session's own

The start check judges the record's agent while the router starts the adapter of params.agent, and the attach wire let the two differ. Admission now refuses agent !== provider as requestMalformed, so the agent checked is the agent started. Every host-built attach already sets them equal.

* fix(agent-session): offer to start a chat only when the start would accept it

The restart offer, a failure's retryable flag and the pre-send check asked only whether the adapter runs the chat's location, while the start also refuses a record this build cannot drive; such a chat was offered Resume and its Retry failed forever. One host predicate, hostCanStartRecord (location support and agentDrivesSession), now answers all four; adapterSupportsRecord stays the reader gate, and an undrivable chat's offer is kept, not retired.

* fix(agent-session): the model-catalog probe uses a record's account only if this build drives it

A record-scoped catalog read started the agent's lister under the record's account path whatever variable the record pinned. It now uses that path only when agentDrivesSession holds, and otherwise resolves the account as for a read with no record.

* refactor(agent-session): the record store admits agent ids; comments say where transport is checked

The store only ever asked whether an agent is registered, so its admission list is now the registered ids; the definition is the one home of an agent's transport and account variable. Comments that said the record store checks transport, and one that cited a nonexistent function, now point at agentDrivesSession and the attach admission. The isPersistedAgentSessionRecord(value, agents) call shape and the test fixture the cross-version probe imports are kept.

* docs(agent-session): the record store admits the registered agents' ids

* refactor(native-chat): Grok's registration declares where it runs; ACP no longer borrows Codex's location rule

The rule a self-supervised agent child runs under (this machine, no WSL, Windows only with process
start-time proof) is its own module that Codex and the ACP adapter both use. Grok's registration
takes the full account-home resolver signature, and D3's tests build hosts with the agent registry.

* fix(native-chat): Grok follows the ACP runtime's request contract and the managed process's close

A request the agent or a Stop cancels is answered with the agent's own cancelled reply by the code that
owns it (the runtime no longer answers a silent handler), so a Stop needs no separate decline pass. A
permission answer still being saved when the agent stopped waiting is reported unconfirmed, since the
protocol already answered it cancelled. Cancelling the agent's own turn is the plain cancel. Request
rows are matched under their generation-scoped ids. A refusal's reason comes from the dialect's wording
path. The child drops its own stderr tail and close policy for the managed process's, and a close
whose process tree was not proven gone is reported as the adapter contract asks.

* fix(native-chat): a Grok chat Orca already holds resumes without writing what Grok replays

A chat with a saved Grok session reattaches with session/resume where the agent offers it, else
session/load. Either way the call runs inside the translator's load window, so what Grok sends while
it reattaches (its saved exchange, a task the dead process left running, ended by the restart) opens
no turn and writes no row; only context usage reads on. A reply an Orca or Grok crash cut short is no
longer completed from Grok's saved history: it reads like a Claude or Codex chat's, with the existing
notice. The attach window also closes after a failed attach, and a created session that session/resume
reports missing is replaced like one session/load reports missing.

The replay reconciliation is removed: the lane no longer reads the journal, and D3's replayed-input
grammar test and completed-turn check in the assembler go with it.

* refactor(native-chat): a failed Grok reattach needs no window close of its own; its lane is replaced

* test(native-chat): D3's merged tests use the shipped declarations and the launch options main requires

* fix(native-chat): typecheck fallout of the base merges; any agent's empty chat is reusable

Main's idle-empty-chat lookup and launch join now take any registered agent, as the rest of the
launch path does. The refusal check moved into the prompt turns and the prompt-block conversion beside
the turns that send it, keeping both files in their line limit.

* fix(native-chat): a Grok Stop ends the process once Grok settles its turn; the next send resumes

Grok's session/cancel ends only the running turn: work it already moved to the background keeps
running and can begin a turn of its own after the person pressed Stop. Stop is now a session
boundary, as it is for Claude: the cancel answers open requests and lets Grok end the turn, the host
waits a bounded grace for that, then ends the process; the next send relaunches and resumes.
The adapter's own bounded close of a turn Grok began is gone. Its named-turn check stays: the host
ends the session unless the provider declines a Stop naming a turn that has since ended.

* test(native-chat): a Grok Stop ends the process only after Grok answered the cancel

* fix(native-chat): Steer on a Grok card cancels the running prompt, then sends it

A send that reached Grok while a prompt ran was held in the adapter until that turn ended: Steer
on a queued card took the card out of the host's editable queue and meant 'send after this turn'.
It now cancels the running prompt (session/cancel; the session stays) and sends as the next prompt
once Grok answers the cancel, as the common pattern does; a steer behind another cancels it in
turn, so the last one runs. The adapter holds a send only while that cancel lands, so its general
held-send queue and its holdsDispatch report are gone (every send it holds has its turn open in
the journal). An older client's mid-turn send takes the same path. capabilities.steering is
unchanged and still unread.

* refactor(native-chat): a close or Stop cancels a start through the acquire's own abort signal

The host owns the acquire it runs, so it now owns its cancellation: each attach's acquire gets an
AbortSignal, aborted from outside the session's queue by a close and by a Stop admitted now (the
same admission rule as before). The optional abandonStart adapter hook, the router's fan-out to
every adapter and the ACP adapter's session-keyed start map are gone; the ACP adapter keeps an
unkeyed set of starts only so quit can prove their children gone, and keeps a failed start's
unproven child until its exit is proven.
The hook also let a later close ask that child again. The host now does that from state it holds:
a close of a chat with no live child whose record still names an owner process with no death
evidence asks the adapter to release it. The answer is not recorded as proof (the lease probe
does that), so an owner pid an earlier Orca left is never killed or marked gone. Claude and Codex
ignore the signal and hold no such child; their release is a no-op (tested).

* fix(native-chat): a Grok crash that closes stdout before its exit still ends with Grok's last words

On macOS and Linux the agent's stdout ends before its exit is observed, with or without the
supervisor's EOF forwarding, so the connection's loss closed the journal first and its error text
became the session's ended reason, dropping Grok's stderr. The reason is now read at the proven
exit: the agent's last words when it left any, else why the connection closed. The failure already
carried them. Comments that assumed the exit comes first, that early frames past the cap refuse the
start, and that dispatch re-checks image support are corrected.

* fix(native-chat): nothing Grok sends while a held chat reattaches is written, marked as replay or not

The reattach window relied on the dialect's replay verdict, and Grok's frames read as live unless
they carry isReplay, so an unmarked chat frame during session/resume opened a turn that never
ended. D3 now marks every frame inside the window as replay before the translator reads it, so the
translator keeps only context usage whatever the agent marked; options and commands are still
adopted. The translator's load semantics are unchanged.

* test(native-chat): a Stop after a resume finds no turn an unmarked old reply opened

* test(native-chat): a resumed Grok chat keeps its last context reading; the resume refreshes only the window

* test(native-chat): a Grok background task a Stop ended reads as stopped reporting

* refactor(native-chat): quit's stop of each start answers through one promise kind

* fix(native-chat): quit aborts every start the host has in flight before draining attaches

A Grok that never answered its handshake held quit until the start's own 60 s bound, past the
20 s quit deadline. The host's teardown now aborts each in-flight acquire (and any the drain
still begins), so the adapter's own quit controller and its map of starts are gone: a start
has one canceller, the host's signal.

* fix(native-chat): a Grok start's abort stops reaching its child once the start has returned

The listener stayed on the host's signal until the attach finished committing, so a Close in that
window killed the now-live child behind the host's back and it read as Grok crashing. The start
now detaches it when it ends; a later Close goes through the session's own stop.

* fix(native-chat): a close or Stop during any attach phase stops the start before it launches

The attach began its abort controller only after reconciling leases, resolving recovery and
probing the previous owner, so a close or admitted Stop in those phases reached nothing and Grok
launched anyway. The controller now begins first, and the acquisition checks it before asking the
adapter to start.

* test(native-chat): a close during the attach's owner probe asks no adapter to start

Also renames the close test after the hook it no longer exercises.

* test(native-chat): a close's re-ask closes a Claude or Codex child a failed cleanup left

The re-ask is not a no-op for them: when the adapter still holds the child its cleanup could not
prove gone, the close stops it again as a requested close, and Claude persists the handle of the
conversation it ran so the next send resumes it. Corrects the tests' and comment's wording; the
close awaits the re-ask, bounded by each adapter's kill ladder.

* fix(native-chat): Steer during a turn Grok began itself cancels it and sends once it ends

A send while Grok ran a turn of its own (a background task waking it) went straight to Grok, which
queued it behind that turn where Orca could no longer withdraw it, while Stop treated the same turn
as the running reply. The send now waits as a steer, the turn is cancelled once, and the message
goes when the turn ends; a Stop withdraws it and an exit rejects it as never sent.

* test(native-chat): a steer whose cancel Grok never answers ends Grok and is rejected as never sent

Pins the bounded steer cancel kept from the runtime: past the bound the connection closes, the
running reply reads unverifiable, Grok's end reads as its exit, and the waiting steer is rejected
as never sent.

* fix(native-chat): a Grok crash stays a crash when a stop lands before its exit is proven

After the connection broke and the close could not prove Grok's exit, any later stop Orca asked
for (the next start, a Stop, a Close) marked the child as closed by Orca, so the crash read as a
requested close and Grok's last words were dropped; a send meanwhile was recorded unconfirmed.
The connection loss now decides the cause, and a send on that session is rejected as never sent.

* test(native-chat): fixtures this PR's registered Grok and desktop capability made stale

CI's unit shards failed on tests outside the PR's own lists. Each encodes something this PR changes
on purpose: Grok is now a registered agent (the seam test's unregistered agent is now Cursor); the
desktop now advertises registered agents (the restart-offer tests' older client drops that
capability explicitly); the attach context carries the start's abort controllers (the forget-status
double gains them); and the ACP real-host test rig sends to the host directly (listed beside the
other real-host rig in the send ratchet).

* fix(native-chat): a start quit stops is not the queued message's start failure

With quit now aborting a start it would have waited for, the delivery step recorded the aborted
start as the message's failure ("couldn't restart"). After quit has stopped delivery, the step
leaves the message to quit, which settles it as a close does ("The chat closed before this message
was sent."). The test that pinned quit waiting for that start and stopping its child now pins that
nothing is launched behind quit.

* fix(native-chat): a message sent after a Stop or close aborted a start gets its own start

A start the host aborts (an admitted Stop, a close, or quit) returned its refusal to the delivery
loop, which then rejected whatever was queued at that moment with "couldn't restart", including a
message the user sent after the Stop. The attach now reports that the host aborted it, and the loop
re-derives from the journal instead: what the Stop or close withdrew is already settled, a message
accepted since gets a start of its own, and quit's next step stops the loop. This replaces the
quit-only carve-out with the same rule for every abort and every agent.

* test(native-chat): the message sent after an aborted start is answered, so no settlement outlives the test

* fix(native-chat): a Grok model pick Grok never answers no longer holds Stop or Close

The pick runs on the session's queue. It now registers in the host's out-of-queue
abort registry beside a start, so a close, an admitted Stop or quit abandons it, and
the ACP adapter bounds it at 30 s like Claude and Codex. A late answer is still adopted.

* fix(agent-launch): a phone's launch opens a terminal for an agent whose chat it cannot show

agent.launch now reads the caller's capabilities by the rule tabs and restart offers
use (clientRendersStructuredAgent). A phone without registered-agents.v1 gets Grok as
a terminal again, as on main; the host's own callers and desktop clients are unchanged.

* fix(acp): strip every agent hook variable from the ACP child, from the shared list

ACP_CHILD_ENV_TO_DELETE was a second copy of the hook runtime keys that missed
ORCA_AGENT_HOOK_TRANSPORT; it now spreads AGENT_HOOK_RUNTIME_ENV_KEYS beside the pane
identity keys.

* refactor(native-chat): the mutation context carries the provider-wait registry itself

Keeps the host file within its line limit; one field instead of two closures over it.

* fix(agent-launch): agent.launch.v2 still vouches for Claude and Codex chats

The caller rule from the previous commit also turned Claude and Codex into terminals
for a client advertising only agent.launch.v2, whose contract says it opens a chat
(mobile retry-authority tests). Only an agent beyond those two now needs the client to
read it (clientRendersStructuredAgent); the test fixtures go back to what they were.

* refactor(native-chat): drop saved-history adoption from the timeline assembler

The common pattern discards the history a provider replays while loading a
session, so the assembler has no use for an input.history event.

* refactor(acp): drop session/load history adoption from the translator

The common pattern discards the history an agent replays during session/load,
keeping only what it says about the context window. Remove the adoption path
(acp-history-adoption.ts, the adopt option, and the historical background-task
liveness rewrite it fed) so load replay is always dropped except usage.

* refactor(native-chat): a pending input is only Orca's send now

Review follow-up to the adoption removal: drop the comment naming the
provider's saved message, and make requestedAt required since every pending
input comes from input.accepted.

* test(acp): keep the task-result status table on live frames

Review follow-up to the adoption removal: the result-status mapping was only
tested through adopted history, so run the same table on live frames, and
cover an unmarked task notice during a load being dropped.

* test(acp): a frame helper for a shell command Grok is running

* fix(acp): a Grok crash settles through the host's provider-exit batch, scoped to the turn it ended

A Grok crash ended the journal unverifiable before the adapter reported the exit, so the host's
provider-exit settlement found no running turn and wrote nothing: the adapter's failure (with
Grok's last words) never reached the journal, and a later stale-session pass wrote a bare,
thread-scoped cut-short row, so the partial reply was not folded as Claude's and Codex's are.

At a proven exit the ACP lane now ends its running turn interrupted at the exit instant, as the
host's exit contract expects of a child's own translator (Codex's does the same). When Grok's
stdout closed first (every POSIX crash), the turn is unverifiable only until the exit is proven:
the host's provider-exit settlement now takes the exit as proof naming the child's fence and
revises what that child left unverifiable in the same batch, with the turn-scoped row and the
adapter's failure. Claude and Codex write no unverifiable turn of a live child except a command
whose hand-off is in doubt; that turn is now revised at the exit instead of at the next open.

* test(acp): a crash seen first leaves the host no Grok turn to revise

* refactor(native-chat): what a gone generation left unfinished gets its own module

The settlement file passed 300 lines with the exit-proof revision. The unfinished-work reads
(capture, interrupted-by-the-exit, in-progress) are their own concept and move out unchanged,
apart from the exit proof they now take.

* refactor(native-chat): a watched exit revises what its child left unverifiable without reading Stop marks

An exit's own instant is the turn's end, so the revision needs only each row's fence: the
settlement's journal type gains itemFence alone, and the host test fakes say so.

* test(native-chat): drop the duplicate itemFence on the fake that already had one

* test(claude, codex): an exit whose stdout ended first still reports as it always did

The provider supervisor now ends Orca's stdout when the agent's ends, so on every crash EOF
arrives before the exit is seen. Claude's and Codex's connections report nothing at EOF and
report the exit, with its usual reason, once it is seen.

* fix(acp): reopen a chat with session/load, as the common pattern does

An agent that offers both now reloads its session instead of resuming it; the
reattach window still discards what it replays except context usage.

* fix(acp): drop the 60 s handshake bound; an abort fails the start's waits at once

Neither common design bounds an ACP handshake: Close, Stop and quit end a start
that never answers. The abort now also closes the connection, as a kill there
does, so the start settles even before the child's exit is proven. The
host-stopped start refusal only this bound produced goes with it; the idle
sweep keeps its words.

* fix(acp): a Stop naming an ended turn follows Claude's rule

It still stops nothing while another turn is live, but in the gap before a
follow-up's turn opens, which no client can name, it now stops what is in
flight and the session ends, as a Claude Stop does.

* fix(native-chat): a close no longer re-asks a failed start's unproven child

Neither common design retries that stop at Close, and Orca's Claude contract
re-asks only at the next start and at quit. The ACP adapter keeps the child
until its exit is proven and asks it again there, as Claude does.

* fix(acp): a message sent during a turn the agent began itself goes at once

Both common designs send it straight to the agent with no cancel; only Orca's
own running prompt is steered (cancelled, then re-prompted).

* test(native-chat): dismiss-all through a remote client's audience keeps a newer Orca's offer

Uses an audience production sends (one that cannot show every agent), per review.

* fix(acp): launch Grok as `grok agent stdio`, without the update and leader flags

The common pattern passes neither --no-auto-update, --no-leader nor
GROK_DISABLE_AUTOUPDATER; full access still adds --always-approve.

* fix(acp): an agent that ends its stdout, or answers unreadably, is not a lost connection

As in the common pattern, only a broken stdin (or Orca's own close) ends the
agent; one that closed its output but can still be written to stays until a
Stop, a close or its exit. The provider supervisor goes back to its base
content, so Claude and Codex no longer get the forwarded stdout end either.

* fix(native-chat): a person's close joining a failed one still binds the turn its child end cuts

On main every close of the chat writes its own Stop and settle. Here a later close joins the
first and writes no row, and the first's settle closed when its kill failed, so a turn that opened
in between and was cut by the next close read as failed. A person's close joining a person's close
whose Stop opened a settle now reopens that settle until its attempt is done.

Tests: a close whose kill failed still closes its settle; a turn opened between a failed close and
the next reads as the person's cancellation (each fails without its half of the fix).

* test(native-chat): Grok opens as a chat only behind the structured-chat setting

agent.launch and orchestration worker-start read the same setting as the
renderer route; pin both states for Grok on each. The setting's description no
longer names only Codex and Claude, in every catalog.

* docs(acp): generic ACP comments say what holds for every agent, not Grok

Stop ends the session for every ACP agent, as in the common pattern; the
adoption hook comment goes (adoption is not planned); a failed start's child is
retried at the next start or quit.

* test(claude, codex): type the EOF-before-exit test's streams; the supervisor no longer forwards EOF

The Claude test wrote to the child's stdout and stderr through their Readable
type, which the node typecheck rejects; it now holds its own PassThrough
streams. The comments no longer credit the reverted supervisor change.

* feat(acp): a steer's cancel asks once and never ends the agent

The runtime had one cancel: send session/cancel, wait at most 10 s for Orca's prompt to settle,
then close the connection, which ends the agent. A steer used it too, so a slow agent lost its
process just because the person added a message. requestSteerCancel() now sends session/cancel
once per prompt, cancels the agent's open requests and answers later permissions cancelled, and
never bounds or closes: the prompt's own reply ends it and the steer's prompt follows. cancel()
stays the Stop: bounded, then close. A Stop after a steer still bounds and closes. Both cancel
paths move into acp-prompt-cancel.ts over one cancel channel.

* chore(native-chat): keep the record store and recovery capsule under max-lines after the main merge

* fix(acp): a repeated steer shares the cancel in flight; say what the caller owns

Per review: a second steer before the first write lands returns that write instead of resolving
early. The steer's JSDoc says the wait for the prompt's reply is unbounded and that a prompt that
fails instead must not take the steer until the caller rebuilds the session; the Stop's says a
prompt that settles in time leaves the agent for the Stop's owner to end. The steer test now gives
the runtime a handler that would allow: the open permission's signal aborts and the late one never
reaches it.

* fix(native-chat): drop the stopDelivery the A3 merge doubled

* fix(acp): a steer's cancel asks Grok once and never ends it

A steer now uses D1's notify-only cancel. Two messages sent during a reply Grok began itself
cut that reply, as the common pattern does, and then both run; before, the queued first
message could not answer the bounded cancel and Orca ended Grok although Grok answered.
A Stop keeps the bounded cancel and its 4 s grace.

* fix(acp): a permission Grok asks with no prompt of Orca's running is declined

During a turn Grok began itself nobody asked it to act, so the request is answered
cancelled at once instead of opening a card that waits, as the common pattern does.

* fix(acp): a Grok that dies while starting is reported with its own last words

A dying process's stdout ends before its exit is seen, so the start failed as a closed
connection and Grok's stderr was lost. A start whose connection closed now waits, bounded by
the Stop grace (or a Close/Stop), for the exit before it is told.

* test: a Stop after a steer sends its own cancel; drop the import the A3 merge doubled

* test(native-chat): main's Stop-note test builds its turn context with the agent registry

* test(claude): say why the close test's fake child cast is safe

* test(native-chat): build the Stop-opened-turn test's identity and turn context the current way

The test (#25056) landed before the opaque provider handle (#24991), so main still built
the old {kind, threadId} handle; the turn context also needs this branch's agent registry.

* test(native-chat): build the Stop-opened-turn test's identity with the opaque handle

The test (#25056) landed before the opaque provider handle (#24991), so main still built
the old {kind, threadId} handle.

* test(ratchet): require src/main/provider-process now that it has landed

* test(native-chat): keep main's opaque-handle import in the Stop-opened-turn test

Main's #25706 and this branch both added the import at different lines; the merge kept both.

* test(native-chat): keep main's opaque-handle import in the Stop-opened-turn test

Main's #25706 made the same fix as this branch at a different line; the merge kept both imports.

* feat(native-chat): record a fresh provider conversation that replaced one the agent could not restore

A chat whose saved conversation the agent cannot reopen can now continue in a
fresh one: the handle chain records the new conversation as a creation that
replaces the lost one (which, why, and when), keeping every earlier link.

Rows keep a shape older builds read: the stored chain starts at the latest
replacement and carries the earlier links inside it.

* test(native-chat): build this stack's journal identities with main's opaque provider handle

Main's #24991 replaced the {kind, ...} handle with {transport, agent, nativeId}; three test
files from this stack still wrote the old shape. Same lines the downstream ACP branch uses.

* docs(acp): every reattach drops the agent's replay, not only for a chat the journal holds

* refactor(native-chat): store a replaced conversation flat; refuse it where older builds read the row

Older builds only read Claude and Codex records, so the nested stored form
protected rows no replacement can reach while adding a cap mismatch after a
downgrade. Store the chain as held, refuse a replacement in a Claude or Codex
chain until one has a stored shape older builds read, and refuse a supersession
key on a replacement that names no creation in the chain.

* refactor(native-chat): read hosts' structured agents from the app-shell services

Main grew the startup hydration hook to its line limit; the host agents sync is an app-lifetime subscription like the structured session tabs sync beside it, so it moves there.

* Use current provider handles in transition tests

* Use current provider handles in timeline fixtures

* test(native-chat): prove replacement rows survive downgrade and re-upgrade

* Require the ACP directory in the runtime import check

* test(ratchet): require src/main/acp now that this PR lands it

* feat(acp): a saved session the agent cannot reopen continues in a new one, with one warning row

When session/load (or session/resume) of a saved ACP session fails, the chat starts a new session and records it as a creation that replaces the lost one (#25747's 'replaces' link), and writes one warning row that the agent no longer remembers the earlier messages. A created session the agent reports missing is still superseded silently; a signed-out agent or a start that is over (Close, Stop, a lost agent) still fails the start.

* chore(acp): rewrap the acquire header comment

* test(acp): a start closed while the agent reopens fails without opening or announcing a new session

* Let ACP connections own their supervised agent process

* Preserve ACP cleanup evidence and isolate exit observers

* Expose ACP cleanup observations and type the permission fixture

* refactor(native-chat): the registered-agents capability lives in its own module

Main's growth put protocol-version.ts one counted line over its 300-line limit once the capability
was added; like main's other per-feature capabilities, it now has its own module, and importers read
it from there.

* refactor(acp): one connection owns the Grok process and its protocol

D3 now opens each ACP agent through createAcpAgentConnection (ACP-ALIGN #25810): one object spawns the
process on the execution host, owns its stdio and protocol, and reports its proven exit. It is built and
tracked before the handshake, so a start's abort (Close, Stop, quit) still reaches it, and a failed start
keeps that same connection for the next close to retry rather than spawning another process.

Deleted: the spawnAcpStructuredChild wrapper and its test, the raw-stream runtime assembly, the caller's
exit -> runtime.close wiring, the stdout-EOF heuristic (the connection no longer treats stdout EOF as
exit), and the 10 s steer/Stop cancel bound with requestSteerCancel. Reader control maps to
pauseReading/resumeReading; a close is connection.close after the host's existing 4 s Stop grace.

The adapter owns what the protocol no longer does: one session/cancel per running prompt however many
steers arrive (cleared with that send's settlement, retried after a failed write), and a Stop or steer
answers every open agent request the person has not already answered with the agent's own cancelled
reply. An answer already being saved when the Stop lands is sent.

Tests: blocked cancel write never holds Stop's grace, two quick steers send one cancel, a failed cancel
write is retried, a real process exiting while a child holds its stdout ends the session, and the
existing start-abort, retention, crash, connection-loss and reload-failure suites on the new rig.

* fix(acp): Grok signs in on its own machine with its API key or cached sign-in

When Grok reports that it needs authentication, Orca now names a sign-in method on the machine Grok runs
on, read from the same environment Grok was launched with: xai.api_key when XAI_API_KEY is set there and
Grok offers that method, else cached_token when Grok offers it, else none and the chat keeps the existing
not-signed-in refusal. The rule lives in Grok's launch spec; the adapter applies any agent's rule for new
and reopened sessions through the protocol client's caller-named method (authenticate, then retry once).
No new sign-in UI; interactive methods are never chosen.

* fix(acp): the adapter decides which of Grok's requests reach the person

The turn owner now admits every agent request, permission or question, from its own turn state: a
request reaches the person only while Orca's prompt runs and no steer or Stop is cutting it short (a
question may also come from a turn Grok began itself, until a Stop). Anything else gets the agent's
own cancelled reply and opens no card, so a question arriving after Stop or during a steer never
appears. A steer, like a Stop, withdraws the requests already open; an answer already being saved is
still sent. The protocol client's abort-on-cancel path is no longer used: after the connection
change its request signal aborts only when the connection closes.

* fix(acp): a plan Grok proposes shows as a plan, with no approval card

When Grok leaves plan mode it asks the client to approve its plan (x.ai/exit_plan_mode). Orca showed a
blocking 'Approve plan / Request changes' card for it; the common pattern has no such gate. Now the
plan goes into the chat's existing Plan row (the plan-document status row Codex and ACP plan updates
already use) and the request is answered at once with 'abandoned' plus feedback telling Grok to stop and
wait for the person's feedback or a request to implement it in a later turn, so nothing is approved on
the person's behalf. Dialects gain settleRequest for requests answered without asking anyone.

* fix(orchestration): worker-start opens a Grok worker in a terminal, as before

With the structured chat setting on, worker-start decided 'structured' for Grok and then the structured
worker factory (Claude and Codex only) refused it, so the start failed; main opened a terminal Grok
worker. Worker-start now decides with no registered agents beyond Claude and Codex, so Grok gets a
terminal worker as before. agent.launch and the app's own launches still open Grok as a structured
chat. Temporary until structured workers take registered agents.

* fix(acp): a prompt answer Orca can't read ends the turn instead of hanging it

A session/prompt rejection that was not the agent's own error answer (an answer that fails Orca's
schema, or one too large to read) left the turn running: the next message became a steer with nothing
to cancel and was never sent or settled, and Stop waited its full grace. As in the common pattern, any
prompt failure now ends the turn as failed (a failed-turn row without words, since none are the
agent's) and settles the send, so the next message goes. Only a closed connection keeps the send
running, for the connection-loss path to settle.

* fix(acp): send Grok's prompt-identity extension only to agents that echo it

session/prompt carried _meta {promptId, requestId} for every ACP agent, though only Grok's dialect
echoes it (injectedPromptIdentity). Now only an agent whose dialect declares it gets the extension;
other ACP agents get a plain prompt.

* refactor(native-chat): the registered-agents capability lives in protocol-version again, as on main

This reverts 0ef6d21. That commit moved the capability to its own module only because main's
protocol-version.ts was then one counted line over its limit; main now defines it there itself within
the limit, and main's new restart test imports it from there. Main's test also reads the desktop's
capability list as an older client; on this branch the desktop advertises registered agents, so its
older client is that list without this one capability.

* test(acp): read the sign-in method with a schema, not a type assertion

* refactor(native-chat): composer transport and Stop control in their own modules

Main's rewind change (#19338) brought NativeChatStructuredSession.tsx and use-structured-agent-session.ts
to their line limits, leaving no room for this branch's image-acceptance and unpublished-Stop lines.
The composer's transport (sends, commands, options, image acceptance) moves to
use-native-chat-structured-composer-transport.ts, and whether Stop shows and what it does moves to
structured-agent-session-stop-control.ts. Behavior is unchanged; the runtime cast on the composer's
'local' | 'remote' is now a typed return.

* test(native-chat): read registered agents by id, as main's structuredAgentsReadBy now takes

Main's A3 squash changed structuredAgentsReadBy to take agent ids; this branch's test still passed
{ agent } objects (CI typecheck TS2322).

* fix(acp): a first reopen warns when Grok forgets a chat that exchanged turns

A Grok session the chat created was treated as one Grok never saved, so
when Grok reported it missing on the chat's first reopen, Orca swapped in
a fresh session silently, even after completed exchanges: the person saw
the old messages while Grok had forgotten them.

The launch now counts a created session as never saved only when the
chat's journal, read at the failed reopen, holds no turn of that Grok
session; anything else, an unreadable journal included, takes the normal
path: the fresh session is recorded as replacing the old one and the one
warning row is written.

* fix(acp): a start writes the warning row an earlier attach failure dropped

The row saying Grok forgot the chat was written only into the attach's
deferred sink, while the fresh session's link was saved earlier. An attach
failure, quit or crash in between dropped the row forever.

Every start now derives the owed rows: each conversation the chain says was
lost to a failed restore gets its row unless the chat's journal already holds
it. The row now names the lost conversation rather than the fresh session, so
a row an unused replacement wrote still counts after Grok supersedes it.

* fix(acp): a question during a turn the agent began itself gets its cancelled reply

A question or other card-opening request the agent sends while no prompt of
Orca's runs (a turn it began itself, as when a background task wakes it) now
gets the agent's own cancelled reply and opens no card, the same rule
permissions already follow there. Nobody is waiting on that turn. A plan the
agent shares in it is still shown.

* test(native-chat): import the unfinished-work capture from the module that owns it

Main's reasoning sweep test (#19221) imported it from the dead-generation settlement, which
this branch split it out of.

* refactor(native-chat): keep the session host within its line budget after main's Stop work

Main's #25949 left the host at exactly its 300-line budget, and this branch's acquire-abort wiring
adds one line. The reveal module now comes in as a namespace import, as the host already does for
its other helper modules, and the earlier reorder of two type imports is undone.

* test(codex): move the stdout-before-exit test into its own file

Main's connection test file is at its 800-line test budget, and this branch's exit-order test
pushed it over (CI lint, max-lines).

* fix(acp): cancel a running turn before a close, dispose or quit ends the agent

Closing a tab, disposing a session or quitting while an ACP agent's turn ran
killed the process without asking the agent to cancel first. A requested
close now does what a Stop does: withdraw the agent's open requests and held
steers, send session/cancel once, and wait for the turn to end, bounded by
the Stop's grace (4 s), before closing the process. A close that follows a
Stop sends no second cancel and waits only what is left of that Stop's grace.
An idle close, a lost connection and a sink-failure force close are
unchanged. The cancel-and-wait moves to acp-structured-stop.ts, shared by
Stop and close.

* test(codex): pass resolveLaunchArgs in the stopped-send-order test so typecheck passes

Main's typecheck fails here too: #25721 made resolveLaunchArgs required and
this test (#25051) predates it. Same line, same place as the open main fix
(#25977), so merging main after it lands is a no-op.

* test(native-chat): run the close-aborts-start test on the Node runtime, as its SQLite journal fixture requires

* refactor(native-chat): the Stop control reads the outbox itself, so the chat hook stays within its line limit

* Keep the session host under the line cap after main's two new delegates

Pass the lifetime's conversation opener to reveal directly; it is already passed unbound to the mutation context.
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
…er (#25146)

* feat(native-chat): a paired server keeps a store for chat attachments

A structured chat on a paired Orca server had nowhere to put a file the client
attached. The server now keeps a per-chat attachment store under its userData,
filled through agentSessionAttachment.uploadStart/Append/Commit/Abort and read
back for previews through agentSessionAttachment.read, behind the structured
session gate and advertised as agent-session.attachments.v1.

Nothing records cleanup as owed: a sweep re-derives what may go from the host's
chat records and journal (unfinished uploads after an hour, uploads for a chat
that never existed or that its journal never mentions after a day). The clipboard
RPC's in-flight bookkeeping moves into a shared ChunkedUploadRegistry both use.

* feat(native-chat): upload attached files into a paired server's chat store

Main streams dropped or picked files (fs:uploadPathsToAgentSessionAttachments)
and pasted images (clipboard:saveImageAsTempFile with agentSessionAttachment)
into the chat's store on its paired server, reusing the file-import slice
streamer and pinning every call to the pairing revision and server process.
The browser client's paste takes the same route.

* feat(native-chat): attach files to a structured chat on a paired server

Pasted images, files dropped from Finder/Explorer and the file picker no longer
refuse with "Local attachments are not available for remote sessions." in a
structured chat on a paired server: they upload into that server's chat store and
the chat gets only the stored path. Dropped files show as pending chips at once
so Send waits for them; every attached item carries the server, pairing and chat
it was stored for, and a send to anywhere else drops it with the existing
"changed hosts" notice. Chips and transcript images read stored files back
through the server, never from this machine's disk. An older server gets an
update notice instead of an upload. The terminal-backed chat keeps refusing.

* test(native-chat): type the attachment test doubles without bare casts

* refactor(native-chat): keep the clipboard RPC on its own upload bookkeeping

The chunked upload registry stays for the chat attachment store only, beside it.

* feat(native-chat): claim chat attachments when the host admits a message

A message's references into the host's attachment store are claimed in the same
journal transaction that makes it durable (a direct send, a queued draft, a /clear
carry). The sweep deletes an upload only after marking it in that database while
no claim exists, so a send and a sweep can never both win: a client's send that
names an expired or foreign upload is refused whole with the new attachmentExpired
reason, before anything is recorded. This replaces scanning chat transcripts for
paths.

The store is now <root>/<upload id>/<name>, refuses uploads for chats the host
does not hold, caps stored names in UTF-8 bytes and avoids Windows device names.
The preview read goes through the protected bounded read and the request's reply
budget, so an image too large for one reply is refused instead of closing the
connection.

* feat(native-chat): let structured Claude read the host's chat attachment store

Attached non-image files live outside the workspace; the store root is passed as an
added directory so the agent reads them without asking, the common pattern.

* fix(native-chat): skip a dropped folder before staging walks into it

* fix(native-chat): pin the browser client's chat paste to its server

Every upload call goes to the environment the paste was meant for and checks its
pairing and server process, as the desktop upload does; a re-pair ends the upload
instead of storing the image on another server.

* refactor(native-chat): let the host's claim be the only attachment check

The composer no longer records which server and pairing each attachment came from,
and Send no longer strips attachments it judges foreign: the host refuses an
expired or unknown stored path when it admits the message, which also covers
retries, Stop-restores and queued edits the client check missed. Previews of
stored files route through the chat's own server by path.

Removing a file's chip while it uploads now keeps its @path out of the draft. A
dropped or picked file shows its name and kind on its chip while it uploads, with
no separate progress toast, and files that did not attach are named in one notice.
A rich-text paste into a chat on an older server no longer shows the update notice
beside the pasted text.

* chore(native-chat): keep the claim hook beside the draft consume and fit the line budgets

The submission's claim runs in the same append hook as a draft's consume, owned by
the queued-message collaborator, so the journal store stays within its size limit.
Formats the new files and regenerates the runtime-required English catalog.

* test(native-chat): pin the attachment store grant in the Claude launch, restore upload result types

* fix(native-chat): create the attachment store at install and claim only exact store paths

Claude drops an added directory that does not exist when it starts, so the store
root is created when the host installs it, best effort. A commit keeps its upload
in flight until the rename lands, so a sweep never takes a slow upload's part file.
Only a path under this host's exact store root is claimed and required; any other
mention of a store path is plain text and never refuses the message.

* refactor(native-chat): reuse the newer-Orca notice and drop leftover attach options

An older server's refusal to store attachments uses the notice every other write
it refuses already shows, so one string fewer in every catalog; the attach
callbacks lose an options parameter no caller passes since the provenance check
went.

* fix(native-chat): give a message refused for an expired attachment back to the composer

Sending the same message again can never bring the attachment back, so instead of
a Retry that always fails the message's text and images return to the composer,
as a Stop's withdrawn message does, and the notice says what to remove.

* fix(native-chat): keep uploads across a prompt, say why a file did not attach

An upload that finishes while a prompt card has replaced the composer lands in the
scope's attachment and draft caches, which the composer reads back when it
returns, instead of the unmounted composer. The single failure notice names the
cause the files share, such as the size limit. Rich text pasted into a chat on an
older server no longer flashes an image chip: the chip waits for the server to
take the image. A picked command waits, as Send does, while an attachment is still
uploading.

* fix(native-chat): keep a paste's upload across a prompt; pin the Claude grant hand-off

A paste uploading into a paired server's store keeps its chip live when a prompt
card unmounts the composer, and its result is filed for the composer's return,
as a drop's is. A failure cause ending in full-width punctuation gets no extra
full stop. A runtime test pins that the store root reaches Claude's launch
resolver through the adapter.

* test(native-chat): type the grant hand-off captures without casts

* test(native-chat): pin that the composer files a paste's upload across a prompt

* fix(native-chat): retry a held rename at commit and keep cut names Windows-safe

A commit renames the part file with the Windows retry the app uses elsewhere, so
an antivirus or indexer holding it briefly no longer loses the upload. A name cut
to the byte limit is stripped of trailing dots and spaces again. The upload
pump's result cast states why it holds.

* fix(native-chat): keep attachments still on their way in the pane's attachment cache

A prompt card unmounts the composer, and an attachment still saving or uploading
lived only in that composer: one that came back showed no pending chip, so Send
went out without the file and the file then landed in the next draft. Pending
chips now live in the pane's attachment cache beside settled ones and settle
there whichever composer is showing, so Send waits for them after a remount too.
A stored file's @path goes into the pane's draft cache, which keeps it through an
input-method composition and a remount. A rich-text paste's image is registered
as a hidden pending chip before the server is asked, so Send waits for it from
the start, and it shows only once the server takes it.

* test(native-chat): a dropped file still uploading stays pending across a remount

* fix(native-chat): reveal a rich-text image in a composer that came back; keep caret inserts

A rich-text paste's held image is revealed through the pane's attachment cache,
so a composer a prompt card remounted during the server's answer shows it rather
than holding Send on an invisible chip. A stored file's @path goes in at the
caret while the composer is showing and not composing, as every other attach
does; only mid-composition or after a remount does it go to the pane's draft.

* fix(native-chat): never evict a pane's attachments while a composer shows them or one is on its way

The pane attachment cache is now where chips live, so its 128-scope bound passes
over a scope a mounted composer subscribes to or that still holds a pending
attachment, and evicts the oldest scope nobody uses instead. Protected scopes are
bounded by mounted composers and attachments in flight.

* fix(native-chat): never evict the scope just written when every older one is in use

* fix(native-chat): keep the attachment store out of the RPC method table's imports

The method table is imported far and wide (the SSH relay's CLI included), and the
attachment RPCs pulled in the store, whose preview read loads modules that read
fs constants at load: any test that mocks fs/promises without them failed to
import. The installed store now lives in a small registry module; only the host
wiring loads the store itself.

* refactor(native-chat): the submission hook gets its own module

Merging main added the operation receipt to the queued-message insert, which
put journal-queued-messages.ts over the 300-line limit. The submission hook
only uses the queued-messages object's public methods, so it moves out.

* refactor(native-chat): fit the attachment sweep and paste upload into main's line budgets

After merging main, the record store and the clipboard handlers each ran
a few lines over the 300-line budget. The sweep now asks the record store
whether a chat is recorded through the two lookups it already has
(readable or unreadable) instead of a new id-listing method, and the
paired-server paste upload lives beside the other attachment uploads.
No behavior changes.

* fix(native-chat): pending chips sit beside main's saved draft, which keeps server-stored images

Main now saves a composer's draft (text and settled images) in one store
that survives a reload or quit. This branch kept every chip, settled or
not, in its own pane cache. After the merge the saved draft owns settled
images and the pane cache holds only chips still on their way: they come
back pending in a composer a prompt card remounted, hold Send, settle into
the saved draft whichever composer is showing, and are never saved
themselves, so a restored draft cannot bring back an upload as if it were
attached.

An image a paired server stored for the chat is saved with the draft as
the real image (a pasted one is no longer turned into a "Not kept"
placeholder), and the restore check leaves it to that server, whose claim
at Send refuses one it no longer holds, instead of asking this machine's
disk for a path that only exists on the server.

Also: previews and the pending subscription move into their own small
hooks to fit the line budget, the "local attachments" notice moves to the
composer-target module so the attachments hook no longer pulls in the
upload module's imports, and tests follow main's new mocks.

* test(native-chat): give the claims test main's provider handle and message source

* fix(native-chat): a paste still uploading dies with its tab or workspace

A pasted image still uploading to a paired server sits in the pending
chip cache, which by design outlives the composer and its tab. Removing
the workspace deleted its saved drafts but left those chips, so the
upload finishing afterwards recreated and saved the deleted draft. With
the workspace's tabs gone it had no owner either, so no later workspace
cleanup could ever remove it.

A pending chip now records the owner its draft would have, resolved the
way the draft store resolves one when the chip is added. Workspace
removal and a user's tab close drop pending chips by the same owner,
conversation and tab matches they delete drafts by, and a chip that
settles after its chat's tab closed writes its draft under the owner it
was begun in.

* fix(native-chat): the claim type uses the journal's own SQLite type

* refactor(native-chat): read the pending-image handlers off the composer's attachments

Main's multi-file picker (#23956) and this branch together took NativeChatComposer over the 400-line limit; reading the two pending-image handlers the way the neighbouring ones already are keeps it at the limit.

* test(native-chat): pass the launch-args resolver main now requires in the attachment grant test

* fix(native-chat): grant the attachment store beside folders the saved Claude Arguments add

Main (#25721) now builds additionalDirectories from the user's --add-dir arguments; this branch's attachment-store grant replaced that list instead of adding to it. Both are granted now. Moves the Claude session-id derivation to its own module to keep the resolver under the line limit.

* test(native-chat): run the attachment store's SQLite tests in the Node runtime project

* test(native-chat): follow main's attach ownership recheck (#25749) in the attachment tests

* test(native-chat): a named upload chip still shows when the agent takes no images

* fix(native-chat): a paired-server image paste failure keeps its error apart, as main's notice card does

* fix(native-chat): the attachment sweep reads the chat list directly now that records have no import still owed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant