Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
4f20e4b
fix(native-chat): decide Claude restart delivery by the message's own…
brennanb2025 Sep 28, 2026
07368bb
perf(native-chat): read Claude history on restart only for sends that…
brennanb2025 Sep 28, 2026
f1ba667
fix(native-chat): find a Claude send folded or merged into a running …
brennanb2025 Sep 28, 2026
2f34ca0
test(native-chat): pin a running turn keeping a Claude send unconfirmed
brennanb2025 Sep 28, 2026
f3c6bf1
docs(native-chat): say where Claude keeps a folded frame's id
brennanb2025 Sep 28, 2026
4e261d0
fix(native-chat): keep a merged Claude send unconfirmed once the row'…
brennanb2025 Sep 28, 2026
2aeb8b0
test(native-chat): pin the Claude adapter sending a frame under its r…
brennanb2025 Sep 28, 2026
50b9bf2
fix(native-chat): decide a restarted Claude send only by finding its …
brennanb2025 Sep 28, 2026
9a773b5
Merge remote-tracking branch 'origin/main' into brennanb2025/sta-7416…
brennanb2025 Sep 28, 2026
bfd12f7
revert(native-chat): drop the transcript id lookup for restarted sends
brennanb2025 Sep 28, 2026
460cd0f
fix(native-chat): end a restarted session with one row instead of rea…
brennanb2025 Sep 28, 2026
f00a880
fix(native-chat): word the restart row and retire the unconfirmed-id …
brennanb2025 Sep 28, 2026
632e5eb
fix(native-chat): draw a send left in doubt as sent, with no barrier …
brennanb2025 Sep 28, 2026
8083343
test(native-chat): read the new cases' calls without casts
brennanb2025 Sep 28, 2026
ddd9c15
test(native-chat): name the drawn-as-sent projection cases plainly
brennanb2025 Sep 28, 2026
96134dc
test(native-chat): read the sent requests by shape, not through the n…
brennanb2025 Sep 28, 2026
0140e10
fix(native-chat): keep a live unknown send until it settles, so a lat…
brennanb2025 Sep 28, 2026
ef56226
docs(native-chat): say an older host's not-delivered send carries no …
brennanb2025 Sep 28, 2026
9d62068
chore: restore pnpm-lock.yaml
brennanb2025 Sep 28, 2026
84c3bed
Merge origin/main into brennanb2025/sta-7416-delivery-by-message-id
brennanb2025 Sep 28, 2026
d25b69b
fix(native-chat): draw an older host's restart-doubted send as sent too
brennanb2025 Sep 28, 2026
a72f755
refactor(native-chat): decide a send's settlement in one classifier
brennanb2025 Sep 28, 2026
328fbed
fix(native-chat): settle a dispatch state this build cannot place as …
brennanb2025 Sep 28, 2026
0862016
fix(native-chat): keep a failure row outside a settled turn's fold
brennanb2025 Sep 28, 2026
2ce2803
docs(agent-status): list a crash-doubted send as a request
brennanb2025 Sep 28, 2026
fb06b52
docs(native-chat): name failure rows where the fold skips them; a dou…
brennanb2025 Sep 28, 2026
b75ef65
Merge remote-tracking branch 'origin/main' into brennanb2025/sta-7416…
brennanb2025 Sep 28, 2026
7a97fdb
test(native-chat): name the settlement classifier's input for the sub…
brennanb2025 Sep 28, 2026
4002f3c
fix(native-chat): explain a send retired in doubt with no exit or res…
brennanb2025 Sep 29, 2026
d6d00bf
test(native-chat): pin that the unanswered-send row stays outside a f…
brennanb2025 Sep 29, 2026
0a9a356
fix(native-chat): say Claude stopped when its exit is settled after t…
brennanb2025 Sep 29, 2026
b7c37f7
revert(native-chat): drop the "didn't respond to your last message" row
brennanb2025 Sep 29, 2026
1c50e6e
fix(native-chat): keep an agent running while a send it took is unans…
brennanb2025 Sep 29, 2026
03e44d2
fix(native-chat): keep a failure row in view only when the turn ended…
brennanb2025 Sep 29, 2026
c309dfc
feat(native-chat): draw a crash-doubted send as sent on released clie…
brennanb2025 Sep 29, 2026
71dd198
test(native-chat): assert the projected stream without a type assertion
brennanb2025 Sep 29, 2026
a8949ab
fix(native-chat): name the crash-doubted send capability without a ra…
brennanb2025 Sep 29, 2026
a0ca713
fix(native-chat): publish an older host's "not delivered" as sent to …
brennanb2025 Sep 29, 2026
6e7ebc5
fix(native-chat): keep only the failure a turn ended in outside its fold
brennanb2025 Sep 29, 2026
cd2da8e
refactor(native-chat): read the in-flight send at teardown through th…
brennanb2025 Sep 29, 2026
7cb874a
Merge origin/main into brennanb2025/sta-7416-delivery-by-message-id
brennanb2025 Sep 29, 2026
c2004eb
test(native-chat): read the Claude crash-restart journal as a current…
brennanb2025 Sep 29, 2026
f395e98
Merge remote-tracking branch 'origin/main' into brennanb2025/sta-7416…
brennanb2025 Sep 29, 2026
f71d13b
fix(i18n): drop the wait-for-setup help keys main removed, which the …
brennanb2025 Sep 29, 2026
e69eee9
Merge remote-tracking branch 'origin/main' into brennanb2025/sta-7416…
brennanb2025 Sep 29, 2026
0e4263f
fix(native-chat): give a doubted send a way to die at its turn end, a…
brennanb2025 Sep 29, 2026
c709f01
fix(native-chat): word a crash row by the boundary it crossed, not by…
brennanb2025 Sep 29, 2026
58f406f
test(native-chat): type the turn-end and crash-boundary fixtures agai…
brennanb2025 Sep 29, 2026
8e25362
fix(native-chat): settle a doubted send at the provider's turn-over, …
brennanb2025 Sep 29, 2026
3361da8
fix(native-chat): keep a failed turn-over settle from stalling the ev…
brennanb2025 Sep 29, 2026
76c2d5b
test(codex): leave the cast journal fake untouched; the turn-over set…
brennanb2025 Sep 29, 2026
814cf11
test(codex): pin that a subagent thread's turn end leaves the user's …
brennanb2025 Sep 29, 2026
6ea9c3d
Merge origin/main into brennanb2025/sta-7416-delivery-by-message-id
brennanb2025 Sep 29, 2026
726cf30
fix(native-chat): read main's two new send-state checks through the s…
brennanb2025 Sep 29, 2026
2586cf9
test(codex): give main's turn-grouping fold rows the failure field th…
brennanb2025 Sep 29, 2026
c9ecc80
test(native-chat): pin that a Stop naming no turn settles the send it…
brennanb2025 Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/reference/agent-status-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,12 @@ ingests the summary into the hook server as a status row:

