Skip to content

fix(native-chat): keep a message accepted before a quit or crash as a held card - #24660

Merged
brennanb2025 merged 42 commits into
mainfrom
brennanb2025/accepted-message-survives-quit
Oct 6, 2026
Merged

brennanb2025 merged 42 commits into
mainfrom
brennanb2025/accepted-message-survives-quit

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 41 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2271 $\color{#cf222e}{\Huge{\mathbf{−}}}$​176 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2095
Prod 68 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1043 $\color{#cf222e}{\Huge{\mathbf{−}}}$​466 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​577

ELI5

You send a message in a chat while the agent is still starting up. Orca says "got it" right away. If Orca then quit or crashed, or you closed the chat, before the agent actually received the message, Orca threw it away: when you came back it was not in the message box, not in the chat, and not in the queue. Now Orca keeps it. When you open the chat again, the message waits as a card above the message box that says "Not sent yet — press Send to send it." It goes to the agent only when you press Send on it, and you can Edit or Delete it instead.

What Changed

The problem, as you see it.

  • In a structured chat, a message you send while the agent is still starting (the first message of a new chat, a chat started with a prompt from the phone or the CLI, or a message waiting behind a running /compact) is accepted at once but handed to the agent a few seconds later.
  • If, in that window, Orca quit or crashed, or the chat was closed (closing its tab, removing its worktree, or an orchestration coordinator stopping the worker), the computer running the agent threw the message away. It was marked "not sent" with the reason "chat closed" or "Orca restarted", and a message marked that way is hidden from the chat.
  • Seen live in feat(native-chat): a half-typed chat message survives quitting Orca, with one draft per chat #24461's QA on a Mac (a build without this change, Orca killed mid-start): all 9 of 9 messages sent less than about 280 ms before the kill were rejected "Orca restarted" and gone after relaunch; messages sent 3 seconds before the kill, after the agent had them, were delivered.
  • On the phone, from the CLI, and for a chat started with a prompt, the message was simply gone. On the desktop a graceful quit usually got it back, but only because the desktop keeps its own copy in browser storage and sends it again after relaunch; a kill, or a copy that did not save, lost it (QA-7 in the same QA: send, quit about a second later, reopen, no message). After a tab close the desktop discards its copy, so a chat reopened later from its history showed nothing.

Before: quit, crash or close in that window, and the message is lost, unless the desktop's browser copy happened to survive a quit.

After:

  • Quit, crash and close all behave the same. A person's message (typed, or a chat's first prompt from agent.launch) becomes a card at the head of the chat's queue, in the order you sent them. After a quit or crash that happens the next time the chat opens; after a close it happens at the close, and the card is there when the chat is reopened from Agent Session History.
  • The card says "Not sent yet — press Send to send it." and its button reads Send (not "Steer"), because nothing is running for it to steer. Resume never sends it; a "Queue paused · Resume" row appears only when other cards are paused.
  • The card is never sent by itself: not at startup, not when the chat opens, and not when you send a newer message. Only your Send, Edit or Delete on that card releases it. Edit and Delete leave no trace of the original message: no "not sent" row, on any surface. The original send records which card holds it, in the same database transaction as the card, so this holds even after the card is gone.
  • A newer message you type goes to the agent right away, and a message queued behind the kept card still goes in its turn: the kept card waits on its own, as a card whose send failed does.
  • The phone now shows these cards, with the same caption and a Send button, even though queueing new messages is still off for phones.
  • On the desktop this is a change you will notice after a graceful quit. Before, a message cut short by a quit usually came back by itself: the desktop sent its browser copy again after relaunch and the agent got it. Now it waits as a kept card until you press Send.
  • The desktop shows the card once. Its browser copy has the same id, so the card replaces it, with no "not sent / Retry" row beside it. If the desktop sends its copy again after relaunch, the host answers from that message's own record and creates nothing new.

The mechanism.

  • One function, holdUnsentSends (journal-unsent-send-hold.ts), settles a message the host accepted and can no longer hand over, given why: hostRestarted (at chat open, and the delivery loop's first step as a retry: the messages an earlier Orca process accepted) or chatClosed (the close itself, before the agent is stopped, and the delivery loop's retry of a close that stopped the agent and then did not finish). Each message is marked "not sent" for that reason, and a person's message becomes a card in the same database transaction, so a crash can never leave one without the other.
  • Quit no longer marks anything (structured-agent-session-host-teardown.ts), and it stops the queue's automatic sending together with delivery (structured-agent-session-host.ts), so it starts no hand-off that only the next Orca process could settle, not even from a step already running when quit begins. The next open settles what quit left, exactly as after a crash.
  • The card's hold is stored on the card itself: a new value, kept, in the card's existing hold_reason column, published to clients as the existing per-card pausedReason. The column naming which Orca process owns the card is left meaning only that.
  • A kept card is held on its own, exactly as a card whose send failed (send_failed) is: the queue never sends it by itself and goes on to the cards behind it.
  • A card whose Send the person had pressed, cut short by a quit, crash or close before the agent got it, comes back as a kept card too (journal-dispatch-settlement.ts), so their next message can never release it. A card the queue was sending on its own comes back where it stood, as before: under the restart pause after a quit or crash, and as an ordinary queued card after a close in the same Orca session. So Send now on an ordinary card, cut short by a quit, comes back as a kept card that waits for its own Send, while Resume sends the rest of the queue. Both are decided from the rejected send itself (its reason, and that a person asked for it), so the repair that runs at open after a skipped bookkeeping step reaches the same answer and nothing new is stored.
  • Each send records who it is from, using the same idea a queued card already has on main (feat(orchestration): a native chat gets the orchestration pointer a CLI agent gets, through the same send as your messages #25078's AgentSessionMessageSource: the person, or another agent). There is one concept, not a second "who sent it" field:
    • A person's message (typed, /compact, or a chat's first prompt from agent.launch) records the kind user. Orchestration mail records agent. A worker's dispatch instructions and a restart's "continue" message record nothing, since no person wrote them.
    • Only a send whose recorded kind is exactly user becomes a card. Everything else is rejected as before: orchestration mail is sent again by its mailbox after a restart, and after a close once the agent runs another accepted turn or new mail arrives; a restart's "continue" offer is offered again; a worker's dispatch belongs to orchestration (see Review); /compact is not re-run.
    • A rejected message shows "not sent" with Retry only on the desktop that sent it, after a quit, and only until that tab closes; elsewhere it is not shown.
    • A row written by an older build has no source; one a person sent (origin: client) is kept. A kind this build does not know (a newer build's), or a source it cannot read, is kept as written and never treated as missing, so it is never turned into a card. The check is strict on purpose: the card's own source reader treats anything unreadable as the person's, which would keep the wrong message here.
    • Deliberate choice: a sent message keeps only the kind, never the senders. Sent messages are published to clients as they are stored, and feat(orchestration): a native chat gets the orchestration pointer a CLI agent gets, through the same send as your messages #25078 keeps an agent's senders (address, terminal handle, Orca session id) on this machine only. So the sender detail stays on the card. Recording it on a sent message later would need a step that strips it before publishing.
  • A send replayed under the same id answers from its own record unless that send asked to be queued (structured-agent-session-mutation-plans.ts), so a client that never asked to queue never gets the "queued" answer it cannot read.
  • On the desktop, a card can show while the host does not offer queueing; then the card's menu hides "Turn off queueing" and its tooltip hides the Cmd/Ctrl+Enter hint, because both would do nothing.

Why

  • Why it is never sent on its own after a restart or a close. There is no common pattern here, so Orca picks: it waits for the person. Surfaces that cannot show the card make people retype the message, and a release by their new message would deliver it twice; a Stop on the reopened chat would otherwise withdraw it for good; and restoring chats at launch would start their agents unasked.
  • Why a card, not sending it again automatically. A Stop withdraws every message that was accepted and not yet sent, with no way back, and after a restart no message box holds the text, so the reopened chat's first Stop would destroy it. Every restored chat holding such a message would also start its agent at launch, before you look, possibly days later. A card keeps the text, shows it, and does nothing until you act.
  • Why only an action on that card releases it. Older phones, and desktops older than the queue, cannot show these cards, so the message looks missing and people type it again. If the new message released the kept card, the agent would get the same instruction twice. With this rule the retyped message is delivered once, and the card waits for someone to delete it.
  • Why store the hold on the card. The first version of this change marked a kept card by writing a fake owner into the column that names the owning Orca process. Every path that rewrites the owner (Send, the queue's own turn, the step that ends a restart's pause) then silently dropped the hold, so a Send cut short by a second quit let the next message release the card, and the agent got the message twice (reproduced in a test, now a regression test). It also needed four special cases, and an older build released it after a person's next message. A per-card hold is the concept the column already models: Send, Edit and Delete release it, nothing else does, and older builds do not send a held card on their own (one exception, see Downgrade).
  • Why one rule for closes too. Once its agent has received a message, a closed chat is listed in Agent Session History and can be reopened, so dropping an accepted message at its close is the same loss as at a quit. The common pattern keeps an accepted message when a chat is closed or archived.
  • Why not turn on queueing instead. It does not fix this: the first message to an idle chat whose agent must start is not a queued card even with queueing on, and neither are launch prompts or sends from clients that do not ask to queue.
  • Why not rely on the desktop's browser copy. The phone, the CLI and agent.launch have none, the desktop's copy can fail to save, and the desktop discards it at tab close.

Differences from the common pattern

  • The message's own record is marked "not sent", and a separate card holds its text; the common pattern keeps the original accepted message as its own record. Intended. A submission Orca marks "not sent" is final and can never be handed over under its id, so the same message can never reach the agent twice, whatever a client resends. The card is the queue's existing object, with Send, Edit and Delete, that every current client already shows and acts on. Keeping the original record queued instead fails in Orca, observed with this PR's restart hold turned off (structured-agent-session-unsent-send-hold.test.ts, which then fails): the reopened chat started its agent and handed the message over on its own, with nobody looking, and a Stop on the reopened chat withdrew it as "cancelled" with no card left, so the text was gone.
  • Messages with images are not kept; they are rejected as before. Temporary. Cards are text-only today. Follow-up: kept cards that carry images (ticket to be filed; it also covers the case where the image message is lost on the phone and after a desktop tab close, where nothing shows it).
  • Clients older than this change show a kept card differently. Temporary, mixed versions only; ends as those builds age out.
    • A desktop with the queue's cards (from feat(native-chat): mid-turn messages wait as editable cards above the composer #23731) but older than this change shows it as "Paused" with a Send button that works, and no Retry.
    • Released desktops without card support show "not sent / Retry" for that message and no card. Retry sends it under a new id and it is delivered once; the card stays on the host for a newer client, where a later Send would deliver it again.
    • A phone older than this change hides cards while queueing is off.

Linked Issue

Fixes QA-7 from #24461's live QA. #24461 depends on this PR: once the host keeps an accepted message, #24461 can release its client-side hold when the host accepts.

Visual Proof

Live QA on a Mac (M4 Air), desktop unless noted; Q-numbers are the live QA scenarios (see Testing).

Q1: a message sent while the agent is still starting, just before a graceful quit.

pr24660-q1-before-send.jpg

Q1: after relaunch, one kept card ("Not sent yet — press Send to send it.", Send), and no Retry row.

pr24660-q1-kept.jpg

Q2: Orca killed (kill -9) about a second after the send; the relaunch shows the kept card.

pr24660-q2-relaunch.jpg

Q2b: killed 100 ms after the send: one kept card.

pr24660-q2b100-kept.jpg

Q2b: killed 200 ms after the send: one kept card.

pr24660-q2b200-kept.jpg

Q2b: killed 300 ms after the send: one kept card.

pr24660-q2b300-kept.jpg

Q3: Send on the kept card delivers it to the agent once.

pr24660-q3-delivered.jpg

Q4: a new message goes to the agent; the kept card stays.

pr24660-q4-c-and-kept.jpg

Q6: /clear carries the kept card into the new conversation, still kept.

pr24660-q6-cleared-kept.jpg

Q7: after a tab close, the reopened chat shows the kept card. Reopened through the history row's reveal action: the test agent writes no transcript, so the history list itself was empty.

pr24660-q7-reopened.jpg

Q7: the same reopened chat, card and transcript.

pr24660-q7-reopened-2.jpg

Q8: Edit puts the kept card's text in the message box and removes the card.

pr24660-q8-edit.jpg

Q8: Delete removes the kept card.

pr24660-q8-delete.jpg

Q9: with queueing off, the card's menu offers only Edit, its button reads Send, and there is no "Turn off queueing" or shortcut hint.

pr24660-q9-menu.jpg

Q10: after relaunch the desktop shows the kept card alone, with no "not sent / Retry" row.

pr24660-q10-kept.jpg

Q11: the kept card, sent from the phone (which showed "Not sent yet — tap Send to send it"), delivered once, as the desktop shows it.

pr24660-q11-sent-desktop.jpg

Final head smoke, Q1: the kept card after a graceful quit.

pr24660-s2-q1-kept.jpg

Final head smoke, Q5: still kept after a second relaunch, no agent started, no restart header.

pr24660-s2-q5-relaunch2.jpg

Final head smoke, Q5: still kept after a third relaunch.

pr24660-s2-q5-relaunch3.jpg

Final head smoke, Q4: a new message delivered, the kept card stays.

pr24660-s2-q4-c-and-kept.jpg

Testing

  • I manually tested these changes locally

Live QA on a Mac (M4 Air). The full run was on 272bfa8: 12 scenarios pass and Q12 is blocked (no phone screen launches an agent with a prompt for a local repo). A smoke run on the final head 5e6ea97 passed Q1, Q5 (second and third restart) and Q4, with no restart header and no Resume over the kept card. Killing Orca (kill -9) at 100, 200 and 300 ms after a send left exactly one kept card each time, where main lost 9 of 9 such messages. Limits of that run: the test agent writes no transcript, so Q7's reopen used the history row's reveal action directly; Q10's desktop outbox was already empty, so the same-id resend path was covered by tests only; phone screenshots could not be captured, so Q11 was checked from the phone's accessibility tree.

  • Automated tests added/updated

Each new test was checked against a deliberate break of the code it guards (the test failed, then the break was reverted):

  • structured-agent-session-unsent-send-hold.test.ts (real host, store and journal): after a quit and after a crash the message is a kept card and its submission is marked not sent and hidden; nothing is sent across repeated reopens; several messages keep their order; a newer message (the same words retyped) is delivered alone, Resume sends nothing, and the card's own Send sends it once; cards queued behind a working turn come back the same after a quit and after a crash, under the restart pause, with no hand-off made at quit; Delete; a Stop on the reopened chat leaves the card; a Send cut short by a second quit returns the card kept, and a retyped copy is still delivered once; after a tab close (user-close) and a worktree teardown (evict) the reopened chat shows the kept card and not a bubble; the desktop's same-id resend creates nothing and delivers nothing; orchestration mail and dispatch are not kept; a failed card write falls back to "not sent"; the delivery loop settles what the open could not; /clear carries the card still kept.
  • journal-unsent-send-hold.test.ts: which sends are kept and which are not (including a source from a newer build and a dispatch); a close keeps in place and settles only what it names; head-of-queue order, including a card's own interrupted send, a run a crash cut short, and a card kept before a rewind started a new numbering.
  • queued-message-pause.test.ts: a kept card is skipped like a failed one, and the cards behind it still send; adoption keeps the hold.
  • journal-dispatch-settlement.test.ts, queued-message-store.test.ts, journal-unsent-send-hold.test.ts: a Send the person asked for, cut short by a restart or close, returns kept and goes to the head; the queue's own hand-off returns plainly where it stood; one a Stop withdrew is not kept.
  • structured-agent-session-provider-child-record.test.ts: a person's message that a close cut short is a kept card, and no agent starts for it.
  • kept-card-downgrade.unit.test.ts (cross-version): the build this branched from lists a kept card first, keeps it held after a person's next turn and never picks it to send, and settles a message this build's quit left queued.
  • Who a send is from, each checked by removing the code it covers:
    • journal-unsent-send-hold.test.ts: kept for user (typed and launch) and for an older build's client row; rejected for agent, for no source (dispatch, continuation), for an unknown kind, and for a source with no readable kind. A sent message keeps only the kind, never the senders.
    • structured-agent-session-unsent-send-hold.test.ts (real host): mail and a dispatch accepted before a crash are rejected, and mail records only agent.
    • send-agent-turn-host.test.ts: mail sent to an idle chat records agent with no sender detail.
    • agent-launch-structured-prompt.test.ts: the launch prompt sends as the person's.
    • orchestration-structured-worker-session.test.ts and structured-agent-session-restart-continuation-wait.test.ts: the dispatch and the continuation send no source.
  • Desktop: NativeChatQueuedMessageList.test.tsx (caption, Send, hidden queueing controls), structured-agent-session-queued-cards.test.ts (cards behind a kept one are not held by it). Phone: use-mobile-structured-agent-session-queued.test.tsx (cards show from a host that does not offer queueing, and Resume is offered for the card behind a kept one), mobile-structured-queued-message-cards.test.ts.

Platforms: logic only, no platform-specific code. Remote (SSH): the card is made on the computer that runs the chat; quitting only the desktop does not restart a remote host, and a remote host's own restart or close keeps the message. Folder workspaces behave the same as git worktrees.

Review

  • When keeping fails. If writing the card fails (a database error), the message is marked "not sent" as before and is lost, as it was in every earlier build. If even that write fails, the message stays queued; the delivery loop tries again before it hands anything over, and if that fails too, the loop's own failure marks every queued message "not sent", this process's new ones included. Separating those is not done here.
  • A worker's dispatch is still lost at a quit or crash during its start, as before. It records no source and is marked "not sent", but nothing in orchestration sends it again after a restart, so that worker never gets its task. This is older than this PR; the follow-up belongs to orchestration (ticket to be filed).
  • Send, then Stop. Pressing Send on a kept card and then Stop before the agent took it treats it like any card a Stop took back: it waits under the Stop's pause and goes out after the person's next message.
  • Kept cards and chat lifetime. A closed chat keeps its history. Agent Session History lists a chat only once its agent has written a transcript, so a chat closed before its first message reached the agent (a new chat closed while the agent was still starting, or a cancelled launch) cannot be reopened from anywhere, before this change and after it. A card kept there is never seen, never starts an agent, never sends, and shows nowhere else. Where the chat can be reopened, the card lives as long as its history and shows when the chat opens again.
  • Downgrade. On an older build that has the queue, a kept card is an unknown per-card hold, which that build reads as a plain hold: it lists the card first and does not send it, even after a person's next message, and sends the cards behind it as this build does. One exception: that build's /clear carries every card into the new conversation without its hold, so the first message there lets it send the carried card, which a person may already have retyped. Builds older than the queue ignore cards, and the message reads as "not sent", as today. A message this build's quit left waiting is marked "not sent" by an older build's next open, as today.
  • Mixed versions. No new wire field or stream frame. kept is a new value of the existing pausedReason, which clients already treat as a plain hold when unknown; source (its kind only) is an extra key on published submissions that clients ignore.
  • Not in this PR: turning on queueing; kept cards that carry images.

Notes

Security, cross-platform, SSH, mobile, backwards compatibility and performance considered above. Settling runs once per chat open, once per close, and in the delivery loop's first step; each is one pass over the chat's sends and writes nothing when nothing was left unsent.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

… held card

A send the host accepted while the agent was still starting, and never handed
over, was rejected unseen at quit or at the next open after a crash. The next
open now keeps a person's message (typed, or a launch's first prompt) as a
waiting card at the head of the queue, held until Resume, Send now, Edit or
Delete; quit no longer rejects it. Each submission records its source so a
restart knows which leftovers to keep. A direct send's replay answers from its
own record, never the queued arm. Cards shown without the queue capability
hide Turn off queueing and the steer chord.
…e, not only a restart

The host now keeps a person's message it accepted and never handed over with one
rule wherever it can no longer hand it over: a quit or crash (settled at the next
open) and a close of the chat (tab close, worktree teardown, orchestration stop).

- The hold is card state: a new per-card hold_reason 'kept', published as the
  existing pausedReason, instead of a fake host_instance value. host_instance
  means the owner again, and the pause clause, adoption filter and /clear carry
  special cases are gone. A kept card holds the cards behind it until the
  person sends, edits or deletes it; a send_failed card still does not.
- A card hand-off rejected by a restart or a close returns as a kept card
  (rejectedDraftSettlement), so a person's next message can never release it.
- One hold function, parameterized by cause (hostRestarted / chatClosed),
  replaces the close path's plain rejection.
- The phone shows published cards and per-card holds whatever the queue
  capability says; only queueing a new send stays gated.
- source gains 'dispatch' for the orchestration preamble (still rejected); an
  unknown source is kept as written and never takes the legacy rule.
- Kept cards from earlier settlements stay ahead of a batch's new ones.
… leave the queue as a crash does

- A kept card is held on its own, as a send_failed one is: the queue sends the
  cards behind it, and the "a message ahead needs attention" caption no longer
  appears behind it (desktop and phone).
- Quit disposes the queue's drain together with delivery, so it mints no
  hand-off that only the next process could settle.
- Only a Send the person asked for (origin client) that a restart or close cut
  short returns kept; the queue's own hand-off returns where it stood, under the
  restart's pause, as on main.
- Comments that said only a capable host gets cards or the card actions now say
  the capability gates only queueing a new send.
…nds a hand-off

A drain step already past its first check when quit begins no longer makes a
hand-off. Adds regression tests for that, for Send now on an ordinary card cut
short by a quit, and for the open-time repair deriving kept from the hand-off's
origin; fixes the pause and settlement comments that called a kept card one the
queue never passes.
…the others

After a second restart a kept (or send_failed) card is another process's, but it
waits for its own Send, so it no longer pauses every other card under
"Queue paused because Orca restarted".
…ace survives an Edit or Delete

The rejection row of a send the host kept as a card now names that card
(`keptAsQueuedMessageId`), in the same transaction that writes the card, and the
fold publishes it on the submission. The shared projection draws such a send
only as its card: once the card is sent, edited or deleted, neither the send
nor the sending desktop's local copy of it shows.
No client parses published submissions with it (they arrive as typed frames,
and the host's history pages carry the field, as the Edit/Delete test reads);
listing the field put the file over its line budget.
…ing it

The outbox reconcile and the send disposition drop an entry whose submission
the host rejected as kept as a card, as they already do for a Stop's
withdrawal, so the copy never comes back as "Not sent / Retry" once the
submission falls out of the loaded page. The projection reads the reconciled
outbox, so its separate filter goes.
@brennanb2025
brennanb2025 marked this pull request as ready for review October 2, 2026 17:05
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status

The problem. In a structured chat, a message Orca accepted but had not yet handed to the agent (the agent was still starting, a chat's launch prompt, or a message waiting behind /compact) was thrown away if Orca quit or crashed, or the chat was closed, in that window. It was marked "not sent" and hidden, so after reopening it was nowhere. In #24461's live QA, all 9 messages sent less than about 280 ms before Orca was killed were lost this way. On the desktop only a graceful quit got the message back, because the desktop resent its own browser copy.

What changes for you. The message now comes back as a card above the message box that says "Not sent yet — press Send to send it." This happens after a quit, a crash or a tab close, on the desktop and on the phone. It is never sent by itself, not at launch and not when you send a newer message. It goes out when you press Send, or you can Edit or Delete it. A newer message you type goes out right away, and other queued cards are not held behind it. On the desktop, a message cut short by a graceful quit now waits as this card instead of being resent by itself.

Fixed during review (each with a regression test that fails when its fix is reverted):

  • Closing a chat (closing its tab, removing its worktree, or an orchestration stop) still lost the message. A close now keeps it the same way a restart does.
  • The phone showed nothing while queueing is off. The phone now shows these cards with a working Send.
  • The hold was first stored as a fake owner value, so a Send cut short by a second quit lost the hold and delivered the message twice. The hold is now stored on the card itself (hold_reason = kept). That also keeps older builds from sending it after your next message.
  • A kept card at the front of the queue held up the cards behind it while a newer typed message overtook them. It is now passed over the way a card whose send failed already is.
  • A quit and a crash left the same queue in different states. Quit now stops the queue's own sending along with delivery, so both leave the queue the same.
  • A person's message whose source value came from a newer build was turned into a card. Only rows with no source at all are treated as person messages from older builds. Kept cards from before a history rewind came back out of order. An orchestration worker's first instructions now record their source.
  • A kept card left over from an earlier restart paused every other card under "Orca restarted". It now pauses nothing.
  • Edit or Delete on a kept card could later bring back the original message as "Not sent" (and Retry could send deleted text) once rejected messages are shown in the chat (fix(native-chat): show a message Orca accepted and then failed to deliver as "Not sent" in the chat #24710). The original send now records the card that holds it (keptAsQueuedMessageId), and the desktop drops its local copy. Edit and Delete leave no trace.
  • Stale comments about the old rule were corrected, and the PR description was corrected and narrowed.

Deferred (not in this PR):

  • Messages with images are still not kept, because cards are text-only. Follow-up ticket to be filed.
  • An orchestration worker's first instructions are still lost at a quit or crash during its start, as before. Follow-up for orchestration.
  • Kept cards count toward the queue's size limit. This only matters once queueing is turned on.
  • If both writes fail, the delivery loop's failure path marks this process's new messages "not sent" as well. That behaviour is older than this PR.
  • Minor wording and comment-wrapping nits.

Verified

  • Code review in five rounds (two full, three narrow on the fixes), a readiness checklist (no P0/P1), and a design check of the queue and close behaviour against the common pattern.
  • Targeted tests: 3,165 across 344 files, the phone session suite (1,983), and the cross-version suite run against the older build.
  • tc:node, tc:web, tc:cli and the mobile typecheck pass, as do lint and the changed-lines gates.
  • Live QA on a Mac against 272bfa8: 12 scenarios pass. A smoke run of Q1, Q4 and Q5 on 5e6ea97 also passes. The later commits (the Edit/Delete fact, the outbox drop, and main merged in at 1eacd65) were covered by tests and review only, not live QA. Killing Orca at 100, 200 and 300 ms after a send left exactly one kept card each time. Screenshots are in the description.

Not verified

  • Q12 (a phone agent.launch prompt) was blocked: no phone screen does it for a local repo.
  • The test agent writes no transcript, so the reopen after a tab close went through the history row's reveal action.
  • The desktop's resend-after-relaunch path is covered by tests only.
  • Phone screenshots could not be captured; the phone was checked from its accessibility tree.
  • No real Claude or Codex agent was used in live QA.
  • A chat closed before its first message reached the agent cannot be reopened from anywhere, with or without this change. A card kept there is harmless.

@coderabbitai

coderabbitai Bot commented Oct 2, 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: 7db4e6eb-e8dc-4ed2-8af1-1d06040e9bc3
📥 Commits

Reviewing files that changed from the base of the PR and between 4a4d5b5 and 5a3afd3.

📒 Files selected for processing (1)
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.queue-delivery.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.queue-delivery.test.tsx

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


📝 Walkthrough

Walkthrough

The journal records send sources and links rejected submissions to retained queued cards. On restart or chat close, eligible unsent messages are retained as individually held cards, and other selected submissions are rejected without cards. Queue selection, recovery, and card publication account for the kept hold. Mobile and desktop clients show the card and its Send guidance. Message projection and outbox reconciliation use the recorded card association to suppress duplicate local copies. Tests cover recovery, card actions, rendering, and older-build compatibility.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 5a3af

A minor wording issue in the desktop Chinese caption remains open; it is cosmetic and does not block merging, but it should be fixed as a quick follow-up.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 4a4d5

Interrupted messages now remain available until the user chooses to send or discard them. The reviewed paths preserve explicit-send and session-ownership checks. Older-version behavior and some client paths remain only partly verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The security-sensitive outcome is later delivery of retained user text to the existing agent session. Reviewed Send operations obtain content from that session's journal rather than accepting replacement card content from the caller. The inspected paths do not demonstrate added tool privileges or broader cross-session access; upstream identity provenance remains incompletely traced.

Security Findings and Attack Paths

  • observed — The inspected recovery tests counter the unintended-replay attack path: later sends and Resume leave retained cards held, while explicit card Send creates a separate submission. Send also returns an existing consumed submission instead of creating another for an already-dispatched card. These controls support rejection of an automatic-redelivery concern within the reviewed paths, not a complete security clearance.

Trust Boundaries and Controls

  • observed — Card mutations use the session-serialized admission path with a caller key and host-computed payload fingerprint. Admission refuses conflicts and reevaluates the current lease before rerunning an operation with no durable result. Explicit Send retains conversation-block and pending-prompt gates before consuming the card.

Resilience and Maintainability Implications

  • observed — Mobile Edit copies displayed text into the composer before requesting Delete and does not delete if copying fails. A failed deletion leaves the card beside the copy; a deletion racing dispatch reports that the message was already sent. Host Delete uses withdrawal receipts for replay. Composer persistence across interruption was not established.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.69% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 103 functions across 90 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: messages accepted before a quit or crash are preserved as held cards. It is concise and specific.
Description check ✅ Passed The description covers the user-visible change, its purpose and mechanism, visual proof, testing, review notes, and compatibility considerations. It is detailed and mostly complete; the linked issue i…
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch brennanb2025/accepted-message-survives-quit
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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: d92a4f91-4515-49d0-bfdc-56cbbbd75cd1

📥 Commits

Reviewing files that changed from the base of the PR and between de8bffe and 1eacd65.

📒 Files selected for processing (77)
  • mobile/src/session/MobileNativeChatQueuedMessages.tsx
  • mobile/src/session/mobile-native-chat-controller-contract.ts
  • mobile/src/session/mobile-native-chat-pending-echo.ts
  • mobile/src/session/mobile-structured-queued-message-cards.test.ts
  • mobile/src/session/mobile-structured-queued-message-cards.ts
  • mobile/src/session/use-mobile-structured-agent-session-queued.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • mobile/src/session/use-mobile-structured-queued-message-controls.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-reducer.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-settlement.test.ts
  • src/main/native-chat/agent-session-journal/journal-dispatch-settlement.ts
  • src/main/native-chat/agent-session-journal/journal-queued-messages.ts
  • src/main/native-chat/agent-session-journal/journal-row-builders.ts
  • src/main/native-chat/agent-session-journal/journal-row-schema.ts
  • src/main/native-chat/agent-session-journal/journal-store-contracts.ts
  • src/main/native-chat/agent-session-journal/journal-store.ts
  • src/main/native-chat/agent-session-journal/journal-submission-fold.ts
  • src/main/native-chat/agent-session-journal/journal-unsent-send-hold.test.ts
  • src/main/native-chat/agent-session-journal/journal-unsent-send-hold.ts
  • src/main/native-chat/agent-session-journal/queued-message-holds.ts
  • src/main/native-chat/agent-session-journal/queued-message-pause.test.ts
  • src/main/native-chat/agent-session-journal/queued-message-pause.ts
  • src/main/native-chat/agent-session-journal/queued-message-positions.ts
  • src/main/native-chat/agent-session-journal/queued-message-retention.ts
  • src/main/native-chat/agent-session-journal/queued-message-settlement.ts
  • src/main/native-chat/agent-session-journal/queued-message-store.test.ts
  • src/main/native-chat/agent-session-journal/queued-message-table.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-claude-unproven-stop-send.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-lifetime.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-provider-child-record.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig.test-fixture.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-publication.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-wiring.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restore-without-import.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts
  • src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts
  • src/main/runtime/orchestration/structured-mailbox-pointer-host.ts
  • src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts
  • src/main/runtime/rpc/methods/agent-launch-structured-prompt.ts
  • src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts
  • src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts
  • src/main/runtime/rpc/methods/structured-agent-session-queued-methods.ts
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx
  • src/renderer/src/components/native-chat/structured-agent-session-queued-cards.test.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-outbox.queue-delivery.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • 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/runtime/structured-agent-session-host-capability.ts
  • src/shared/agent-session-journal-types.ts
  • src/shared/agent-session-queued-message-wire.ts
  • src/shared/protocol-version.ts
  • src/shared/structured-agent-session-message-projection.test.ts
  • src/shared/structured-agent-session-outbox.ts
  • src/shared/structured-agent-session-send-disposition.ts
  • tests/e2e/cross-version-wire/kept-card-downgrade.unit.test.ts

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

"turnOffQueueing": "关闭排队",
"withdrawnHold": "在发送前已停止",
"pausedSendFailed": "无法发送。点按发送以重试。",
"pausedKept": "尚未发送。点按发送即可发送。",

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use "点击" for the desktop caption.

This renderer string is the desktop caption. The English source says "press Send". "点按" is the mobile tap verb. On desktop, use "点击".

Proposed fix
-        "pausedKept": "尚未发送。点按发送即可发送。",
+        "pausedKept": "尚未发送。点击发送即可发送。",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"pausedKept": "尚未发送。点按发送即可发送。",
"pausedKept": "尚未发送。点击发送即可发送。",

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — two rough edges in error paths, inline.

Reviewed changes

  • Held-card settlement core. New holdUnsentSends (journal-unsent-send-hold.ts) rejects a send the host accepted and never handed over, creating a kept queue card in the same transaction via holdInTransaction, and placing it at the head of the queue (headOfQueuePositions + new moveQueuedMessages).
  • Open / delivery-loop / close wiring. The open, the delivery loop's first step (replacing rejectQueuedSubmissions) and the close all call holdUnsentSends; quit teardown stops settling and disposes the queue drain, leaving the next open to settle.
  • Source tracking. New AgentJournalSubmissionSource (person|launch|mail|dispatch|continuation|queue) recorded on submissions; only person/launch (or an older build's origin:'client' row) becomes a card.
  • Rejection settlement. rejectedDraftSettlement now returns {kept} for hostRestarted/chatClosed when a person asked; settleRejectedQueuedMessage stores the kept hold; repair-at-open reaches the same answer.
  • Card semantics. A held card pauses nothing, is skipped by the drain, survives /clear carry with its hold, and its local outbox copy is retired rather than shown as Retry.
  • UI un-gating. Desktop and mobile now render published cards (and their actions) regardless of the queue capability; queueCapable only gates delivery, the queueing setting and the steer chord. New pausedKept string in six locales.
  • Tests. New unit/host/cross-version/projection tests plus updates to pause, store, dispatch-settlement, outbox and UI tests.

Two low-severity findings are inline. Both sit on error/edge paths rather than the normal quit or crash flow, which I traced end to end without finding a double-delivery or an unsettled-leak path.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

const { clientMessageId } = submission
const moves = [
// The cards an earlier settlement kept move with the first row.
...(index === 0 ? positions.earlier : []),

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.

This batch's relocation of pre-existing kept cards (positions.earlier) is folded only into the first row's transaction. If the first resolveDispatch throws and its transaction rolls back, the earlier moves are never re-applied, yet every later card is still inserted at the position headOfQueuePositions computed for the moved queue — so a new kept card can land on a position an earlier-kept card still holds, and the documented "behind the cards an earlier settlement kept" order can invert. Only held cards collide, so the drain is unaffected; the observable effect is duplicate position values and a wrong order among held cards.

Technical details
# `earlier` moves are lost when the first row fails

## Affected sites
- `journal-unsent-send-hold.ts:100-108` — `moves` only includes `positions.earlier` for `index === 0`.
- `journal-unsent-send-hold.ts:149-163` — a keep failure falls back to a plain `resolveDispatch(reject)`, discarding the rolled-back moves.
- `journal-unsent-send-hold.ts:200-205` — `first = anchor - earlier.length - placed.length` assumes the `earlier` block moved.

## Trigger
- Cards: `X` kept at position 0, ordinary card `Y` at 1.
- Reopen with unsent `s0` (index 0), `s1`: intended `X->-2, s0->-1, s1->0`.
- `s0`'s append throws; its transaction rolls back (`X` stays at 0, no `s0` card).
- `s1`'s row succeeds and inserts at the precomputed 0 -> `X(0)` and `s1(0)` collide.

## Required outcome
- After a per-row write failure, the earlier block and the newly placed cards must still end at unique, non-overlapping positions (or neither side moves), so the head-of-queue ordering invariant holds regardless of which rows failed.

## Suggested approach
- Apply the `earlier` moves in their own preceding transaction (or recompute `placed` positions after a failure) so a later row never assumes a move that was rolled back.

# Conflicts:
#	src/main/runtime/orchestration/structured-mailbox-pointer-host.ts
#	src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found in this delta.

Reviewed changes

This run covered what changed since the prior Pullfrog review: a merge of origin/main into the branch, whose only substantive effect is re-wiring the PR's submission source tracking onto main's refactored orchestration send path.

  • source routed through sendAgentTurn. StructuredSessionTurnSend gains a required source: AgentJournalHostSendSource (launch | mail | dispatch | continuation), forwarded onto the agentSession.send mutation (send-agent-turn.ts:52, :138).
  • Structured callers carry their source. The mail pointer lane sends source: 'mail' and a structured worker's dispatch preamble sends source: 'dispatch', resolving the two merge conflicts while preserving the values the pre-merge files set directly.
  • Tests updated to match. send-agent-turn.test.ts, send-agent-turn-host.test.ts, structured-mailbox-pointer-host.test.ts and orchestration-structured-worker-session.test.ts assert the new field.
  • Drain quit-gate test adapted. structured-agent-session-unsent-send-hold.test.ts re-hooks from main's removed flushStreamedEvents step to queuedMessages.settlementOwed/settleOwed; it still asserts that a quit beginning mid-step makes no hand-off.
  • Comment-only fix in structured-agent-session-queued-methods.ts (cards are not gated on agent-session.queued-messages.v1).

The two prior error-path findings (the lost earlier position moves on a first-row keep failure, and the delivery loop's lost read-only fail-closed guard) are unchanged by these commits and remain open, so this review is not an approval.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…to its own fixture

The rig fixture grew past the 300-line limit once main's changes merged in.
# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-queued-messages.ts
#	src/main/native-chat/agent-session-journal/journal-store.ts
# Conflicts:
#	src/shared/structured-agent-session-outbox.ts
#	src/shared/structured-agent-session-send-disposition.ts
… still existing

The transcript hid a rejected send while a queued card held it under its id, so the card's Edit or
Delete brought back a "Not sent" row. It now reads the send's own keptAsQueuedMessageId, which the
host records with the rejection, and the live card list is no longer threaded to the transcript or
the delivery notices.
…n journal collaborator

The journal store went past its line limit once main's ledger receipt joined this branch's
transaction hook. The submission and dispatch-transition writes, and what commits in their
transaction, now live in JournalSubmissionWriter; the store's methods delegate to it unchanged.
The schema drops keys it does not list, so a reader that kept a parsed submission would lose
keptAsQueuedMessageId and source, both persisted with the row. The submission schema and the
failure fact it shares with item bodies move to their own modules, with room for both fields.
…2025/accepted-message-survives-quit

# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
#	src/shared/agent-session-journal-schemas.ts
…rns no text

The host answers a resend of a message it kept as a card with that
message's rejected submission, marked keptAsQueuedMessageId. The phone read
it as any rejection: "Message not sent" and the text back in the composer,
while the card showed the same text. It now answers like a queued send, as
the desktop's send disposition does: the id is spent, no error, and the card
holds the text.
…2025/accepted-message-survives-quit

# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-claude-unproven-stop-send.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host-lifetime.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
Quit now stops delivery, the queue drain included, at its first step
(stopDelivery), before teardown drains recovery. The drain-step quit test
runs from that step as well as from the flush.
…2025/accepted-message-survives-quit

# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-store.ts
#	src/main/native-chat/agent-session-journal/queued-message-pause.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig.test-fixture.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found in this delta.

Reviewed changes

This run covered what changed since the prior Pullfrog review (a70aa698): one authored mobile fix and three merges of origin/main (through 990b0ed5cd).

  • A kept resend answers like a queued send on the phone. mobileStructuredSendDelivery now maps a rejected submission carrying keptAsQueuedMessageId to { outcome: 'queued', operationIdSpent: true, error: null }, so the phone shows no "Message not sent" and does not hand the text back to the composer — matching the desktop's disposeStructuredAgentSessionSendResult.
  • Merged origin/main three times. The merges bring main's opaque provider handle (AgentSessionJournalProviderHandle, claudeProviderHandle/codexProviderHandle), the child-close stop refactor, and the OpenCode worker fix. No new wire field or stream frame.
  • Conflict resolutions keep the branch's logic. journal-store.ts keeps the branch's JournalSubmissionWriter delegation; agent-session-journal-schemas.ts keeps AgentSessionFailureFactSchema beside main's openDiscriminatedUnion; the open/close/delivery seams keep holdUnsentSends / holdClosedStructuredAgentSessionSends beside main's new stop machinery; stopDelivery now disposes both the conversation delivery and the queue drain.

I ran pnpm tc:node, pnpm tc:web, and the mobile tsc --noEmit; the focused suites (mobile-structured-send-delivery, journal-unsent-send-hold, queued-message-pause, structured-agent-session-unsent-send-hold) all pass.

The read-only fail-closed finding is resolved: main removed the public AgentSessionJournal.isReadOnly getter, so the merge dropped holdUnsentSends's early return and its writes now throw journal_read_only, aborting the delivery step before ensureProviderChild. The earlier position-move finding (lost earlier moves when the first row's keep fails) is unchanged and still open, so this is not an approval.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…2025/accepted-message-survives-quit

# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-dispatch-reducer.ts
#	src/main/native-chat/agent-session-journal/journal-queued-messages.ts
#	src/main/native-chat/agent-session-journal/journal-row-builders.ts
#	src/main/native-chat/agent-session-journal/journal-row-schema.ts
#	src/main/native-chat/agent-session-journal/journal-store-contracts.ts
#	src/main/native-chat/agent-session-journal/queued-message-table.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts
#	src/main/runtime/orchestration/send-agent-turn-host.test.ts
#	src/main/runtime/orchestration/send-agent-turn.test.ts
#	src/main/runtime/orchestration/send-agent-turn.ts
#	src/shared/agent-session-failure-fact-schema.ts
#	src/shared/agent-session-journal-schemas.ts
…as another build would

#25078 made a card's source required, so the tests that insert a card pass the
person's. The hold's unknown-kind and unreadable-source cases now rewrite the
stored row the way a newer build would leave it, instead of casting a type.
…2025/accepted-message-survives-quit

# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-store-collaborators.ts
#	src/main/native-chat/agent-session-journal/journal-store.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-queued-message-rig.test-fixture.ts
…at quit

Main's #25159 left the host at its line limit; quit's stop now disposes both in one
expression instead of a block.
…2025/accepted-message-survives-quit

# Conflicts:
#	src/renderer/src/runtime/structured-agent-session-host-capability.ts
Bun formats a method's stack frame without its class ("at step"), so the quit
test's caller check never matched, the step was never held, and both cases timed
out once CI ran Vitest on Bun (#25840). Only the drain step heals owed queue
bookkeeping, so the hold needs no caller check.
@brennanb2025
brennanb2025 merged commit 5cf3585 into main Oct 6, 2026
53 of 55 checks passed
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…-history

- #24660: the row writer's planned write is public writeRows (the new step writer
  calls it); the transactional write that also stores the chat's status is
  commitRows, so every path still writes the status row once per transaction.
- #25181: the open settlement plan ends a running call as its turn's row ended
  (runningCallEnd / terminalAgentJournalBody) and revises calls an unproven settle
  closed once a proof names their owner; the status facts read
  requiresTerminalSettlement, the same facts as before. Liveness keeps main's
  lostLiveWorkJournalBody under the per-item roster revision.
- #24660: the open plan settles what a gone process left queued through
  holdUnsentSends (a person's message becomes a held card); its leftovers are
  exactly the queued sends written before the open.
- #25159: reading and settling a chat need no adapter. The persisted tab listing
  lists every tab with a record, startup settles any chat whose record exists,
  and adapterSupportsRecord / hostCanSettleRecord are gone, as on main.
- Line caps: the runtime's startup step moves into its own layer
  (orca-runtime-structured-agent-session-startup-step.ts), and the host exposes
  its restore as one `startup` surface instead of four pass-through methods.
- The golden digest is unchanged, so the status rules stay at 3.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…essage

#24660 records on each submission who it is from (`source.kind`) so a restart
or a close keeps only a person's unsent send as a card. That record and its
keep rule are kept exactly; the kind is now read off what the send carries,
in one place (sendPlan): a person's send ('user': a client's, or a launch's
first prompt the host sends for them), another agent's message whose body
names its sender ('agent'), or neither (a dispatch preamble, a restart
continuation). The queued card's own source column is dropped again: the
sender is on the card's body, and kept cards are written without it.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…r-stop

Main's #24660 keeps a person's unsent message as a card after a quit, crash or
close, and it reads the submission's origin (client or host) to decide which
sends to keep. This branch had removed origin and the client send's userSend
flag as unused, so they are restored as main has them; comments now say origin
only decides what a restart or a close keeps, never what lifts a pause (any
accepted turn or Resume still does). The queue drain's quit gate is kept once,
with main's re-check right before the hand-off.

Main's new kept-card tests are adjusted to this branch's rule that the
restart's hold is never published (queuePause null; the cards still wait).
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…is host publishes no restart pause

Main's #24660 test published queuePause 'restarted', which this branch's wire
type no longer lists, so the mobile tests typecheck ratchet failed.
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
…nd nothing sends by itself after a restart (#24586)

* fix(native-chat): drop the queue-paused header and Resume button

A Stop, a restart or /clear holds the queued cards. The hold stays; only the
header row naming why, and its Resume button, go. A held card shows no
caption, and its own Steer, or any new message, releases the queue.

* test(native-chat): type the unknown hold reason a newer host may publish

* fix(native-chat): a held card offers Send, not Steer, when no turn runs

Steer vs Send now follows whether a turn is running, not the card's hold,
so a card held after a Stop, a restart or /clear reads Send.

* fix(native-chat): the queue sends past held cards instead of stalling behind them

A card queued after a Stop (or written after a restart or /clear) sent only
once the cards held before it were released; with no header to explain or
release the hold, it sat silently. The next sendable card now skips held
cards; a returned card still blocks what is behind it.

* fix(native-chat): the queue's send of a card is the person's turn, so held cards follow it

After a Stop, a card queued later sent past the held cards, but the queue
recorded that send as Orca's own turn. It never ended the Stop's pause, so
the held cards then waited forever with nothing on the card saying why.

A queued card is always something the person wrote: only the client send
RPC may now create one. The queue's send of it is therefore recorded as the
person's turn, which ends the Stop's pause once the agent takes it, and the
held cards then drain in order.

* fix(native-chat): a queued card carries its author, so the queue's send of it is that author's turn

Main now lets Orca's own sends ask to queue (sendAgentTurn's 'queue' delivery),
so "every card is a person's" no longer holds by refusing host sends. Each card
records who wrote it (the submission's client/host vocabulary) in a new nullable
column; the drain records that origin, so a person's card ends a Stop's pause
and Orca's does not. /clear carries the author. Rows from before the column
read as a person's. The userSend-only admission gate is removed.

* docs(native-chat): state why an unrecorded card author reads as a person's

* fix(native-chat): a restart holds only cards written before it, and an idle held queue offers Resume

A restart's pause held every waiting card, including one a person typed after the restart while
Orca's own continuation ran, and nothing released it except a per-card Send. It now holds only
cards another host process wrote, the same way a Stop holds only cards queued before it.

The composer's primary button becomes Resume (Play) while nothing is typed, no turn runs and the
host holds a card Resume would send, whatever held it (Stop, restart or /clear). It calls the
existing agentSession.queuedMessagesResume, guarded against a second press in flight.

A card nothing holds keeps the run going between a turn's end and the queue's send of it, so its
Steer no longer flips to Send for the frame in between.

* fix(native-chat): the host publishes which pause holds each queued card

The host published one pause for the whole queue, so a client held every waiting card while it was
set. Between a turn's end and the queue's send of a card queued after a Stop or restart, the
composer could flash Resume and the cards Send, and a card queued after a Stop lost its
"Waiting for your answer" caption.

Each published card now carries an optional `heldBy`: the pause holding it, or null, derived from
the same rule the drain reads. A client holds only those cards; against a host without the field
it falls back to the queue-level pause.

* test(native-chat): Resume needs the queue capability and is disabled whenever Send is

* docs(native-chat): describe per-card holds in the queue contract and table comments

* fix(native-chat): the composer goes from Resume straight to Stop, and Resume returns focus

After Resume, the host lifts the hold in one update and sends the first card in a later one. In
between nothing was running, so the composer's button flashed a disabled Send. A card nothing
holds now keeps the queue's run going for the button too: an empty composer shows Stop, disabled
until the turn starts. Not when the host refuses every send (a rewind whose outcome is unknown,
read from its status), where nothing is coming. The same fix removes the Stop, Send, Stop flip
between queued turns.

Resume disables the button, which dropped keyboard focus; focus now returns to the composer.

* fix(native-chat): the host names the card its queue sends next, so the chat stays working across the gap

A turn's end, or a Resume, and the queue's send of the next card commit as two host updates. In
between nothing was running, so the working status, timer, pickers and composer button flipped
for one update. The client guessed the drain from its own copy of the host's gates, which missed a
/clear-replaced source and covered only the button.

The queue publication now carries `nextQueuedMessageId`: the drain's own next card through the
drain's own gate (`nextStructuredQueuedMessage`, which the drain step now calls), null whenever the
host would refuse the send. The client derives one fact, the queue is about to send, and every
working reader follows it; Stop stays disabled until a turn can be stopped. The client-side copy of
the gates and the status-feed rewind read are removed.

* test(native-chat): the queue's next card survives the coalescer, the reducer and a history page

* test(native-chat): build the snapshot that names the next card through its helper

* feat(native-chat): a held queue keeps its header row, and a new message asks before passing it

The queue's header row ("Queue paused because you interrupted", or Orca
restarted, or you cleared the conversation) comes back above the cards it
holds, with Resume; it names the oldest held card's pause, as the host
publishes it per card, and hides over cards held only on their own or
returned. The header's Resume and the composer's share one in-flight guard.

A held card reads Steer again whether or not a turn runs; a card held on its
own or returned keeps Send.

Sending a message while the header shows (Enter or the button) first asks
"Send message?": Clear queue deletes every card and then sends (a failed
delete sends nothing), Send message sends and keeps the cards, which follow
the new turn, and dismissing sends nothing and keeps the draft. Host
commands send as they are.

* fix(native-chat): the paused row goes while your own message is on its way to lift it

After "Send message" over a held queue, the row kept saying "Queue paused…"
until the agent accepted the new turn. The chat now reads that gap from the
outbox: while this composer's direct send is recorded by the host and not yet
accepted, the controller shows no paused row (and so no Resume or
confirmation). A refusal settles the entry and the row comes back, since the
hold did not lift. Orca's own sends never enter this outbox, and the queue's
send of a card goes under a fresh id, so neither hides it. Nothing is stored.

* fix(native-chat): a "Send message?" choice is taken once, and a failed Clear queue is one toast

The closing dialog stays mounted and clickable through its exit animation,
and a double-click or a held Enter lands twice before any re-render, so
Send message (or Clear queue) could send the captured message twice. The
pending send now lives in a ref that the first choice takes; a second one
finds nothing.

Clear queue deletes one card at a time and stops at the first failure, so a
failed press shows one toast instead of one per card.

The dialog keeps its compact width at desktop sizes and the primitive's
narrow-window gutter (`max-w-sm sm:max-w-sm`, as the other compact
confirmations).

* fix(native-chat): Clear queue's message goes out once, and keeps text typed while it waits

After Clear queue, the message waited in the composer while the cards were
deleted one by one. A second Enter in that window sent it again, and text
typed meanwhile was wiped when the chained send was accepted.

From the Clear queue choice until its message has gone out, the composer's
structured send does nothing. The chained send (and Send message's) now
carries the composition it was taken from, and the composer is cleared on
acceptance only if it still holds exactly that, as host commands already do.

Also: the v1 contract comment names `nextQueuedMessageId` and its absent-
means-null fallback, and the own-send check returns at once on an empty
outbox.

* fix(native-chat): the queue carries on after any turn, in order, and a restart sends nothing by itself

- Any accepted turn ends a Stop's or a /clear's pause, whoever sent it (a person,
  Orca's own messages, or the queue), and so does Resume. The card and submission
  author fields that only fed the old person-only rule are gone.
- The queue sends strictly in order: a card never overtakes a held one.
- After a restart nothing sends by itself and no paused row shows: the chat's next
  turn (the carry-on, or the person's own message) runs first, then the cards.
- Resume and "Send message?" are offered only while nothing runs and no prompt waits.

* fix(native-chat): after a restart no queue pause shows, and a card written before the next turn waits for it too

* fix(native-chat): a quit hands no queued card off, and the paused row goes while any turn that will lift it is on its way

- The queue stops handing cards off when the host tears down. A card sent during
  a quit was refused at close, and that refused send withdrew the chat's restart
  offer, so resuming after the relaunch sent nothing.
- The host publishes no pause while a turn sent after it (your message, Steer, or
  Orca's own) waits for the agent; a refusal shows it again. This replaces the
  client's own-send check.
- A card written after a restart is an ordinary card again: it waits while any
  card from before the restart still waits.

* refactor(native-chat): the host's paused-row-while-a-turn-is-on-its-way check in one expression

* refactor(native-chat): the composer's queue Resume rides the structured transport beside the held queue

* fix(native-chat): the "Send message?" choice ends with the pause it asked about; tests follow main's draft props

- The open dialog closes when the queue's pause lifts under it (Orca's mail, another client's
  Resume, any accepted turn): nothing is sent, the draft stays, and the next Enter sends as
  usual. The pending choice records the hold it was asked under; nothing new is stored.
- The composer-field Resume test passes main's dropScopeKey/draftScopeKey.
- The dialog test expects main's rule: only the sent text leaves the composer.

* test(mobile): a host-kept card's test stands in a Stop's pause, as this host publishes no restart pause

Main's #24660 test published queuePause 'restarted', which this branch's wire
type no longer lists, so the mobile tests typecheck ratchet failed.
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
… and follows the chat's next turn (#25960)

* fix(native-chat): drop the queue-paused header and Resume button

A Stop, a restart or /clear holds the queued cards. The hold stays; only the
header row naming why, and its Resume button, go. A held card shows no
caption, and its own Steer, or any new message, releases the queue.

* test(native-chat): type the unknown hold reason a newer host may publish

* fix(native-chat): a held card offers Send, not Steer, when no turn runs

Steer vs Send now follows whether a turn is running, not the card's hold,
so a card held after a Stop, a restart or /clear reads Send.

* fix(native-chat): the queue sends past held cards instead of stalling behind them

A card queued after a Stop (or written after a restart or /clear) sent only
once the cards held before it were released; with no header to explain or
release the hold, it sat silently. The next sendable card now skips held
cards; a returned card still blocks what is behind it.

* fix(native-chat): the queue's send of a card is the person's turn, so held cards follow it

After a Stop, a card queued later sent past the held cards, but the queue
recorded that send as Orca's own turn. It never ended the Stop's pause, so
the held cards then waited forever with nothing on the card saying why.

A queued card is always something the person wrote: only the client send
RPC may now create one. The queue's send of it is therefore recorded as the
person's turn, which ends the Stop's pause once the agent takes it, and the
held cards then drain in order.

* fix(native-chat): a queued card carries its author, so the queue's send of it is that author's turn

Main now lets Orca's own sends ask to queue (sendAgentTurn's 'queue' delivery),
so "every card is a person's" no longer holds by refusing host sends. Each card
records who wrote it (the submission's client/host vocabulary) in a new nullable
column; the drain records that origin, so a person's card ends a Stop's pause
and Orca's does not. /clear carries the author. Rows from before the column
read as a person's. The userSend-only admission gate is removed.

* docs(native-chat): state why an unrecorded card author reads as a person's

* fix(native-chat): a restart holds only cards written before it, and an idle held queue offers Resume

A restart's pause held every waiting card, including one a person typed after the restart while
Orca's own continuation ran, and nothing released it except a per-card Send. It now holds only
cards another host process wrote, the same way a Stop holds only cards queued before it.

The composer's primary button becomes Resume (Play) while nothing is typed, no turn runs and the
host holds a card Resume would send, whatever held it (Stop, restart or /clear). It calls the
existing agentSession.queuedMessagesResume, guarded against a second press in flight.

A card nothing holds keeps the run going between a turn's end and the queue's send of it, so its
Steer no longer flips to Send for the frame in between.

* fix(native-chat): the host publishes which pause holds each queued card

The host published one pause for the whole queue, so a client held every waiting card while it was
set. Between a turn's end and the queue's send of a card queued after a Stop or restart, the
composer could flash Resume and the cards Send, and a card queued after a Stop lost its
"Waiting for your answer" caption.

Each published card now carries an optional `heldBy`: the pause holding it, or null, derived from
the same rule the drain reads. A client holds only those cards; against a host without the field
it falls back to the queue-level pause.

* test(native-chat): Resume needs the queue capability and is disabled whenever Send is

* docs(native-chat): describe per-card holds in the queue contract and table comments

* fix(native-chat): the composer goes from Resume straight to Stop, and Resume returns focus

After Resume, the host lifts the hold in one update and sends the first card in a later one. In
between nothing was running, so the composer's button flashed a disabled Send. A card nothing
holds now keeps the queue's run going for the button too: an empty composer shows Stop, disabled
until the turn starts. Not when the host refuses every send (a rewind whose outcome is unknown,
read from its status), where nothing is coming. The same fix removes the Stop, Send, Stop flip
between queued turns.

Resume disables the button, which dropped keyboard focus; focus now returns to the composer.

* fix(native-chat): the host names the card its queue sends next, so the chat stays working across the gap

A turn's end, or a Resume, and the queue's send of the next card commit as two host updates. In
between nothing was running, so the working status, timer, pickers and composer button flipped
for one update. The client guessed the drain from its own copy of the host's gates, which missed a
/clear-replaced source and covered only the button.

The queue publication now carries `nextQueuedMessageId`: the drain's own next card through the
drain's own gate (`nextStructuredQueuedMessage`, which the drain step now calls), null whenever the
host would refuse the send. The client derives one fact, the queue is about to send, and every
working reader follows it; Stop stays disabled until a turn can be stopped. The client-side copy of
the gates and the status-feed rewind read are removed.

* test(native-chat): the queue's next card survives the coalescer, the reducer and a history page

* test(native-chat): build the snapshot that names the next card through its helper

* feat(native-chat): a held queue keeps its header row, and a new message asks before passing it

The queue's header row ("Queue paused because you interrupted", or Orca
restarted, or you cleared the conversation) comes back above the cards it
holds, with Resume; it names the oldest held card's pause, as the host
publishes it per card, and hides over cards held only on their own or
returned. The header's Resume and the composer's share one in-flight guard.

A held card reads Steer again whether or not a turn runs; a card held on its
own or returned keeps Send.

Sending a message while the header shows (Enter or the button) first asks
"Send message?": Clear queue deletes every card and then sends (a failed
delete sends nothing), Send message sends and keeps the cards, which follow
the new turn, and dismissing sends nothing and keeps the draft. Host
commands send as they are.

* fix(native-chat): the paused row goes while your own message is on its way to lift it

After "Send message" over a held queue, the row kept saying "Queue paused…"
until the agent accepted the new turn. The chat now reads that gap from the
outbox: while this composer's direct send is recorded by the host and not yet
accepted, the controller shows no paused row (and so no Resume or
confirmation). A refusal settles the entry and the row comes back, since the
hold did not lift. Orca's own sends never enter this outbox, and the queue's
send of a card goes under a fresh id, so neither hides it. Nothing is stored.

* fix(native-chat): a "Send message?" choice is taken once, and a failed Clear queue is one toast

The closing dialog stays mounted and clickable through its exit animation,
and a double-click or a held Enter lands twice before any re-render, so
Send message (or Clear queue) could send the captured message twice. The
pending send now lives in a ref that the first choice takes; a second one
finds nothing.

Clear queue deletes one card at a time and stops at the first failure, so a
failed press shows one toast instead of one per card.

The dialog keeps its compact width at desktop sizes and the primitive's
narrow-window gutter (`max-w-sm sm:max-w-sm`, as the other compact
confirmations).

* fix(native-chat): Clear queue's message goes out once, and keeps text typed while it waits

After Clear queue, the message waited in the composer while the cards were
deleted one by one. A second Enter in that window sent it again, and text
typed meanwhile was wiped when the chained send was accepted.

From the Clear queue choice until its message has gone out, the composer's
structured send does nothing. The chained send (and Send message's) now
carries the composition it was taken from, and the composer is cleared on
acceptance only if it still holds exactly that, as host commands already do.

Also: the v1 contract comment names `nextQueuedMessageId` and its absent-
means-null fallback, and the own-send check returns at once on an empty
outbox.

* fix(native-chat): the queue carries on after any turn, in order, and a restart sends nothing by itself

- Any accepted turn ends a Stop's or a /clear's pause, whoever sent it (a person,
  Orca's own messages, or the queue), and so does Resume. The card and submission
  author fields that only fed the old person-only rule are gone.
- The queue sends strictly in order: a card never overtakes a held one.
- After a restart nothing sends by itself and no paused row shows: the chat's next
  turn (the carry-on, or the person's own message) runs first, then the cards.
- Resume and "Send message?" are offered only while nothing runs and no prompt waits.

* fix(native-chat): after a restart no queue pause shows, and a card written before the next turn waits for it too

* fix(native-chat): a quit hands no queued card off, and the paused row goes while any turn that will lift it is on its way

- The queue stops handing cards off when the host tears down. A card sent during
  a quit was refused at close, and that refused send withdrew the chat's restart
  offer, so resuming after the relaunch sent nothing.
- The host publishes no pause while a turn sent after it (your message, Steer, or
  Orca's own) waits for the agent; a refusal shows it again. This replaces the
  client's own-send check.
- A card written after a restart is an ordinary card again: it waits while any
  card from before the restart still waits.

* refactor(native-chat): the host's paused-row-while-a-turn-is-on-its-way check in one expression

* refactor(native-chat): the composer's queue Resume rides the structured transport beside the held queue

* fix(native-chat): the "Send message?" choice ends with the pause it asked about; tests follow main's draft props

- The open dialog closes when the queue's pause lifts under it (Orca's mail, another client's
  Resume, any accepted turn): nothing is sent, the draft stays, and the next Enter sends as
  usual. The pending choice records the hold it was asked under; nothing new is stored.
- The composer-field Resume test passes main's dropScopeKey/draftScopeKey.
- The dialog test expects main's rule: only the sent text leaves the composer.

* fix(native-chat): a message kept after a quit or close waits like every other card, and follows the chat's next turn

A message Orca accepted but never handed to the agent before a quit, crash or
close came back as a card held on its own ("Not sent yet — press Send"): the
queue skipped past it, so later cards sent first, and only the person's own
Send released it. It is now an ordinary card at the head of the queue that
waits, with every card the chat closed with, for the chat's next accepted turn.

One rule for a chat that was not running, derived from the journal: when this
host first opens a chat (after a restart or crash), or a person closes it, and
cards are waiting, a reopen mark is written (a tombstone carrier key, like the
Stop and Resume marks). The cards queued before it wait until a turn is
accepted or Resume comes after it; nothing sends by itself, and no paused row
shows, a Stop's included. The idle sweep's own eviction writes nothing and
changes nothing the person sees. A mark that cannot be written leaves the open
working and holds from the open itself until the next turn; the next open marks
again. A rewind restates the mark. /clear's carried cards also wait unshown.

This replaces the host-instance comparison and its adoption write, and the
'kept' hold (stored 'kept' and legacy 'stopped' holds now read as none).

* fix(native-chat): mark the reopen in the one open path, and keep an idle chat with waiting cards open

Review round 1: the startup restore opened chats past the per-host first-open
mark, so their cards could send by themselves after a quit or crash; a card
mid-hand-off at the open got no mark; a close that left the chat open re-marked
after every new send.

- Every open marks when a card waits, or is mid-hand-off with its send unanswered.
- The idle sweep keeps a chat's handle while cards wait, so its eviction never
  reopens one and stays invisible; it drops it on the next sweep once they leave.
- A person's close marks once; its delivery re-check marks only when it settled
  a send.
- A failed mark holds from where the mark would have gone.
- A Stop made after a reopen shows its row.
- The rig's restart is a real quit and relaunch.

* fix(native-chat): review round 2: restore the dropped Resume and failed-Stop tests; a late mark starts where the chat stopped

- Restores eight tests the previous commit dropped by mistake.
- A mark the delivery loop or a close's re-check writes after a later send starts
  where the chat stopped, so that send still lifts it; a later mark never narrows
  an earlier, wider one.
- Only the idle sweep's own close keeps a chat with a card waiting (or mid-hand-off)
  open, and it still releases an ended child's lease first; a person's close
  drops it as before.

* test(mobile): a host-kept card's test stands in a Stop's pause, as this host publishes no restart pause

Main's #24660 test published queuePause 'restarted', which this branch's wire
type no longer lists, so the mobile tests typecheck ratchet failed.

* refactor(native-chat): settle a restart's leftovers and mark them in one host-lifetime step

Keeps the delivery loop under its line limit after main's Stopping change; no
behaviour change.

* test(native-chat): a kept card's Resume and failed-mark tests quit through the held start's release

Main's #25152 holds the start these tests send into; quitting without releasing it left the quit waiting.
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