Repository navigation
Conversation
…ntent and completion-only finalize (cli#617 slice b) Adds the delivery mechanism only, inert at runtime. MailClient gains deliverNext()/completeDelivery(): a commit leaves its placement intent in the in_progress phase, recovered messages are re-presented until completed, and a liveness bound (five presentations or 24 h) moves a stuck intent to terminal failed. recoverPromoted reads the intent phase and withholds a record that resolves to anything but placing, covering the four direct callers deferred by slice (a). runtime/event-loop.ts and runtime/agent.ts are untouched. Refs #617
617 slice (b) — prose sweep (deliverable, not committed)Scope: every sentence this PR adds or changes — changelog fragment, PR body, Changelog fragment
|
| Sentence (compressed) | Claim type | Call |
|---|---|---|
| "MailClient gains a per-message delivery API; a committed message stays re-presentable until completed" | coverage | ok — deliverNext leaves in_progress; completeDelivery removes it; asserted by the delivery tests. |
| "Nothing in the agent runtime calls the new API yet, so delivery is unchanged until slice (c)" | scope (unchanged) | ok — git diff touches neither runtime/event-loop.ts nor runtime/agent.ts; checkNewMail unchanged. |
"commits one message and leaves its placement intent in the in_progress phase, removed by an explicit completion" |
outcome | ok — commitToCur(..., true) → advancePresentation; removed by completePlacement. (Dropped "only" here: the guarantee word added no fact.) |
"after five presentations without completion, or 24 hours after the first, the intent goes terminal failed, the payload is kept and one log line names the mailbox, message ID and presentation count" |
outcome + scope | ok — MAX_DELIVERY_PRESENTATIONS=5, DELIVERY_PRESENTATION_TTL_MS=24h; failed written, payload untouched, one console.warn. |
"recoverPromoted now reads the placement intent's phase and withholds a record that resolves to anything but placing with a retryable busy refusal, so tps mail ack and nack and the bridge and plugin startup sweeps do not present another consumer's in-flight delivery" |
outcome + scope | ok — the check returns {ok:false,class:"busy"} before presentation; busy is in RETRYABLE_REJECT_CLASSES. |
PR body
Reuses the fragment's claims plus the item→code/test mapping and the evidence
numbers. The mapping sentences name functions/tests that exist; the counts name
the commit they were measured on. No outcome/scope/coverage claim beyond the
table above.
Added source comments
| Comment (compressed) | Claim type | Call |
|---|---|---|
io/mail.ts "an in_progress re-presentation … the liveness bound turns a message that is never completed into a terminal failed intent" |
outcome | ok — advancePlacementPresentation failed branch. |
io/mail.ts "The caller finalizes with completeDelivery() after the handler completes" |
scope (softened) | ok — directive, not a description of shipped behaviour; reworded from "only after". |
io/mail.ts "recoverDelivery … unknown is skipped (never presented or finished)" |
scope | ok — if (phase === "unknown") continue; and presentUnderLock returns null for unknown. |
io/mail.ts "finish only a placing intent whose cur/ copy is absent" |
scope + outcome | ok — finishUnlinkedPlacement re-checks resumableOnRecovery. |
mailbox-policy.ts "Deletes only an in_progress intent for this message" |
scope + outcome | ok — completeInProgressPlacement gates on phase + messageId. |
mailbox-policy.ts "a failed or unknown intent, or another message's intent, is never replaced" |
scope | ok — writePlacementPhase refuses unless phase is placing/in_progress and the messageId matches. |
utils/mail.ts "the same bar now covers every direct recoverPromoted caller" |
scope (every) | ok — the check is inside recoverPromoted, so every caller passes through it. |
utils/mail.ts "a failed read never reads as absent" |
scope (never) | ok — a placementPhase throw returns busy, not a pass. |
Test titles / describe strings
Every added describe/test title states an outcome the corresponding
assertion measures. Spot-checked: "later messages stay in new/ until the
in-flight one completes"; "re-presents a committed message until it is
completed"; "finished to in_progress and presented"; "dead-lettered as replay
once committed"; "intent goes terminal, a line is logged, and other mail
flows"; "past the presentation TTL is quarantined"; "unknown … gate stays
closed"; "recoverPromoted withholds a … intent". All true against the code
and asserted by the test body.
Verdict
No failing sentence found. One guarantee word removed at review ("only"). No
new claims were added during the sweep.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
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. Comment |
Refs #617 (slice (b) of the approved design rev 2)
What this slice does
Adds the mechanism only, inert at runtime.
MailClientgains a per-messagedelivery API (
deliverNext()/completeDelivery()) that commits one messageand leaves its placement intent in the
in_progressphase, removed by anexplicit completion. A committed message is re-presented until then, bounded by
the liveness bound (after five presentations without completion, or 24 hours
after the first, the intent goes terminal
failed, the payload is kept, and onelog line names the mailbox, message ID and presentation count). Recovery is
completion-gated: a
placingintent is finished toin_progressand presented,an
in_progressintent is re-presented, andunknownis skipped.runtime/event-loop.tsandruntime/agent.tsare untouched and the batchcheckNewMail()the event loop calls keeps its current behaviour, so mergingthis does not change the running agent's delivery; slice (c) wires the event
loop.
recoverPromotednow reads the placement intent's phase and withholds a recordthat resolves to anything but
placing, with a retryablebusyrefusal. Thatcovers every direct caller the slice (a) PR deferred: the bridge startup sweep,
the OpenClaw plugin startup sweep,
verifyMailActionand the plugin'sinternalInbound.The at-most-once assertions in
mail-interrupted-recovery.test.tsare rewrittento the "re-present until completed" contract, driving the delivery API.
The operator command that clears
failedintents and tombstones is a later,smaller PR; it is not in this slice.
Design items → code / test
Slice (b) mechanism:
in_progresswriter —MailClient.deliverNext,MailClient.commitToCur(..., keepInProgress)andReplayStore.advancePresentationinpackages/agent/src/lib/mailbox-policy.ts.MailClient.completeDeliveryandReplayStore.completePlacement.advancePlacementPresentation(MAX_DELIVERY_PRESENTATIONS,DELIVERY_PRESENTATION_TTL_MS) plus the log line inMailClient.presentUnderLock.in_progressre-presentation in recovery —MailClient.recoverDelivery/presentUnderLock.presentUnderLockmovesplacingtoin_progress.new/" —commitNextForDeliverycommits onemessage.
recoverPromotedpresenters — the phase check inrecoverPromoted(packages/cli/src/utils/mail.ts).Rev-2 precision items:
source is a hard link to
cur/, so a later pass removes it through theexisting same-inode "already-delivered" path, under the relay stripe. Covered
by the kill-before-source-removal case.
failedintent does not keep the per-file gate closed:advancePlacementPresentationwritesfailed;pendingPlacementEntriesomits it and
hasPendingFilereads it as not live. Asserted in the livenesstests.
isConsumed(
presentUnderLock).covers the sweep; this slice's
recoverPromotedcheck covers the four directcallers.
lock, and
commitToCurre-reads under the lock.Evidence
Tests (real signed records through the real parser and signer; real child-process
kills for the crash boundaries):
packages/agent/test/mail-delivery-at-least-once.test.ts— 14 tests: onecommit at a time with later mail left in
new/; completion-only finalize;in_progressre-presentation; completion-gated placing recovery; duplicateenvelope refused as replay; the presentation-count and TTL bounds; the
terminal intent; the preserved
unknownphase; kills before intent write,link, source removal and ledger commit; a kill after the intent becomes
in_progress.packages/agent/test/mail-interrupted-recovery.test.ts— 10 tests, rewrittento the delivery API's "re-present until completed" contract.
packages/cli/test/mail-slice-b-presenters.test.ts— 4 tests:recoverPromotedwithholdsin_progress,failedandunknown, andverifyMailActionrefuses.Mutation checks, one per gate (edit, run, observe the behaviour assertion fail,
restore, observe green):
in_progress→ the "re-present untilcompleted" and "later messages stay in
new/" assertions fail.in_progress→ the re-presentation assertions fail.recoverPromotedphase check removed → the three presenter tests fail.isLivePlacementtreatsunknownas not live → the unknown-phase gateassertion fails.
Checks:
tscforpackages/agentandpackages/cliclean;bun run lint:ciclean;
node scripts/changelog-fragments.mjs checkclean.Suite counts, measured on 0ab2328 (main 78d295e):
across 22 files) — the difference is the new 14-test file.
main: 3270 pass / 7 skip / 0 fail across 3277 tests in 210 files) — the
difference is the new 4-test file.