Sessions with no request (`status === null`) produce no row. A request is a
turn record, an assistant message, a user message the provider journaled itself
(history, an older host), an accepted or unanswered send, or a send the agent or
its start refused; a send that was withdrawn, or left undelivered by a
restart or a close, fails nobody and makes nothing listable.
(history, an older host), an accepted or unanswered send, a send left in doubt
by a crash, a dead agent, its turn's end, a Stop or a start that threw (drawn as
sent; it adds no verdict of its own), or a
send the agent or its start refused; a send that was withdrawn, or refused
because a restart or a close came before it was handed over, fails nobody and
makes nothing listable.
`summary.turnOutcome` is the latest request's verdict: its turn's outcome, or
`failure` for a send the agent or its start refused (a send that joined a running
turn is answered by that turn). The row also publishes `interrupted` from
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/remote-wire-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@ The cross-version suite derives the old client's list by removing this capabilit
baseline's own list, per the rule above, so the downgrade stays exercised after a release ships
it.

The same boundary carries a second downgrade, `agent-session.crash-doubted-send.v1`
(`structured-agent-session-recovered-send-capability.ts`). A send a crash or a dead agent left in
doubt for good (`unknown` with `recovered`) is drawn as an ordinary sent message, as is an older
host's inferred `not_delivered`; a client that predates that reading drew the first unconfirmed,
with a Retry that held its queue, and hid the second. So whatever
`structuredAgentSessionSubmissionSettlement` reads as sent reaches such a client as `accepted`, the
one state it already draws as sent, on history, the live stream and a replayed send's reply. The
journal keeps the real facts. Both compositions live in
`structured-agent-session-client-projection.ts`, so history and the stream cannot disagree.

## Known debt: JSON-RPC errors drop Node's string code

An error raised on an SSH host crosses the relay as JSON-RPC, and
Expand Down
22 changes: 11 additions & 11 deletions mobile/src/session/mobile-structured-send-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,30 @@ function accepted(
}

