Repository navigation
refactor(native-chat): move the phone's send-id retention rule into shared code - #24915
brennanb2025 wants to merge 2 commits into
Conversation
…hared code The phone decides when a retained send operation id is spent, and keys retained ids by session + trimmed text + attachment fingerprints. Both now live in src/shared/structured-agent-session-send-retention.ts so the desktop can reuse them. The phone imports them from there; no behaviour change, and the saved key domains are unchanged so retained ids on an upgraded phone still match.
…'s tests The reliability gate still named the phone's old delivery test, which the move deleted, so the manifest check failed. It now runs the shared test, with fresh evidence runs. The send-answer fixture moves to src/shared so the shared and phone tests use one copy, and the phone's send outcome type becomes an alias of the shared one. The saved-key pin now rebuilds the operation domain literally, so renaming it fails the test.
Review summaryThe problem. In the desktop's native chat, a message that fails to send stays in the conversation with a red line and a Retry button. A planned follow-up gives the text back to the composer instead. That is only safe if sending the same text again can't deliver it twice. The phone already avoids double delivery: it keeps the operation id of any send whose fate is unclear, and reuses it when the same text is sent again. This PR moves that phone rule into shared code so the follow-up could use it on the desktop. What changes for users. Nothing, on the phone or the desktop. The phone runs the same rule from its new location, and no desktop code uses it yet. What the review checked.
Fixed in this round (PR description only, no code change).
Deferred.
Why this stays a draft. Nothing in the code is wrong. Whether this PR should land depends on the follow-up's design, which is still open. If the follow-up keeps unclear sends in the outbox under their original id, rather than keying a kept id by the message text, the desktop never needs this rule. The common pattern works that way. In that case this move adds churn without a user. Verified / not verified.
|
|
Closing this. The send design settled on a different shape: a send's id belongs to its saved outbox entry and is resent under that same id until the host answers, as the common pattern's outbox client does. Ids are not remembered by message text. The phone's text-keyed id store will be replaced when the phone gets the same saved outbox (a later PR), so moving that rule into shared code would spread the shape we're removing. |
ELI5
The phone app already has a careful rule for not sending the same chat message twice. This PR moves that rule out of the phone's code into code the desktop app can also use. Nothing changes for anyone yet: the phone runs exactly the same rule, now from its new home.
What Changed
The problem this prepares for. In the desktop's native chat, a message that fails to send stays in the conversation with a small red line ("Message was not sent." / "Message delivery is unconfirmed.") and a Retry button. The planned fix (a follow-up to #24606) gives the failed message's text back to the composer instead. That is only safe if sending the same text again can't deliver it twice when the first attempt may in fact have reached the agent.
The phone already solves this. Every send carries an operation id, and the host delivers a given id at most once. The phone keeps ("retains") the id for a message whose fate is unclear, keyed by what the message says. Re-sending the same text therefore reuses the same id, and the host answers it from its record instead of delivering again. Two pieces decide this:
What moved. Before, both lived in the phone app (
mobile/src/session/mobile-structured-send-delivery.ts, and inline inmobile-structured-agent-session-send.tsplusmobileStructuredSendOperationKeyin the phone's operation journal). After, they live insrc/shared/structured-agent-session-send-retention.ts:structuredAgentSessionSendDelivery(the spent rule, unchanged line for line);structuredAgentSessionSendIntentKey(the intent key);structuredAgentSessionSendOperationKey(the last hashing step of that key).The phone imports them from there. The call-result type the spent rule reads (
StructuredAgentSessionMutationCallResult) moved from the phone's RPC module intosrc/shared/structured-agent-session-mutation.ts, next to the fingerprint functions, because shared code can't import from the phone. The phone's send-outcome type (MobileNativeChatSendOutcome) is now an alias of the shared one, so the four outcomes are listed in one place. The test fixture for a host's send answer moved tosrc/sharedtoo, so the shared and phone tests use one copy.The reliability gate for "one send id reaches the agent at most once" (
config/reliability-gates.jsonc) listed the phone's old delivery test, which this move deletes. It now runs the shared test instead, with fresh evidence runs.What users see. No change on the phone or the desktop. The desktop does not use the moved code yet; the follow-up does.
Saved keys are unchanged. The phone stores retained ids on the device under the intent key. The hashing domains keep their
mobile.*names, so a phone upgraded across this change still finds the ids it saved. A new test rebuilds the key the way the phone code did before this PR and checks they match.Why
The follow-up plans to reuse this rule on the desktop rather than write a second copy. Two versions of "when is this id used up" would drift, and that rule is what keeps one message from reaching the agent twice. Moving it first, as a change with no behaviour difference, keeps the follow-up's review about behaviour only.
This is not yet the desktop's rule, and the follow-up will have to change it. The desktop's current outbox decides the same question differently in at least two cases:
disposeStructuredAgentSessionSendFailure), while this rule spends it on a first attempt.structured-agent-session-outbox-retry.ts), while this rule keeps it on a retained retry.The follow-up must settle each of these on purpose. Its design is still open, and the PR should wait for it (see Review).
The rule also relies on what a
failedcall result means: that the request provably did not run. Today only the phone's RPC wrapper produces that result and guarantees it. Any desktop caller has to give the same guarantee.Alternatives considered:
Known issue (not fixed here)
The phone's retained ids never expire (
mobile/src/session/mobile-structured-send-operation-journal.ts, "Ambiguity has no TTL"). The moved rule holds the same latch: when the host refuses a retried id because it has expired, the rule keeps the id instead of spending it (therefusedbranch ofstructuredAgentSessionSendDelivery). Sending the same text again is then refused for good. The host stops accepting an id after its replay window, so an expired retained id is refused for good and is never released. Once 4,096 build up (AGENT_SESSION_DURABLE_OPERATION_GLOBAL_LIMIT), the phone refuses every new send ("Message not sent"). This PR is a pure move and leaves that alone. Follow-up: an expired refusal spends the id, retained ids expire with the host's replay window, and a full store evicts expired ids first instead of blocking the send.Linked Issue
N/A (maintainer). This is the shared-rule step of the native-chat failed-send follow-up to #24606.
Visual Proof
N/A: a code move with no behaviour change. No screen draws differently.
Testing
The spent rule's tests moved with it to
src/shared/structured-agent-session-send-retention.test.ts, unchanged apart from names. So did the operation-key hashing test from the journal tests.New intent-key tests: the key equals one rebuilt from the hashing domain names alone, the way the old phone code built it (with and without queueing); and different sessions, texts, attachments and queueing each give a different key.
Ablation: renaming the operation domain, the intent domain, or the image-preview domain each fails the saved-key test. Reverted.
Both commands of the at-most-once reliability gate pass: 13 root files, 196 tests; 8 phone files, 112 tests. The wider phone send, cancel, stop, journal and native-chat suites pass too (15 files, 224 tests).
Reliability gate manifest check passes.
Typecheck: node project clean; web project clean (run before the second commit, which touches no web code); the phone project and its test-file ratchet have no errors outside the existing generated-module ones.
Lint: oxlint clean, formatting clean, and the changed-code quality gate has 0 new findings.
Platforms: pure functions with no platform, SSH or folder-workspace dependence. Not run in the app.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Reviewed for move fidelity, regressions and design. No runtime defect was found. The saved phone keys come out byte-identical to the old code's over edge-case inputs, and the phone bundle pulls in the same modules as before. Hold this as a draft until the follow-up's design is decided: if that design keeps unclear sends in the outbox instead of keying a kept id by the message text, the desktop never needs this rule and this move isn't needed.
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)Author: @BrennanKB5