Skip to content

Save a new chat's first message during create - #26332

Merged
brennanb2025 merged 61 commits into
mainfrom
brennanb2025/chat-start-stop-standard
Oct 9, 2026
Merged

brennanb2025 merged 61 commits into
mainfrom
brennanb2025/chat-start-stop-standard

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 22 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1856 $\color{#cf222e}{\Huge{\mathbf{−}}}$​26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1830
Prod 54 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1849 $\color{#cf222e}{\Huge{\mathbf{−}}}$​625 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1224

ELI5

Starting a new chat used to leave its first message in the window while the agent started. Stop put that text back in the composer but let startup continue. Now the host saves the chat and its first message together: Send stays busy until that save is confirmed, then Stop cancels startup and leaves the sent message in the conversation with the existing stopped marker.

What Changed

Create accepts a first message and the options picked before the request, with the same bounded message body and identity used by send. The host commits the chat, tab, message and create result together through the existing submission writer, then the existing delivery loop starts the agent. A blank modern chat stays at rest until work needs an agent.

Adopted conversations keep their saved history and original provider identity before startup; a rolled-back create cannot leak partial imported history into a fresh retry. The create reply feeds the existing chat reader immediately, so Stop can work while the agent is starting without waiting for a separate history read. A create refusal or 30-second deadline returns the text once after any draft already typed, using the existing notice. Retry, reload and reconnect preserve the exact create payload and message identity. Capability changes also preserve the contract recorded by an earlier create, and an unanswered Retry or automatic message confirmation becomes actionable after 30 seconds without returning the same text twice; a late reply never sends the opening message separately.

Why

The bug came from splitting chat creation and ownership of the first message across the host and window. Saving them together removes that gap and reuses the host's existing delivery and Stop behavior. An extra renderer Stop guard would still leave the message outside the host while startup ran.

Linked Issue

Continues #25959 and the October 7 native-chat start/Stop decision requested by @BrennanKB5.

Visual Proof

Hidden macOS Electron QA on synchronized product commit b2e9a72, rebuilt from that source with a protocol-capable Codex stand-in and delayed initialization. Final commit 007eca4 changes one test fixture only; product source is identical to this QA commit.

The host has saved the opening message and Stop is available while the agent starts.

Opening message saved with Stop available during startup

After Stop, the bubble stays in the conversation with the existing stopped row (it read “Stopped before the agent started” when this was captured; #26381 has since renamed it “Stopped manually” on main); the composer is empty and the sidebar is idle. The stand-in never received this first text.

Stopped opening message remains in the conversation

Only the distinct next message is delivered once, followed by the stand-in reply. The stopped first bubble remains above it.

Only the distinct subsequent message is delivered

A blank chat stays at rest with the empty composer and no startup/error state.

Blank Codex chat remains at rest

UNVERIFIED: held create-response rendering before confirmation, rendered refusal and Retry, reload and older-host rendering, Claude/ACP startup boundaries, live mobile/SSH/WSL/Linux/Windows behavior, and complete profile hash equality including after teardown. These narrower UI gaps are not counted as rendered passes. An existing isolated-profile Codex server toast appeared; no new first-message lifecycle notice was introduced.

The rig used isolated HOME, CODEX_HOME, CLAUDE_CONFIG_DIR and user-data storage, an absolute stand-in and mock keychain. Full-profile hash snapshots are incomplete: missing rows are unobserved, not evidence of removal, and the observed sample cannot establish which roots changed or attribute a change to QA. No real profile contents were opened, copied, edited or restored. The driver stopped its recorded launch tree and closed its ports; read-only hash commands may finish independently. Artifacts initially written to an untracked worktree qa/ directory were moved to lane scratch at teardown and were never staged.

Testing

Second main sync (c1a8722, after review): main had moved 36 commits and conflicted in one file, structured-agent-session.ts. Main's per-agent create checks from #25851 (a create now names its agent for the audience check, the host install and the attach support check) were carried into structured-agent-session-create-handler.ts, where this PR moved that code. The merge also took protocol-version.ts over its 300-line limit, so the two create capabilities (tab id and first message) now live in their own agent-session-create-capabilities.ts, the same way the Stop capabilities do; the advertised list is unchanged. After the sync, web, Node and CLI typechecks pass; 265 tests in the PR's 21 affected files, plus the phone wire-type test, pass; pre-push checks pass 21 of 22, and the only failure is the five phone import-cycle warnings that main also has. No product behaviour changed in this sync, so hidden QA was not repeated.

  • I manually tested these changes locally (hidden macOS QA with a Codex stand-in)
  • Automated tests added/updated

The final synchronized source passes queued web, Node, CLI, mobile production and mobile test-ratchet typechecks after removing stale build metadata. The actual synchronized commit b2e9a72 passes 470 tests across 48 explicit files through the CI Bun/Node coordinator, including actual-HEAD cross-version wire. Final commit 007eca4 changes one test fixture only and passes 199 focused tests in eight explicit files. Removing its empty operation lookup reproduces the same two CI assertion failures; exact-byte restoration passes all nine refusal cases. The original assertions remain, and both affected cases now also prove attach was reached exactly once. A source inventory plus traces of real create-preparation callers found no other incomplete operation-ledger fixture. The synchronized product source has passed the partial hidden QA shown above. All 37 final-head checks have finished: 21 passed and 16 were skipped, with no red CI check or new failure. The ten desktop shards, relay integration, wire compatibility, static analysis/typechecks, mobile verification/bundle, both packages and final verification passed. Final desktop CI run. Earlier regression suites overlap and are not added together: 323 launch/shared/RPC tests, 129 and 70 focused host tests, 176 corrective root tests, 38 mobile tests and 268 affected Bun/Node tests passed. The 54 historical production safeguard reversals failed through assertions, import failures or compiler diagnostics and restored the saved bytes; some snapshots predate later corrections. Three wire-fixture reversals, the final precommit fixture reversal and four test-routing reversals are recorded separately.

CI exposed three new host fixture suites assigned to Bun despite needing Node SQLite. Extending the existing fixture dependency guard also found five existing suites with the same routing problem. Those unchanged five suites pass explicitly under Node (49 tests); the pinned main source executes and fails all 49 under its configured Bun project because node:sqlite is unavailable. They are now assigned to Node. This is a reproduced main-source routing failure, not a claim that historical CI skipped or never ran those suites.

Pre-push static failures inherited from synchronized main are recorded separately: the cross-version surface manifest has 304 counted lines and is byte-identical to main 2460068 (SHA256 aa9552cdcc26cd5d9b0962279c753220c26b97b83af72898e71e56c3ed7edffc); the five mobile dependency-cycle warnings reproduce on a main-source baseline with the same installed dependencies, and their sources and audit configuration are unchanged from synchronized main. The pre-push checker passes 20 of 22 checks, with only those two inherited failures. The earlier synchronized CI run found two owned assertions blocked by an incomplete fake-host ledger, followed by its dependent verification failure. The test-only correction preserves those assertions and restores the empty operation lookup. Final-head shard 5/10 now passes, as do all other executed final CI checks; the GitHub watcher's TLS transport interruption was resolved by resuming the same run and is not a red check.

Tests ran on macOS with stand-in agents. Live Linux, Windows, WSL, native mobile and SSH execution remain unverified. Skipped checks are not passes.

Review

The first review found create replay could change its recorded contract when capabilities changed and an unanswered Retry could remain pending forever. Both are fixed with passing regressions and reversal proofs. Round two found no outstanding proven source/contract defect and independently passed 75 original-head and 44 corrective tests. CI then exposed an overbroad user-message type, a file-length error, incomplete fake host methods and four store initialization cycles; those corrections derive the type from the existing wire, keep strict old/new create assertions and reuse the narrower error and reader-registry modules. Recovery is loaded only when publication needs it.

Round three reviewed the seven-file import/wire correction, found no proven defect and independently passed 78 tests in five files. Its parsed 1,201-module graph found no static path back into the store through the four corrected modules. Packaged module-loading faults were not injected. The coordinator approved preservation of main's changes during the single final synchronization; Round four independently reviewed the final two test-routing files, preserved all prior catalog entries and runtime/type-only distinctions, and passed all 101 tests across eleven files with no defect found. The coordinator personally reviewed final test-only commit 007eca4 and approved its empty-ledger model, accurate safety comment and stricter attach assertions; no production changed, so b2e QA still covers the product. Final hidden QA partially passes for the Codex startup/Stop/distinct-next-message/blank-chat scenarios shown above; other flows and full-profile equality remain unverified.

Agent skill upstream boundary

  • Not applicable; no agent skill source or registry changed.

Notes

Differences from the common pattern

  • Temporary: Hosts without the new create-message capability retain create-then-send and the older unpublished Stop behavior. Remove this fallback when the supported host version floor includes the capability.
  • Intended: New hosts keep acquired-create for older clients, whose existing calls rely on it. This preserves mixed-version compatibility.
  • Temporary: Mobile, agent launch and orchestration calls retain their existing acquired-create paths. The follow-up PR “Record opening messages during create for every chat caller” will be opened from this lane after this PR merges. It will move those three callers to the same create-message contract, including capability negotiation, held options and host Stop during startup.
  • Intended: An option changed after create is issued uses the normal option write without holding publication behind agent startup. It may take effect after the opening turn begins; choices made before create are saved with that request.

Execution remains owned by the selected local or remote host. Folder workspaces and WSL retain their existing launch identities. Live Windows, Linux and remote execution remain unverified; the protocol and lifecycle checks above cover their contracts only.

Author: Brennan Benson (@BrennanKB5).

Checklist

  • This PR is focused on the new-chat first-message lifecycle
  • I explained what changed and why
  • Before/after screenshots or videos attached
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote and backward compatibility considered
  • Required local checks completed and final CI classified; inherited local failures are documented above

…aved outbox

The desktop kept each structured chat's unsent messages in localStorage and
sent them in order, so one message whose fate was unknown froze every later
send, Retry dropped it silently, failed sends could not be discarded, and an
offline chat could send hours later. The host already records every message
and owns the queue; the window now only sends.

- One in-memory sender for composer, launch prompts and messages sent from
  outside the chat. One send in flight per chat; a transport failure resends
  the same id for up to 30 s; a refusal that proves nothing was recorded puts
  the text back in the composer with the reason; a send that went out and was
  never answered shows an in-doubt line with Send again and holds nothing up.
- The host's "unknown" rows get the same in-doubt line and Send again, from
  the journal, in every window.
- A message an older build left in localStorage is never sent: the host's
  conversation outline decides what goes back to the composer, and the copy is
  deleted once that is saved.
…its timeouts

The sender called the runtime RPC directly, skipping the per-method timeouts
every other structured chat call gets. Only a remote host's request skips the
compatibility check the sender already ran.
… no new row line

The common pattern draws nothing extra on a message whose delivery is in
doubt once its turn is over, and puts a failed send's text back in the
composer with the reason. So a send nobody answered in time, or one a host
answers in a way that proves nothing, goes back to the composer worded as
unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt
phase, Send again, and its strings.

A host's made-up row for an id its journal lost now reads as unconfirmed, not
as recorded, so that message comes back instead of vanishing.
…send-queue

# Conflicts:
#	src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx
#	src/renderer/src/components/native-chat/NativeChatMessageRow.tsx
#	src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.probe.test.tsx
#	src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx
#	src/renderer/src/components/native-chat/structured-agent-session-withdrawn-message-restore.ts
#	src/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsx
#	src/renderer/src/components/native-chat/use-structured-agent-session.ts
…p, and keep remote images

- A normal tab close lets sends on their way settle; what the host never took
  goes back to the conversation's draft. Only a cancelled launch or a worktree
  purge drops them.
- After any Stop, a send already on its way is never sent again under its id:
  a doubtful answer, or a resend that was due, hands its text back worded as
  unconfirmed.
- A returned image keeps the SSH connection it lives on; the connection never
  goes to the host.
- An older build's saved message the host recorded and then rejected is left
  to the host's own row, never handed back.
…out, and a kept card is the card's

A Stop that landed while a same-id resend was being readied (its timer
fired, its request not out yet) let that resend go out after the Stop.
The sender now tracks whether a request is awaiting its answer: a Stop
hands back every send between attempts as unconfirmed, lets one whose
request is out settle from its answer, and never issues a request after
it. A normal tab close withdraws the same way instead of doing nothing,
so nothing more goes out and nothing is dropped.

A send the host rejected but kept as a card (keptAsQueuedMessageId) is
the card's, from its reply or the journal, and never goes back to the
composer.

Adds the freeze tests: a send whose fate is unknown holds later sends
no longer than its deadline, and one the host can neither confirm nor
deny releases the next at once.
…proven

Ports the send-answer proof contract. A call the host turned away before
running it (method_not_found, invalid_argument, unauthorized) proves only
that this request wrote nothing, so it reads as never sent on a first
attempt only; on a resend an earlier attempt may have landed, and it goes
again under the same id. A resent id the host says was already used for
other content (messageIdReused) proves nothing either, like an expired or
conflicting id.

Pins the rest of the contract: any row the host returns is its own, a
thrown error is no answer whatever its code, and an older build's
refused, held or outlived-Stop copy is handed back once and never sent.
…ttachment type

Keeps NativeChatStructuredSession.tsx within the file length limit.
A kept send's row is a rejection that was never a Stop's, so the sender
already reads it as recorded, from its reply or the journal. The test
that pins it stays; the two extra checks only covered a kept row that is
also a withdrawal, which the host never writes.
…er the sender calls

The sender sends through callStructuredAgentSession, but these launch
tests replaced that module with a factory that answered nothing (and still
named a probe that no longer exists), so every launch prompt resent until
its 30 s deadline and the tests timed out. Each factory now hands sends to
the runtime RPC mock the tests already answer, and expectations of a local
send no longer ask for the remote-only compatibility option.
…s attachment hook

The worktree purge reaches the launch prompt, which hands text back, and
the attachment hook's imports reach the store. A test that builds the
real store behind a mocked one then waited on itself and hung. Handing
back now writes images to the draft store directly, as the hook's helper
did, and a test pins that each returned image keeps its SSH connection.
A chat with a send out took further messages into an in-memory line and
sent them one by one. A message typed behind one in doubt then hit its
own 30 s deadline and came back as not sent without ever going out.

Now, as the common pattern does, a chat takes one send at a time: while
it is out, Send is disabled and Enter leaves the text in the box. The
sender refuses a second send instead of lining it up, so the 30 s
deadline always runs from the send itself. A Stop or a tab close stops
the one send: settled from its answer if its request is out, handed back
as unconfirmed between attempts, or silently if it never went out.

Notes sent from outside the chat while its send is out stay with their
sender (not ready); a launch prompt that meets the person's own first
message waits in the composer instead of being lost.
…s notes were cleared

Notes sent from outside a chat clear once their message is recorded. A
message the host recorded as pending and a Stop then withdrew came back
only to its sender, which had already let go of it, so the text was
lost. The chat's draft now takes it, as an earlier build's outbox did.
…d the chat for its launch prompt

- A send whose checks failed before its request went out (an unreachable
  or incompatible host, an unreadable history) was tried again at once,
  about a thousand times a second for 30 s. Attempts are now paced by
  the attempts made, whether or not their request went out.
- A host that refuses every resend by throwing (native chat turned off,
  a journal that won't open) gave back only "couldn't confirm". The
  host's reason now comes first, still without claiming not sent.
- A launch's prompt now holds the chat's one send from the click, drawn
  as sending, so a message typed while the chat starts can't overtake
  it; it goes out once the chat exists, and a cancelled launch frees it.
- Notes whose send nobody could confirm say so instead of "did not
  accept", and notes launched into a new chat let go of their text once
  that chat's composer holds it.
- An open chat keeps drawing a recorded send until its row arrives, so a
  reply that beats the history no longer makes the message flicker out.
…a failed chat's message with its restart

A message sent to a chat still starting went out at once to a host that
had no record of the chat yet, read for a fence it could not get, and
came back as not sent. A message sent to a chat whose start failed
restarted it but no longer went with the restart.

While a chat starts, Send stays off and Enter leaves the text in the box,
as with a send already out. A text message sent to a chat whose start
failed restarts it and goes as the restart's first message, through the
same staged-prompt path a launch prompt takes: it holds the chat's one
send slot, drawn as sending, until the chat exists, and comes back to
the composer if the restart fails again. Notes wait while a chat starts
and ride a failed chat's restart, keeping their text until it is sent.
…hand-offs; drop an unused clear

- Pin which sends ask the host to queue, the moved rejection wording,
  and that a queue send whose card was handed off and then refused or
  withdrawn, or whose replay names a withdrawn card, is never handed
  back.
- The send-at-most-once gate now says what the desktop promises after a
  reload: it never sends the id again, and hands the text back.
- The legacy read names when it goes, and drops the notice clear nothing
  called.
… once what can't go out

- Cleaning up a launch prompt released its send slot even after the
  prompt had gone out, which deleted the entry the sender keeps once the
  host records it. A launch prompt recorded as pending and then
  withdrawn by a Stop was lost from both the chat and the box, and an
  open chat dropped the new chat's first message before its row arrived.
  The slot now gives back only a reservation that was never sent.
- A send stopped before its request went out by something trying again
  won't clear (this client and the server can't talk, or the host
  refused the history read) kept Send off for 30 s and then said Orca
  couldn't reach the agent. It now comes back at once with its own
  cause: not sent, since nothing went out, or unconfirmed if an earlier
  attempt did. Transport errors keep the paced retries.
Notes sent to a New agent whose start failed came back to the notes and
also sat in the new chat's composer, so sending both delivered the text
twice. The notes keep their text until it goes out (they hold it from
the click), so the launch now says so and no composer gets a copy, on a
failed start or a refused prompt alike. The tests that asserted the copy
in the composer pinned the old double ownership and now assert the
notes are its only owner.

Notes that rode a failed chat's restart and ended unconfirmed now say
Orca couldn't confirm them, as notes sent directly do, instead of that
the agent did not accept them.
…ross a reload

The desktop keeps a send's id in memory only: a send still unsettled at a
reload or crash is not resent and not handed back. The coverage notes no
longer credit the renderer tests with durable identity across a remount.
Since notes keep their own text through a new agent's launch, a prompt
the host refused, nobody could confirm, or that found the chat's send
taken came back to the notes with nothing said anywhere: the chat shows
no notice for text its caller keeps. The notes menu now reports it once,
with the toast a send to an existing chat already uses: not accepted,
couldn't confirm, or not ready. A start that failed still says so in the
new chat instead.
…ame it at the deadline

A send stopped before its request went out came back at once for any
refusal of the history read, including ones the host names as clearing
(its journal briefly unavailable, a chat detached while the host quits),
so the automatic retry was lost. Only a version mismatch and refusals
that won't clear come back at once now; the rest go again on the paced
schedule, and if the deadline still finds nothing sent, the words are
the host's refusal rather than Orca couldn't reach the agent.
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/nc-host-send-queue to main October 7, 2026 22:51
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Starting a new desktop chat previously left its first message in the window while the agent started. Stop returned the text to the composer but left startup running. The host now saves the chat and opening message together through the existing submission writer and confirms that save before starting the agent. Send is busy before confirmation; afterward Stop cancels startup while retaining the sent bubble and existing stopped marker. A refused or unanswered create returns the text once after any draft already typed, using the existing notice.

Review round one found replay could change an already-recorded create contract when capabilities changed and unanswered Retry or automatic confirmation could remain pending indefinitely. Both now retain the exact operation/message identity and have finite 30-second deadlines, with regressions and removal/restoration proofs. Round two found no outstanding proven defect and independently passed 75 original-head and 44 corrective tests. Later CI corrections derive the first-message type from the existing strict user-message wire, validate persisted bodies with its browser-safe shape, remove four store initialization cycles through existing narrower modules and conditional recovery loading, and preserve strict modern-create/legacy-attach assertions. Round three found no proven defect in that seven-file correction, passed 78 tests in five files and found no static return path into the store in a parsed 1,201-module graph. Packaged module-loading faults were not injected. Round four found no defect in the two test-routing files and passed 101 tests in eleven files while checking all prior catalog entries and runtime/type-only distinctions.

The routing correction sends eight real SQLite fixture suites through the existing Node project. Five unchanged main suites pass 49 tests under Node and reproduce 49 SQLite setup failures under pinned-main Bun; this does not claim they were historically skipped. The final CI-discovered correction changes one test fixture only: it supplies the empty operation lookup, preserves all nine cases and original assertions, and additionally proves attach was reached once in both affected cases. The coordinator personally reviewed and approved that exact final diff. A source inventory and traces of real create-preparation consumers found no other incomplete ledger fixture. Final commit 007eca4 passes 199 focused tests in eight files; removing its lookup reproduces both previous assertion failures and exact-byte restoration passes all nine cases. Production remains identical to hidden QA commit b2e9a72, which passed 470 affected tests in 48 files. All five queued desktop/shared/mobile typecheck scopes passed. Fifty-four historical production safeguard reversals, some preceding later corrections, produced assertions, import failures or compiler diagnostics and restored saved bytes; three wire-fixture, four routing and one final fixture reversals are separate evidence.

All 37 checks on final head 007eca4 have finished: 21 passed and 16 were skipped, with no red CI check and no new failure. All ten desktop shards—including the previously failing shard 5—relay integration, wire compatibility, static/typechecks, mobile verification/bundle, both packages and final verification pass. Final desktop CI run. The watcher's TLS connection interruption was resolved by resuming the same run. Separately, pre-push checks completed 20/22 with only two inherited local failures: the 304-line cross-version manifest is byte-identical to selected main 2460068 (SHA256 aa9552cdcc26cd5d9b0962279c753220c26b97b83af72898e71e56c3ed7edffc), and five mobile cycles reproduce on main source with identical installed dependencies and unchanged audited sources/configuration. Skipped checks are not passes.

Final hidden macOS Electron QA with an isolated profile and delayed Codex stand-in proves startup Stop is available after host confirmation, the stopped bubble and existing marker remain with an empty composer and idle sidebar, the stopped first text never reaches the agent, only a distinct next message is delivered once, and a blank chat remains at rest. Four personally inspected screenshots are uploaded in the PR body; the full stand-in log was also inspected. The coordinator accepted this b2e product evidence for the final test-only commit. UNVERIFIED: rendered held-create/no-Stop before confirmation, refusal/Retry, reload/older-host behavior, the transient Stopping frame, Claude/ACP startup, packaged loading faults, live mobile/SSH/WSL/Linux/Windows and paired network failures. Complete before/activation/teardown profile hash equality is also unverified: snapshots are incomplete, missing rows are unobserved, and the sample cannot establish which roots changed or attribute a change to QA. No real profile contents were opened or restored. The driver stopped its recorded app tree and closed its ports; the coordinator instructed nobody to signal the remaining read-only hash commands or gate readiness on their completion. Untracked worktree QA artifacts were moved into lane scratch and never staged.

Older hosts temporarily keep create-then-send until the supported host floor includes the capability. Older clients intentionally retain acquired-create; later option writes may take effect after the opening turn begins. Mobile, agent launch and orchestration remain a temporary deferral to “Record opening messages during create for every chat caller,” to be opened after this PR merges. Exactly one final main sync was performed, both lockfiles equal selected main, and the final checkout is clean. This PR has not been merged and auto-merge is not enabled.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 8, 2026 00:09
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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: 42a38233-56b5-4ee5-a695-e0c235a79e2a

📥 Commits

Reviewing files that changed from the base of the PR and between 3e75756 and 6031e44.


📒 Files selected for processing (11)
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • src/main/acp/acp-structured-host-configured-default.test.ts
  • src/main/ipc/desktop-renderer-runtime-capabilities.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-options-read.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/rpc/methods/structured-agent-session.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/shared/electron-remote-runtime-client-capabilities.ts
  • src/shared/protocol-version.ts

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



📝 Walkthrough

Walkthrough

The change adds structured agent-session creation with optional opening messages and options. It defines and advertises a create-message runtime capability, adds request validation and create handling, and updates launch prompts to use the creation path when supported. The renderer tracks opening-message send outcomes and deadlines, restores persisted launch data, and publishes creation hydration. Session-reader ownership is retired during tab and worktree cleanup.


Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 6031e

On older hosts, a prompt staged after preparation may return early while waiting for acquisition. This bounded compatibility risk needs owner awareness before merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 52.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 94 functions across 61 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check Passed The description follows the repository template and clearly explains the user impact, implementation, rationale, linked issue, visual proof, testing, review findings, compatibility limits, and checkli…
Title check Passed The title clearly and concisely identifies the main change: saving a new chat's first message during creation.
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.


  • 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: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 11f44264-7429-42ea-a649-bd6c7b670ad5
📥 Commits

Reviewing files that changed from the base of the PR and between 2460068 and 007eca4.

📒 Files selected for processing (72)
  • config/scripts/vitest-sqlite-runtime-boundary.test.ts
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • mobile/src/session/structured-create-wire-types.test.ts
  • src/main/ipc/desktop-renderer-runtime-capabilities.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create-adoption.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create-test-fixture.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-test-data.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-options-read.ts
  • src/main/runtime/agent-session-founding-admission.ts
  • src/main/runtime/agent-session-founding-store.ts
  • src/main/runtime/agent-session-record-store.ts
  • src/main/runtime/agent-session-reservation-admission.ts
  • src/main/runtime/rpc/methods/structured-agent-session-create-handler.ts
  • src/main/runtime/rpc/methods/structured-agent-session-create-message.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-create.ts
  • src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts
  • src/main/runtime/rpc/methods/structured-agent-session.ts
  • src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx
  • src/renderer/src/components/native-chat/native-chat-structured-session-notices.ts
  • src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts
  • src/renderer/src/components/native-chat/structured-agent-session-message-sender.ts
  • src/renderer/src/components/native-chat/structured-agent-session-pending-sends.ts
  • src/renderer/src/components/native-chat/structured-agent-session-read-owner-registry.ts
  • src/renderer/src/components/native-chat/structured-agent-session-read-owner.ts
  • src/renderer/src/components/native-chat/structured-agent-session-send-runtime.ts
  • src/renderer/src/components/native-chat/structured-agent-session-stop-control.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-provisional.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/renderer/src/lib/launch-structured-agent-session.ts
  • src/renderer/src/lib/structured-agent-session-create-hydration.ts
  • src/renderer/src/lib/structured-agent-session-launch-create-call.ts
  • src/renderer/src/lib/structured-agent-session-launch-create-message.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-create-message.ts
  • src/renderer/src/lib/structured-agent-session-launch-create-support.ts
  • src/renderer/src/lib/structured-agent-session-launch-message.ts
  • src/renderer/src/lib/structured-agent-session-launch-options.ts
  • src/renderer/src/lib/structured-agent-session-launch-outcome-tracking.ts
  • src/renderer/src/lib/structured-agent-session-launch-persistence.ts
  • src/renderer/src/lib/structured-agent-session-launch-prompt-outcome.ts
  • src/renderer/src/lib/structured-agent-session-launch-prompt-stop.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-prompt.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-prompt.ts
  • src/renderer/src/lib/structured-agent-session-launch-publication-confirmation.ts
  • src/renderer/src/lib/structured-agent-session-launch-publication.ts
  • src/renderer/src/lib/structured-agent-session-launch-record.test.ts
  • src/renderer/src/lib/structured-agent-session-launch-record.ts
  • src/renderer/src/lib/structured-agent-session-launch-recovery.ts
  • src/renderer/src/lib/structured-agent-session-launch-reload.ts
  • src/renderer/src/lib/structured-agent-session-launch.ts
  • src/renderer/src/runtime/structured-agent-session-client.ts
  • src/renderer/src/runtime/structured-agent-session-tab-retirement.test.ts
  • src/renderer/src/runtime/structured-agent-session-tab-retirement.ts
  • src/renderer/src/store/slices/worktree-sleep-intent-lifecycle.test.ts
  • src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
  • src/shared/agent-session-wire.ts
  • src/shared/electron-remote-runtime-client-capabilities.ts
  • src/shared/protocol-version.ts
  • src/shared/rpc-contract/structured-agent-session-agent-params.test.ts
  • src/shared/rpc-contract/structured-agent-session-message-params.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
  • src/shared/structured-agent-session-create.test.ts
  • src/shared/structured-agent-session-create.ts
  • src/shared/structured-agent-session-mutation.ts
  • src/shared/structured-agent-session-send-mutation.ts
  • tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
  • tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts
  • tests/e2e/cross-version-wire/structured-agent-session-surface-execution.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 +146 to +150
export function prepareLegacyStructuredLaunchPrompts(sessionId: string): void {
for (const prompt of staged.get(sessionId) ?? []) {
prompt.slot?.legacy()
}
}

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

Clear the reservation deadline for prompts staged after legacy preparation.

reserveStructuredAgentSessionSend now starts a 30 s deadline when the slot is reserved. For an older host, only prepareLegacyStructuredLaunchPrompts clears that deadline, through slot.legacy(). It runs once, inside prepareStructuredLaunchCreateMessage. A prompt can be staged after that point, for example by a claim joining an in-flight blank launch. On an older host, that prompt keeps its deadline while the create waits for acquisition. After 30 s the slot hands the text back with an unreachable notice, even though the chat is still starting. The person must then send it again.

Record the legacy decision per session. Apply it when a prompt is staged.

Proposed fix
--- "a/src/renderer/src/lib/structured-agent-session-launch-prompt.ts"
+++ "b/src/renderer/src/lib/structured-agent-session-launch-prompt.ts"
@@ -143,11 +143,14 @@
   }
 }
 
+const legacySessions = new Set<string>()
+
 export function prepareLegacyStructuredLaunchPrompts(sessionId: string): void {
+  legacySessions.add(sessionId)
   for (const prompt of staged.get(sessionId) ?? []) {
     prompt.slot?.legacy()
   }
 }
 
 /** Transfers the first staged prompt to create, retaining the sender's deadline and bubble. */
 export function prepareStructuredLaunchPromptForCreate(

In stageStructuredLaunchPrompt, after the prompt is created:

if (legacySessions.has(sessionId)) {
  prompt.slot?.legacy()
}

Remove the session from legacySessions when its launch state is cleaned up or cancelled.

…rt-stop-standard

# Conflicts:
#	src/main/runtime/rpc/methods/structured-agent-session.ts
…sync

# Conflicts:
#	src/renderer/src/components/native-chat/native-chat-structured-session-notices.ts
…sync

# Conflicts:
#	src/main/ipc/desktop-renderer-runtime-capabilities.ts
#	src/main/runtime/agent-session-record-store.ts
#	src/renderer/src/components/native-chat/use-structured-agent-session.ts
#	src/shared/electron-remote-runtime-client-capabilities.ts
…still teaches its agent's default

After the main sync, a created chat's agent starts from the delivery loop rather than the create
call. Grok (through the real ACP adapter and model catalog) and Claude (through the real adapter)
each still report the model their own config resolved to once the chat reads its options.
@brennanb2025
brennanb2025 merged commit 573ff0b into main Oct 9, 2026
32 checks passed
brennanb2025 added a commit that referenced this pull request Oct 12, 2026
…on receipt

Main's create path (from #26332) hands performSend the founding receipt; the
Stop changes widened the turn context's receipt to track its commit. Wrap it
with the existing observedReceipt.
brennanb2025 added a commit that referenced this pull request Oct 13, 2026
…d Stop does nothing (#27076)

* feat(native-chat): add a chat-owned command receipt store

Adds the agent_session_command_receipts table and an insert-if-absent
receipt write that runs inside a command's own journal transaction. A
receipt records the command's identity, canonical hash, status and a
pointer to the row holding its result; it dies with its chat record.
Nothing uses it yet: the existing operation ledger stays authoritative.

* fix(native-chat): key command receipts like today's ledger ids

Rows are keyed by caller and operation id; a send looks across callers and
other methods look up only their caller's row, matching the existing ledger.
Bad rows and newer table shapes read as unreadable instead of throwing.

* fix(native-chat): order receipt lookups by insertion and key by operation id

Global lookups now take rows in insertion order, so a read and an insert
agree on which receipt answers an id regardless of the host clock. The
primary key leads with the operation id, so its own index serves both
lookups.

* fix(native-chat): accept sends through their own receipt

Send (direct, queued, steer) and /compact no longer commit a separate
ledger reservation before the message is saved. Their command receipt is
inserted-if-absent in the same transaction as the submission or queued
draft; a repeated id replays from it, a changed request conflicts.
Operation ids are checked for syntax only, so age, clock skew, pruning
and the reservation's own storage can no longer refuse a send.

The ledger success row they already co-committed stays as a temporary
insert-if-absent write so families not yet switched keep seeing these
ids; it is never read on this path.

* fix(native-chat): answer a send's receipt one way, share /compact's id space with Send, wake delivery once

- After its commit, a send whose publication fails answers through the same receipt replay a retry takes, so one id gets one answer; the in-memory copy and journal reload are gone.
- /compact ids are global like Send's: a retry under a new connection replays instead of needing a second acknowledgment write.
- The receipt route calls its runner directly; the runner wakes delivery when a new message row commits.
- The compatibility ledger row expires from host time; each temporary site is labelled.
- Receipt read errors and post-commit failures are logged.

* test(native-chat): expect receipt answers where tests assumed the send ledger

Restart-continuation tests now model the case Send cannot vouch for (saved but
not loaded into memory) and keep it unconfirmed; a later throw after the message
is in the chat answers continued, as a resend would. Drop /compact's unreachable
legacy replay branch and fix stale comments.

* test: run the queued tests that open the journal database in the Node runtime project

* fix(native-chat): queued-message actions and prompt answers accept through their own receipt

Send Now, Delete, Resume and prompt answers move onto the command-receipt route
Send uses: the receipt is inserted with the command's own write. A no-op answers
only once its receipt commits (otherwise outcome unknown), and a prompt answer's
replay reads the exact revision it was accepted at.

* fix(native-chat): replay a queued action from what its receipt recorded

Send Now's acknowledgement points at the submission it handed off; a question
answer's receipt keeps the accepted resolution; a no-op that cannot save its
receipt answers outcome unknown and holds nothing in memory; classified refusals
pass through.

* docs(native-chat): receipt comments say what a replay answers from

* fix(native-chat): Stop saves first, captures its target, and holds queued messages

Stop accepts through its own receipt: one transaction saves the Stop record
(target turn, agent generation, cutoff), returns unsent messages to the queue as
paused cards, and writes the receipt; only then does it interrupt the captured
target. A failed save leaves the agent running and says so. A late Stop is a
recorded no-op, and a retry is answered from the receipt.

* fix(native-chat): Stop round 1: late Stop is a no-op, saved-then-failed refuses, hold only what the Stop did not stop

- A Stop naming a turn that already ended is an accepted no-op.
- A card's Cancel saves its receipt with the dismissal row.
- A Stop saved but not carried out is refused as stopFailed ("Couldn't stop <agent>. Try again.") and its receipt is rewritten as rejected.
- A Stop during a start is saved first, then aborts that start; the abort-before-admission path is removed.
- The message the Stop stops is withdrawn as before; messages behind it are held as paused cards. Non-text messages are still withdrawn (TEMPORARY).
- Cleanups: one receipt commit path, dead fields and helpers removed, the returned-to-queue text names the agent.

* fix(native-chat): Stop round 2: narrow to receipts and save-first; message fate as on main

- Stop's withdrawal of queued sends is main's own rows (rejected, cancelled), written in the same
  transaction as the Stop event and its receipt; a failed save withdraws nothing.
- A Stop arriving during a start aborts that wait on arrival, as on main (admits-now guard), then
  captures its target once in the session lane. The out-of-lane admission is removed.
- A committed receipt is never rewritten: a failure after the save answers from the receipt and
  writes the existing 'Cancellation was not confirmed.' note. 'Couldn't stop <agent>' only when
  the save fails; a card Cancel that can't be saved says the question or approval wasn't cancelled.
- A Stop naming a turn a rewind removed is late (an accepted no-op).
- Removes the held-card settlement and returnedToQueue: what happens to a stopped message stays as
  on main.

* test(native-chat): name the Stop row comparison for what it drops

* fix(native-chat): Stop round 3: a committed Stop always acts; notes only for unconfirmed interrupts

- A Stop whose write throws after its receipt committed still interrupts (never 'Couldn't stop').
- An interrupt that worked answers stopped even when only its note fails to write.
- A strict card interrupt that throws after its receipt committed gets the 'Cancellation was not
  confirmed.' note, through one shared helper with the turn path.
- A target-less Stop stands while the main agent works, so a turn that opened while its save waited
  (a send made before the Stop) is interrupted, as on main.
- A card Cancel that can't be saved says 'Couldn't cancel this question or approval. Try again.'

* fix(native-chat): Stop round 4: name the turn the Stop actually stops

- The Stop event's turn is resolved when its write runs (the named turn, else the live turn), so a
  turn that opened while the save waited is the one marked interrupted and shown stopping.
- The unconfirmed note is keyed by that turn; tests cover it and its own write failing.
- Whether a Stop withdrew anything comes from its capture, so a throw after the commit can't flip it.
- A Stop counts as saved anyway only when its own write committed the receipt.

* fix(native-chat): a new chat's first send gets the Stop-aware operation receipt

Main's create path (from #26332) hands performSend the founding receipt; the
Stop changes widened the turn context's receipt to track its commit. Wrap it
with the existing observedReceipt.

* fix(native-chat): a background task the agent won't stop still says not done

A background-task Stop now saves its receipt before it asks the agent. A
refusal after that save (task still running, or a timeout / lost connection)
was being replaced by the receipt's replay, so the person never heard the Stop
failed. That refusal is now the first answer, as on main; the receipt stays
saved and a retry of the same id is answered from it without asking again.

* test(acp): a Grok Stop naming an ended turn leaves the unechoed follow-up alone

The late-Stop rule makes a Stop naming a turn that has ended an accepted no-op
for every agent. The Claude case was rewritten with it; this is Grok's.

* test(native-chat): Stop tests exercise what they name after the late-Stop rule

- The "leaves the child alone" Stop now reaches the provider (handed over, a
  turn with no row yet); the at-rest refusal case was a duplicate of the late
  no-op test and is removed.
- The adapter comment and two test names say the gap rule covers a named turn
  with no journal row yet.
- The Grok late-Stop test checks no status row and that the follow-up completes.
- Name the two refusals refusalAnswers passes through.

* fix(native-chat): a background-task Stop answers its acceptance and notes what happened

A background-task Stop is saved before it asks the agent. The answer is
that saved acceptance: a stopped task answers cancelled:true as before
(the phone keeps the row "stopping" on it), and anything else answers
cancelled:false. A retry answers from the receipt without asking again.

What the agent then says comes back as a note keyed by the Stop's own id,
written through the existing Stop-note path, and never as a refusal:
- still running: "The background task wasn't stopped." (or "The
  background tasks weren't stopped." for Stop all), the new status-row
  failure kind backgroundTaskNotStopped;
- timed out or lost the agent: "Cancellation was not confirmed."
The note is the thread's, since tasks outlive their turn. A note that
cannot be written is logged and changes no answer.

The background-task body moves out of performCancel into the targeted
cancel, and the refusalAnswers flag and its receipt are deleted.

* test(runtime): the Codex strip's Stop notes a command it could not stop

A background-task Stop now answers its saved acceptance and says what happened in a note: "The background task wasn't stopped." when the command still runs, "Cancellation was not confirmed." when Codex was lost.

* test(native-chat): keep the Stop tests that pin behaviour, cut the rest

Merges the receipt, save-first, failed-after-save and background-task Stop
tests into one flow file; drops unit and per-branch tests already covered
by a flow test.
brennanb2025 added a commit that referenced this pull request Oct 15, 2026
* fix(native-chat): the host owns the send queue; the window keeps no saved outbox

The desktop kept each structured chat's unsent messages in localStorage and
sent them in order, so one message whose fate was unknown froze every later
send, Retry dropped it silently, failed sends could not be discarded, and an
offline chat could send hours later. The host already records every message
and owns the queue; the window now only sends.

- One in-memory sender for composer, launch prompts and messages sent from
  outside the chat. One send in flight per chat; a transport failure resends
  the same id for up to 30 s; a refusal that proves nothing was recorded puts
  the text back in the composer with the reason; a send that went out and was
  never answered shows an in-doubt line with Send again and holds nothing up.
- The host's "unknown" rows get the same in-doubt line and Send again, from
  the journal, in every window.
- A message an older build left in localStorage is never sent: the host's
  conversation outline decides what goes back to the composer, and the copy is
  deleted once that is saved.

* fix(native-chat): send through the structured chat RPC wrapper, with its timeouts

The sender called the runtime RPC directly, skipping the per-method timeouts
every other structured chat call gets. Only a remote host's request skips the
compatibility check the sender already ran.

* fix(native-chat): an unconfirmed send goes back to the composer, with no new row line

The common pattern draws nothing extra on a message whose delivery is in
doubt once its turn is over, and puts a failed send's text back in the
composer with the reason. So a send nobody answered in time, or one a host
answers in a way that proves nothing, goes back to the composer worded as
unconfirmed, and a host "unknown" row shows nothing extra. Removes the in-doubt
phase, Send again, and its strings.

A host's made-up row for an id its journal lost now reads as unconfirmed, not
as recorded, so that message comes back instead of vanishing.

* test(native-chat): pin that nothing resends a send given back as unconfirmed

* fix(native-chat): sends survive a tab close, never resend after a Stop, and keep remote images

- A normal tab close lets sends on their way settle; what the host never took
  goes back to the conversation's draft. Only a cancelled launch or a worktree
  purge drops them.
- After any Stop, a send already on its way is never sent again under its id:
  a doubtful answer, or a resend that was due, hands its text back worded as
  unconfirmed.
- A returned image keeps the SSH connection it lives on; the connection never
  goes to the host.
- An older build's saved message the host recorded and then rejected is left
  to the host's own row, never handed back.

* fix(native-chat): a Stop or a tab close never resends a send already out, and a kept card is the card's

A Stop that landed while a same-id resend was being readied (its timer
fired, its request not out yet) let that resend go out after the Stop.
The sender now tracks whether a request is awaiting its answer: a Stop
hands back every send between attempts as unconfirmed, lets one whose
request is out settle from its answer, and never issues a request after
it. A normal tab close withdraws the same way instead of doing nothing,
so nothing more goes out and nothing is dropped.

A send the host rejected but kept as a card (keptAsQueuedMessageId) is
the card's, from its reply or the journal, and never goes back to the
composer.

Adds the freeze tests: a send whose fate is unknown holds later sends
no longer than its deadline, and one the host can neither confirm nor
deny releases the next at once.

* fix(native-chat): read a resend's turned-away call or reused id as unproven

Ports the send-answer proof contract. A call the host turned away before
running it (method_not_found, invalid_argument, unauthorized) proves only
that this request wrote nothing, so it reads as never sent on a first
attempt only; on a resend an earlier attempt may have landed, and it goes
again under the same id. A resent id the host says was already used for
other content (messageIdReused) proves nothing either, like an expired or
conflicting id.

Pins the rest of the contract: any row the host returns is its own, a
thrown error is no answer whatever its code, and an older build's
refused, held or outlived-Stop copy is handed back once and never sent.

* refactor(native-chat): type the structured composer's send with its attachment type

Keeps NativeChatStructuredSession.tsx within the file length limit.

* refactor(native-chat): drop the kept-card guards the sender never needed

A kept send's row is a rejection that was never a Stop's, so the sender
already reads it as recorded, from its reply or the journal. The test
that pins it stays; the two extra checks only covered a kept row that is
also a withdrawal, which the host never writes.

* test(native-chat): route launch tests' sends through the client wrapper the sender calls

The sender sends through callStructuredAgentSession, but these launch
tests replaced that module with a factory that answered nothing (and still
named a probe that no longer exists), so every launch prompt resent until
its 30 s deadline and the tests timed out. Each factory now hands sends to
the runtime RPC mock the tests already answer, and expectations of a local
send no longer ask for the remote-only compatibility option.

* fix(native-chat): hand a message back without importing the composer's attachment hook

The worktree purge reaches the launch prompt, which hands text back, and
the attachment hook's imports reach the store. A test that builds the
real store behind a mocked one then waited on itself and hung. Handing
back now writes images to the draft store directly, as the hook's helper
did, and a test pins that each returned image keeps its SSH connection.

* fix(native-chat): one send per chat, with no line of sends behind it

A chat with a send out took further messages into an in-memory line and
sent them one by one. A message typed behind one in doubt then hit its
own 30 s deadline and came back as not sent without ever going out.

Now, as the common pattern does, a chat takes one send at a time: while
it is out, Send is disabled and Enter leaves the text in the box. The
sender refuses a second send instead of lining it up, so the 30 s
deadline always runs from the send itself. A Stop or a tab close stops
the one send: settled from its answer if its request is out, handed back
as unconfirmed between attempts, or silently if it never went out.

Notes sent from outside the chat while its send is out stay with their
sender (not ready); a launch prompt that meets the person's own first
message waits in the composer instead of being lost.

* fix(native-chat): give a Stop-withdrawn note back to its chat once its notes were cleared

Notes sent from outside a chat clear once their message is recorded. A
message the host recorded as pending and a Stop then withdrew came back
only to its sender, which had already let go of it, so the text was
lost. The chat's draft now takes it, as an earlier build's outbox did.

* test(native-chat): type the composer-actions probe without a cast

* chore(native-chat): drop outbox wording left in comments and an empty locale group

* fix(native-chat): pace failed checks, keep the host's reason, and hold the chat for its launch prompt

- A send whose checks failed before its request went out (an unreachable
  or incompatible host, an unreadable history) was tried again at once,
  about a thousand times a second for 30 s. Attempts are now paced by
  the attempts made, whether or not their request went out.
- A host that refuses every resend by throwing (native chat turned off,
  a journal that won't open) gave back only "couldn't confirm". The
  host's reason now comes first, still without claiming not sent.
- A launch's prompt now holds the chat's one send from the click, drawn
  as sending, so a message typed while the chat starts can't overtake
  it; it goes out once the chat exists, and a cancelled launch frees it.
- Notes whose send nobody could confirm say so instead of "did not
  accept", and notes launched into a new chat let go of their text once
  that chat's composer holds it.
- An open chat keeps drawing a recorded send until its row arrives, so a
  reply that beats the history no longer makes the message flicker out.

* fix(native-chat): hold a chat's sends until it has started, and send a failed chat's message with its restart

A message sent to a chat still starting went out at once to a host that
had no record of the chat yet, read for a fence it could not get, and
came back as not sent. A message sent to a chat whose start failed
restarted it but no longer went with the restart.

While a chat starts, Send stays off and Enter leaves the text in the box,
as with a send already out. A text message sent to a chat whose start
failed restarts it and goes as the restart's first message, through the
same staged-prompt path a launch prompt takes: it holds the chat's one
send slot, drawn as sending, until the chat exists, and comes back to
the composer if the restart fails again. Notes wait while a chat starts
and ride a failed chat's restart, keeping their text until it is sent.

* chore(native-chat): test the queue request, rejection words and card hand-offs; drop an unused clear

- Pin which sends ask the host to queue, the moved rejection wording,
  and that a queue send whose card was handed off and then refused or
  withdrawn, or whose replay names a withdrawn card, is never handed
  back.
- The send-at-most-once gate now says what the desktop promises after a
  reload: it never sends the id again, and hands the text back.
- The legacy read names when it goes, and drops the notice clear nothing
  called.

* fix(native-chat): keep a sent launch prompt's entry, and give back at once what can't go out

- Cleaning up a launch prompt released its send slot even after the
  prompt had gone out, which deleted the entry the sender keeps once the
  host records it. A launch prompt recorded as pending and then
  withdrawn by a Stop was lost from both the chat and the box, and an
  open chat dropped the new chat's first message before its row arrived.
  The slot now gives back only a reservation that was never sent.
- A send stopped before its request went out by something trying again
  won't clear (this client and the server can't talk, or the host
  refused the history read) kept Send off for 30 s and then said Orca
  couldn't reach the agent. It now comes back at once with its own
  cause: not sent, since nothing went out, or unconfirmed if an earlier
  attempt did. Transport errors keep the paced retries.

* fix(native-chat): notes keep their own text through a new agent's launch

Notes sent to a New agent whose start failed came back to the notes and
also sat in the new chat's composer, so sending both delivered the text
twice. The notes keep their text until it goes out (they hold it from
the click), so the launch now says so and no composer gets a copy, on a
failed start or a refused prompt alike. The tests that asserted the copy
in the composer pinned the old double ownership and now assert the
notes are its only owner.

Notes that rode a failed chat's restart and ended unconfirmed now say
Orca couldn't confirm them, as notes sent directly do, instead of that
the agent did not accept them.

* chore(native-chat): the send-once gate says what the desktop keeps across a reload

The desktop keeps a send's id in memory only: a send still unsettled at a
reload or crash is not resent and not handed back. The coverage notes no
longer credit the renderer tests with durable identity across a remount.

* fix(native-chat): say once why notes sent to a new agent did not go

Since notes keep their own text through a new agent's launch, a prompt
the host refused, nobody could confirm, or that found the chat's send
taken came back to the notes with nothing said anywhere: the chat shows
no notice for text its caller keeps. The notes menu now reports it once,
with the toast a send to an existing chat already uses: not accepted,
couldn't confirm, or not ready. A start that failed still says so in the
new chat instead.

* fix(native-chat): retry a history refusal the host says clears, and name it at the deadline

A send stopped before its request went out came back at once for any
refusal of the history read, including ones the host names as clearing
(its journal briefly unavailable, a chat detached while the host quits),
so the automatic retry was lost. Only a version mismatch and refusals
that won't clear come back at once now; the rest go again on the paced
schedule, and if the deadline still finds nothing sent, the words are
the host's refusal rather than Orca couldn't reach the agent.

* feat(native-chat): a send makes one request, and nothing ever sends it again

The desktop resent a message under its own id for up to 30 s when the
answer was lost. A send now makes exactly one request, as the common
pattern's clients do:
- An answer that is lost, dropped or proves nothing hands the text back
  at once with "couldn't confirm… check the chat".
- A failure before the request goes out (the environment check, the
  history read for the fence, a refused connection) means nothing went
  out: the text comes back at once with its own reason, or as not sent.
- The 30 s cap stays on the one request, so a host that never answers
  can't hold Send.

Nothing ever resends, so nothing can go out after a Stop: a Stop only
takes back a send still in its pre-send checks. The resend state goes
with it (tries, generation, awaiting, stopped, the resend timer, the
send-answers-proof probe, and the first-attempt/resend split in the
evidence), and the send-once gate says the desktop never resends.

* fix(native-chat): notes keep the chat's line, and a send held behind a rewind says it was not sent

- Only a send whose text belongs to the chat's composer clears the chat's line. Notes sent from
  outside the chat no longer wipe a "couldn't confirm... Check the chat" line that explains text
  already back in the box.
- A send the host turns away behind a rewind it could not confirm went back as "not sent" but said
  "couldn't confirm what happened. Check the chat". It now gives the rewind's reason and says the
  message was not sent.
- Reliability gate names the single-request test and records a fresh evidence run; the sender
  test drops its leftover resend mocks and a duplicate Stop test.

* fix(native-chat): a send ends even when putting its text back fails

If writing the returned text into the chat's draft threw, the send never settled: it stayed
"sending", the chat refused every later send until a reload. The send now always ends after a
hand-back, the failure is logged, and the chat's line adds "Couldn't save your message."

* fix(native-chat): a message typed during /clear stays in the box and follows the chat

A /clear moves the chat to a new conversation, and its host refuses any send while it runs. A
message sent then went out, came back refused into the old conversation's draft with its line, and
vanished when the chat moved on: the box and the line now belong to the new conversation.

- A /clear holds the chat's one send slot while it runs: Enter does nothing, Send shows busy and
  the text stays in the box, as for a send that is out. Notes sent from outside get "busy".
- Once it moves the chat, the old conversation keeps taking no send until the view leaves it, and
  what is left of its draft moves into the new conversation's draft, after anything there: when the
  composer's /clear settles, and again when the view moves.

* fix(native-chat): no "Send message?" or queue clear while the chat's send is out

While a send is out (or a /clear runs) the chat takes no message, yet Enter over a held queue still
opened "Send message?", and Clear queue deleted every card before its message was refused. Enter now
does nothing there and the text stays; Clear queue re-checks and deletes nothing if a send went out
after the question opened.

* refactor(native-chat): take the /clear draft carry out of this change

Moving the old conversation's draft into the one a /clear replaces it with fixes a bug main has too
(text left in the box during a /clear stays with the old conversation), so it goes in its own change.
Kept here: a /clear holds the chat's send slot while it runs, and the conversation it moved away from
takes no send until the view leaves it.

* fix(native-chat): a /clear's hold on sends always ends

- A view that unmounted while a /clear that moves the chat was out left the old conversation
  holding its sends until a reload: the late reply kept the hold for a view that was gone. The
  reply now releases it.
- The local call for a conversation command has no deadline of its own, so a /clear that never
  answers kept Send off for good. The hold now also ends at the command's deadline (195 s, the one
  the remote call already uses), whichever comes first.

* test(native-chat): opening a chat an older build left stuck; hand back its copy in send order

Pins, through the real chat hook, sends, legacy recovery and draft store,
what opening such a chat does: the queued messages behind a message the
host recorded in doubt come back to the composer once, in order, with the
"not sent" or "couldn't confirm" wording; the chat is free to send under
its read's fence; nothing happens while the chat has no fence here.

The legacy reader now hands entries back in the order they were sent
(queuedAt), as the older build's reader did, instead of array order.

* fix(native-chat): a send this window turned away for a re-paired server comes back as not sent

A managed server's update rotates its pairing, and this window's main
process then answers the next call itself, before forwarding anything,
with runtime_environment_changed. The send read that thrown answer as
proving nothing, so the person was told Orca couldn't confirm a message
that never left. It is now handed back as not sent, with the reason.
Every other thrown answer still reads as unconfirmed once the request
may have gone out.

* Save a new chat's first message during create

* Align opening-message types with the user-message wire

* Keep chat publication and cleanup outside store initialization

* Complete the create refusal fixture ledger

* test(native-chat): prove a created first message waits for the agent's start and is handed over once

* refactor(native-chat): keep the host and session hook under the line limit after the main sync

* test(native-chat): a chat created with its first message and no pick still teaches its agent's default

After the main sync, a created chat's agent starts from the delivery loop rather than the create
call. Grok (through the real ACP adapter and model catalog) and Claude (through the real adapter)
each still report the model their own config resolved to once the chat reads its options.

(cherry picked from commit 573ff0b)
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