describe('mobileStructuredSendDelivery', () => {
it('keeps the operation id for every unknown, host-recorded or ack-lost', () => {
// The one answer that may be a delivery. Spending the id here turns the next
// identical send into a second copy in front of the model.
it('keeps the operation id when the reply was lost, so a retry stays a replay', () => {
// The host may never have recorded it; rotating here is what sent one message five times.
expect(mobileStructuredSendDelivery({ status: 'unknown' })).toEqual({
outcome: 'unknown',
operationIdSpent: false,
error: null
})
expect(mobileStructuredSendDelivery(accepted('unknown'))).toEqual({
outcome: 'unknown',
operationIdSpent: false,
error: null
})
})

it('reports a written send as sent and spends its id', () => {
// `pending` is written and awaiting the provider's acknowledgement — not doubt.
for (const dispatchState of ['accepted', 'pending'] as const) {
it('reports a written send as sent and spends its id, in doubt or not', () => {
// `pending` awaits the provider's acknowledgement; `unknown` is doubt the host recorded and
// the chat draws as sent. Typing it again is a new message, as on the desktop.
for (const dispatchState of ['accepted', 'pending', 'unknown'] as const) {
expect(mobileStructuredSendDelivery(accepted(dispatchState))).toEqual({
outcome: 'accepted',
operationIdSpent: true,
error: null
})
}
expect(mobileStructuredSendDelivery(accepted('rejected', 'not_delivered'))).toEqual({
outcome: 'accepted',
operationIdSpent: true,
error: null
})
})

it('does not report a retained payload replay as a new accepted send', () => {
Expand Down
18 changes: 10 additions & 8 deletions mobile/src/session/mobile-structured-send-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@
// ledger and never puts it back on the wire. Releasing the id turns that replay
// into a genuine second delivery, which is why only a settled answer releases it:
//
// accepted/pending — the send happened. The id is spent; a later identical
// message is a new message and must carry a new id.
// accepted/pending/unknown — the host recorded the send, and the chat draws it
// as sent even when a crash or a dead agent left its delivery in doubt. The id
// is spent; a later identical message is a new message, which is how the chat
// continues, and must carry a new id.
// rejected — a terminal refusal or rejected submission spends a fresh id. A
// pending-admission refusal, or any refusal after earlier transport doubt,
// keeps it because neither proves a retained delivery did not happen.
// unknown — the one answer that KEEPS its id, whether it came from the host or
// from an ack-loss on the way back. The message may be with the provider, so
// the retry has to stay a replay. Rotating here is what sent one message to a
// model five times.
// no answer — an ack-loss on the way back KEEPS its id: the host may not have
// recorded the message, so the retry has to stay a replay. Rotating here is
// what sent one message to a model five times.

import type { AgentSessionSendResult } from '../../../src/shared/agent-session-wire'
import { agentSessionRefusalOperationState } from '../../../src/shared/agent-session-refusal-retry'
import { structuredAgentSessionRejectionNotice } from '../../../src/shared/structured-agent-session-send-disposition'
import { structuredAgentSessionSubmissionSettlement } from '../../../src/shared/structured-agent-session-submission-settlement'
import type { MobileNativeChatSendOutcome } from './mobile-native-chat-send'
import type { StructuredAgentSessionMutationCallResult } from './mobile-structured-agent-session-rpc'

Expand Down Expand Up @@ -61,10 +63,10 @@ export function mobileStructuredSendDelivery(
}
}
const submission = result.value.submission as AgentSessionSendResult['submission'] | undefined
if (!submission || submission.dispatchState === 'unknown') {
if (!submission) {
return { outcome: 'unknown', operationIdSpent: false, error: null }
}
if (submission.dispatchState === 'rejected') {
if (structuredAgentSessionSubmissionSettlement(submission) === 'refused') {
return {
outcome: 'rejected',
operationIdSpent: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,18 +171,19 @@ describe('mobile structured send operation journal', () => {
clientMessageId: operationId,
fence: 1,
payloadFingerprint,
dispatchState: 'unknown' as const,
dispatchState: 'pending' as const,
providerItemId: null,
reason: 'ack lost',
reason: null,
submittedAt: NOW,
resolvedAt: NOW
resolvedAt: null
}

await clearMobileStructuredSettledSendOperations({ submissions: [submission] })
expect(values.size).toBe(1)

// Doubt the host recorded is settled too: the chat draws it as sent.
await clearMobileStructuredSettledSendOperations({
submissions: [{ ...submission, dispatchState: 'accepted', reason: null }]
submissions: [{ ...submission, dispatchState: 'unknown', reason: 'ack lost' }]
})
expect(values.size).toBe(0)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export async function clearMobileStructuredSettledSendOperations(input: {
}): Promise<void> {
const settled = new Set(
input.submissions.flatMap((submission) =>
submission.dispatchState === 'accepted' || submission.dispatchState === 'rejected'
submission.dispatchState !== 'pending'
? [`${submission.payloadFingerprint}\u0000${submission.clientMessageId}`]
: []
)
Expand Down
2 changes: 2 additions & 0 deletions src/cli/runtime/websocket-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { addEnvironmentFromPairingCode } from './environments'
import { RuntimeClientError } from './types'
import {
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY,
AGENT_SESSION_RECOVERED_SEND_CAPABILITY,
AGENT_SESSION_TURN_ITEM_CAPABILITY,
AGENT_SESSION_BACKGROUND_TASK_STOP_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
Expand Down Expand Up @@ -76,6 +77,7 @@ describe('CLI remote WebSocket transport', () => {
AGENT_SESSION_BACKGROUND_TASK_STOP_CAPABILITY,
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY,
AGENT_SESSION_TURN_ITEM_CAPABILITY,
AGENT_SESSION_RECOVERED_SEND_CAPABILITY,
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
Expand Down
Loading
Loading