Repository navigation
Conversation
A REST send without localId was stored with local_id NULL. Shared Codex then gave the turn a random clientUserMessageId and echoed the user message back under it; the hub dedupes only on (session, localId), so the echo became a second row and the message showed twice. Mint a localId in MessageService.sendMessage when the request omits one, so the stored row, the CLI delivery and the agent echo share an identity. Such sends now take the normal queue/ack/cancel path, like typed web sends. Fixes tiann#2068 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Review mode: initial
Requirement — Pass
REST sends without localId should produce one stored user message. The reported duplication is supported by the existing storage and Shared Codex echo paths.
Evidence
- Issue #2068 describes duplicate rows for one send without localId.
- cli/src/codex/shared/root.ts:118 generates a missing ID; projection.ts:108–114 preserves it in the echo, while hub/src/store/messages.ts:130 only deduplicates messages with matching localIds.
Approach — Pass
Minting the ID before insertion gives storage, CLI delivery, and the echo the same identity. Reusing the existing queue/ack lifecycle is appropriate, and the documentation explains the behavior change.
Evidence
- hub/src/sync/messageService.ts:890 generates an ID only when one is absent; line 924 delivers the stored ID to the CLI.
- hub/src/sync/messageService.ts:966–978 already settles immediate queued messages at session end.
- docs/api/client-contract/pagination.md:153 and :173 document generated IDs and queue/ack semantics.
Code — Reviewed
Reviewed the entire fixed-head merge-base diff and relevant storage, delivery, acknowledgement, and echo handling. No actionable defects found. Verification was read-only; PR code was not executed.
No reportable code issues found.
Testing
Not run (automation; PR code execution is prohibited).
- The added regression checks distinct generated IDs for separate sends and storage-level deduplication of an echo using the delivered ID.
- The new test does not exercise a real Shared Codex round trip or generated-ID acknowledgement/cancellation transitions.
- At inspection, fixed-head CI checks test, integration, and windows-codex-mcp were still running. The PR reports passing local typechecks and tests; those results were not independently verified.
HAPI Bot
Fixes #2068.
Result
When a
POST /api/sessions/:id/messagesrequest has nolocalId, the hub mints one. The stored row, the CLI delivery, and the Shared Codex echo now share one identity, so the existing(session_id, local_id)deduplication keeps one row per send instead of two.This covers every REST sender, including peer messages (#1974), API scripts, and third-party clients. #2024 and #1436 fix their own senders. This change handles the senders that don't.
Behavior change: such sends now follow the normal queue/ack/cancel path, the same one typed web sends already use, instead of being stamped invoked at insert. Session-end sweeps still clear unacknowledged rows. Senders that provide a
localId, scheduled sends, and agent-originated rows are unchanged. The client-contract docs are updated to match.Existing duplicate rows are not removed.
Validation
messageServiceregression test: two sends withoutlocalIddeliver distinct ids to the CLI, and an agent echo under the delivered id adds no row. It fails without the change and passes with it.bun typecheckandbun run testpassed: all packages, including 1,390 hub tests.localIdwas stored as awebapprow withlocalId: null, followed by an identicalcliecho. Codex ran it once.AI disclosure: Claude Code assisted with diagnosis, the change, and verification.
🤖 Generated with Claude Code