Skip to content

fix(ai): the write receipt reads create_event's 'already exists' result as the same event, not a second creation or a failure (Refs #929, #520) - #974

Merged
alex-mextner merged 2 commits into
mainfrom
dup-receipt-fold
Nov 7, 2026
Merged

alex-mextner merged 2 commits into
mainfrom
dup-receipt-fold

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

The write receipt now reads create_event's idempotent "already exists" result as the same event: a creation and its repeat are one created event (not a second "Created" line, never a failure, and the turn-finish guard does not fire when nothing failed); a repeat of an event this run did not create is told once as "Already in the calendar" / "Уже есть в календаре", because the run wrote nothing. Before, each repeat added its own "Created" line, and the receipt-only confirmation called an event this run never wrote "Created".

Refs #929, #520.

Why: since #942 a repeated create_event (a retry after a lost reply, or the second of two overlapping runs) returns success: true, mutationState: 'not_applied' and the structured ToolResult.alreadyExists = { entity: 'event', id }. The ledger recorded that as one more created write, so a receipt listed the same event twice, and the receipt-only confirmation claimed a creation the run did not make. This is the sibling item of #929 that was left for the receipt rewrite (#940).

What changes:

  • write-outcomes.ts records alreadyExists on the attempt and names the event by that id; it never reads the output prose. The ledger otherwise treats the repeat like any success of the same request: it settles an earlier refused try, and an earlier try of unknown result keeps its duplicate warning (a deliberate allow_duplicate copy may have been written; finding the first row cannot prove otherwise).
  • write-receipt.ts tells a repeat of an event that a recorded successful create already told as nothing, and tells a repeat of an unseen event as "Already in the calendar". Group-chat target hiding is unchanged; a different event, a deliberate allow_duplicate copy and a refused try with other details stay visible.
  • receipt-confirmation.ts (the receipt-only confirmation after a validator outage) follows the same rule; ToolEvidence carries alreadyExists. ru and en messages are symmetrical.
  • Version 0.3.6 -> 0.3.7 (pyproject.toml, uv.lock) in a separate commit; the plan note in docs/plans/2026-10-31-secretary-invitation-incident.md records the behaviour.

Tests (written red first against main): test/services/ai/write-receipt-existing-event.test.ts runs the real create_event handler through executeTool into the real WriteOutcomes, in ru and en (20 tests): creation then repeat, a differently spelled repeat, refused then repeat, unknown then repeat (warning kept, including a deliberate copy), a refused different-end try settled by the original repeat, a throttled repeat, a repeat of an unseen event, a different event kept apart, allow_duplicate, group hiding. test/services/ai/receipt-confirmation.test.ts covers the confirmation path (9 tests). On main the first 16 cases of the new file fail 14 (duplicate "Created" lines, a duplicate warning, the wrong label).

Verified: single test files locally (the new file, write-outcomes, write-receipt, receipt-confirmation: all pass); whole-repo tsc --noEmit and bun run lint exit 0 and bun test 9582 pass on the build server for the code head 170a68c, with 6 failures that are the server's own environment, not this change: five stage-QA Python installer/teardown suites (the server has no system git and a nearly full disk, #955) and Chromium (missing libnspr4) in three browser tests. Those are not server-verified; the hosted test job on the exact head is the validator.

Review: two staged-diff reviews of the receipt commit by Sol seats (the Opus seat was refused: account usage over the limit / excluded); the first run's two findings are fixed (a repeat must still reconcile earlier refusals; an unknown deliberate copy keeps its warning) and the final run had no findings; the version commit had none. Not reviewed by Opus.

Not covered: live acceptance of #908/#909 is still pending and stays in those tickets; #929 keeps its other open items (per-run delivery outcome, queue depth cap).

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