Repository navigation
C1-V3-R: approve carries payment outcome; capsules see 'submitted, outcome unknown' - #86
Conversation
… "submitted, outcome unknown" (C1-V3-R)
citrate-core (C1-V3) releases an approved payment whose broadcast had no known
outcome through POST /approvals/approve with {id, outcome:
"submitted_outcome_unknown", txHash, message}. The sidecar read only `id`, so
the decision record said Completed and the capsule was not told what happened.
- hitl: UnknownOutcome (validated: txHash 0x + 64 hex, message non-empty,
<= 280 bytes, no control characters, names the hash when there is one).
ApprovalQueue::approve_by_id_outcome_unknown resolves the head with it;
submit_with_resolution returns Resolution::SubmittedOutcomeUnknown, and
submit()/submit_with_outcome never read it as approved.
- No unattended re-send: the queue remembers the last 256 unknown-outcome
call ids. An identical re-proposal skips the auto-grant fast path and its
PendingView names the earlier tx.
- ApprovalGate::decide (default mirrors request) returns GateDecision; the
eth-send host fn reports "ChainSendOutcomeUnknown: <core's message>" and
never dispatches, so nothing is sent twice.
- sidecar: the approve route parses the optional fields (absent = today's
behaviour; malformed, orphaned, or on reject = 400, before anything is
recorded or resolved). The decision record closes as outcome_unknown with
"Submitted · outcome unknown (tx …)". /approvals carries
priorOutcomeUnknown and a summary note for a re-proposal.
Tests: citrate-agent-core + agent-sidecar 879 -> 898 passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148EsKJD57GeaM8JwAraU3N
|
[DGX] APPROVED at 506b779 DGX review of #86 (C1-V3-R). I read Verdict: approve. On approve, the sidecar reads Wire match with core #366
Core's 409 handling ( Capsule path (the goal of the item)
Rule 12 /
|
| passed | failed | ignored | |
|---|---|---|---|
| PR | 893 | 1 | 7 |
| main @ 75e21cd (same sandbox) | 874 | 1 | 7 |
- +19 tests, which matches the PR body's count.
- The one failure is the same on main and so not caused by this PR:
auto_approve_tests::off_the_list_or_outside_a_project_it_still_asks(asked 3 of 4;npm installisn't asked inside the user-namespace sandbox). It comes from feat(sidecar,shell): HGW runtime stack #80–#84 (project write policy, project_create, 24-step turns, auto-approve, git safety) #84's shell sandbox and depends on the environment. - The PR body says 898 passed. Its run is outside unshare, so the totals differ by environment.
Ran cargo clippy -p citrate-agent-core -p agent-sidecar --all-targets: the same warning counts as main (legacy 5, core lib 13, core lib test 18), so no new warnings.
Ran cargo fmt -p agent-sidecar --check: clean.
Stub-proven vs contract-only
- Stub-proven (in-process): sidecar route parsing and the 13 refusal shapes, the record outcome, the queue resolution, the capsule result string, no dispatch, re-proposal gating.
- Contract-only: the end-to-end core #366 → real sidecar → capsule path. There is no cross-repo test, and #366 is still open. M7-style proof needs a Core build bundling this sidecar.
BerryManifold
left a comment
There was a problem hiding this comment.
Automated Codex static review of 506b7799ac4e3872076c9f72863f0ddf8739d99d is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
Automated Codex static review of 506b7799ac4e3872076c9f72863f0ddf8739d99d is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
…a late callback (C1-V3-R) Four new tests fail on assertions against 506b779: - an_unknown_outcome_is_delivered_even_when_the_record_cannot_be_written (503 today) - a_late_unknown_outcome_is_recorded_after_the_waiter_is_gone (no receipt today) - a_late_unknown_outcome_never_touches_the_current_head (no receipt today) - every_unknown_outcome_callback_keeps_the_transaction (state-matrix tripwire) Control: an_ordinary_approval_is_still_refused_when_the_record_cannot_be_written keeps the write-ahead refusal for plain approvals. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ails or the waiter is gone (C1-V3-R) An unknown outcome means citrate-core already broadcast the transaction, so the approve route no longer refuses it with 503 when the decision record cannot be written: the waiter is told, the tx is remembered for re-proposals, and the reply carries "recorded": false. Plain approvals keep the write-ahead refusal. A callback whose call is no longer pending (waiter timed out or cancelled, or another call at the head) now writes an approved ceremony decision bound to its call id and closes it OutcomeUnknown with the tx hash; it still answers 409 and resolves nothing else. record_ceremony_decision takes the call id and subject so a receipt can be written without a pending view; hic_records::try_record_outcome reports whether the outcome was written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
[MAC] Addressed at 5782a76 Both Codex P2s at 506b779 were real and are fixed. Red 8f9d459, fix 5782a76. No rebase: the two commits sit on top of 506b779.
Refactor in the same commit: Standard
Wire note for citrate-core
🤖 Generated with Claude Code |
Matr0xshka
left a comment
There was a problem hiding this comment.
[MAC independent review] APPROVED at 5782a76
Reviewer: MAC-lane independent reviewer agent (did not author this PR). Not Codex, not DGX. Per owner decision 2026-10-23 this review stands in for the Codex and DGX gates.
Head checked live via REST: 5782a7680abd08c128f7427e076c3ade370eca34. I reviewed the whole diff against main (9 files). I worked in a scratch worktree with its own target dir (macOS, stable toolchain).
What I ran
cargo test -p agent-sidecar -p citrate-agent-core: 903 passed, 0 failed, 7 ignored. That matches the body (898 → 903).- Codex: the result at 506b779 has two P2s, and both are fixed here (below). No Codex result exists yet at 5782a76. There are no pysol-dev reviews on this PR.
Revert check (reproduced; no git stash, files copied aside and restored, tree clean afterwards)
agent-sidecar/src/lib.rsandhic_records.rsrestored from the red commit 8f9d459: 4 fail on assertions. They arean_unknown_outcome_is_delivered_even_when_the_record_cannot_be_written,a_late_unknown_outcome_is_recorded_after_the_waiter_is_gone,a_late_unknown_outcome_never_touches_the_current_headand the tripwireevery_unknown_outcome_callback_keeps_the_transaction. The controlan_ordinary_approval_is_still_refused_when_the_record_cannot_be_writtenpasses, and 8 pass in total.- The feature commit's red was a compile failure, so I also killed its behaviour by mutation, one mutant at a time:
| Mutant | Result |
|---|---|
The sidecar treats an unknown outcome as a plain approve (lib.rs:730) |
7 outcome_unknown_tests fail |
The re-proposal guard is off (hitl/mod.rs:382: auto-grant ignores a remembered unknown outcome) |
a_reproposal_references_the_earlier_tx_and_never_auto_approves fails |
The linker dispatches on SubmittedOutcomeUnknown (capsule/linker.rs:465) |
eth_send_capsule_reports_an_unknown_outcome_and_never_dispatches fails |
QueuedApprovalGate::request returns Ok for an unknown outcome (prod_impls.rs:71) |
queued_gate_passes_an_unknown_outcome_through fails |
Probes (throwaway, not committed)
- The same callback posted twice: the first gives 200 and the capsule gets
SubmittedOutcomeUnknownwith core's words. The repeat gives 409, and a later plain{id}approve gives 409. No record ever closesCompleted. - A callback for an id that was never pending: 409, nothing resolved, a receipt written, and the id remembered, so a later proposal of it waits for the member.
- Under the kill switch: the callback gets 503, and the transaction isn't remembered. Follow-up below.
Findings
- Codex P2 1 (a record failure hid the outcome): fixed. With an unknown outcome, a failed write is logged, and the outcome is still delivered and remembered (
recorded: false). A plain approval still refuses with 503 (write-ahead). - Codex P2 2 (no receipt for a late callback): fixed. A receipt is bound to the callback's call id and closed
OutcomeUnknownwith "not told". No other entry is resolved. - P2 follow-up (#438):
resolve_body(lib.rs:699) refuses before it readsoutcomewhile the kill switch is on, so the receipt and the re-proposal memory are skipped. It's the same shape as Codex P2 1. It doesn't block: core's ceremony still has to sign any second payment. A duplicate callback also writes a second receipt. - Note:
rustfmtfrom the CI toolchain flags some of the new lines inagent/core/src/hitl/mod.rsandcapsule/prod_impls.rs.cargo fmt --checkalready fails acrossagent/coreon main, so this isn't a regression.
CI (confirmed pre-existing and unrelated)
rust/ci: the job fails incargo testoncitrate-agent-browser'slive_chrome_tests::attach_needs_consent_per_session_and_per_origin(the stand-in Chrome port). This PR doesn't touch that crate.- fmt and clippy steps: they fail the same way on main's own green run at ce34d3e (387 fmt diffs,
citrate-agent-coreclippy with 13 errors under 1.99.1). Every clippy error is on a line that predates this PR. - Everything else is green, including
audit.
Wire match with core #366
outcome, txHash (0x + 64 hex or null) and message (must contain the hash) are as DGX tabulated. The approve route is the only one that takes them, and an outcome on reject is a 400.
Standard: met. Red reproduced; the revert check and four behaviour mutants all fail; the state-matrix tripwire is in place.
Verification cleared: yes. Money truth: an unknown outcome is never Completed or Approved to the capsule, is never dispatched twice, and always names the hash.
BerryManifold
left a comment
There was a problem hiding this comment.
[MAC independent review] APPROVED at 5782a76
Reviewer: MAC-lane independent reviewer agent (did not author this PR). Not Codex, not DGX. Per owner decision 2026-10-23 this review stands in for the Codex and DGX gates.
Head checked live via REST: 5782a7680abd08c128f7427e076c3ade370eca34. I reviewed the whole diff against main (9 files). I worked in a scratch worktree with its own target dir (macOS, stable toolchain).
What I ran
cargo test -p agent-sidecar -p citrate-agent-core: 903 passed, 0 failed, 7 ignored. That matches the body (898 → 903).- Codex: the result at 506b779 has two P2s, and both are fixed here (below). No Codex result exists yet at 5782a76. There are no pysol-dev reviews on this PR.
Revert check (reproduced; no git stash, files copied aside and restored, tree clean afterwards)
agent-sidecar/src/lib.rsandhic_records.rsrestored from the red commit 8f9d459: 4 fail on assertions. They arean_unknown_outcome_is_delivered_even_when_the_record_cannot_be_written,a_late_unknown_outcome_is_recorded_after_the_waiter_is_gone,a_late_unknown_outcome_never_touches_the_current_headand the tripwireevery_unknown_outcome_callback_keeps_the_transaction. The controlan_ordinary_approval_is_still_refused_when_the_record_cannot_be_writtenpasses, and 8 pass in total.- The feature commit's red was a compile failure, so I also killed its behaviour by mutation, one mutant at a time:
| Mutant | Result |
|---|---|
The sidecar treats an unknown outcome as a plain approve (lib.rs:730) |
7 outcome_unknown_tests fail |
The re-proposal guard is off (hitl/mod.rs:382: auto-grant ignores a remembered unknown outcome) |
a_reproposal_references_the_earlier_tx_and_never_auto_approves fails |
The linker dispatches on SubmittedOutcomeUnknown (capsule/linker.rs:465) |
eth_send_capsule_reports_an_unknown_outcome_and_never_dispatches fails |
QueuedApprovalGate::request returns Ok for an unknown outcome (prod_impls.rs:71) |
queued_gate_passes_an_unknown_outcome_through fails |
Probes (throwaway, not committed)
- The same callback posted twice: the first gives 200 and the capsule gets
SubmittedOutcomeUnknownwith core's words. The repeat gives 409, and a later plain{id}approve gives 409. No record ever closesCompleted. - A callback for an id that was never pending: 409, nothing resolved, a receipt written, and the id remembered, so a later proposal of it waits for the member.
- Under the kill switch: the callback gets 503, and the transaction isn't remembered. Follow-up below.
Findings
- Codex P2 1 (a record failure hid the outcome): fixed. With an unknown outcome, a failed write is logged, and the outcome is still delivered and remembered (
recorded: false). A plain approval still refuses with 503 (write-ahead). - Codex P2 2 (no receipt for a late callback): fixed. A receipt is bound to the callback's call id and closed
OutcomeUnknownwith "not told". No other entry is resolved. - P2 follow-up (#438):
resolve_body(lib.rs:699) refuses before it readsoutcomewhile the kill switch is on, so the receipt and the re-proposal memory are skipped. It's the same shape as Codex P2 1. It doesn't block: core's ceremony still has to sign any second payment. A duplicate callback also writes a second receipt. - Note:
rustfmtfrom the CI toolchain flags some of the new lines inagent/core/src/hitl/mod.rsandcapsule/prod_impls.rs.cargo fmt --checkalready fails acrossagent/coreon main, so this isn't a regression.
CI (confirmed pre-existing and unrelated)
rust/ci: the job fails incargo testoncitrate-agent-browser'slive_chrome_tests::attach_needs_consent_per_session_and_per_origin(the stand-in Chrome port). This PR doesn't touch that crate.- fmt and clippy steps: they fail the same way on main's own green run at ce34d3e (387 fmt diffs,
citrate-agent-coreclippy with 13 errors under 1.99.1). Every clippy error is on a line that predates this PR. - Everything else is green, including
audit.
Wire match with core #366
outcome, txHash (0x + 64 hex or null) and message (must contain the hash) are as DGX tabulated. The approve route is the only one that takes them, and an outcome on reject is a 400.
Standard: met. Red reproduced; the revert check and four behaviour mutants all fail; the state-matrix tripwire is in place.
Verification cleared: yes. Money truth: an unknown outcome is never Completed or Approved to the capsule, is never dispatched twice, and always names the hash.
BerryManifold
left a comment
There was a problem hiding this comment.
Automated Codex static review of 5782a7680abd08c128f7427e076c3ade370eca34 is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
Automated Codex static review of 5782a7680abd08c128f7427e076c3ade370eca34 is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
Approval at 6232099, recorded on the owner's instruction (2026-10-22, owner merge queue).
6232099 is the cross-lane-approved head 5782a76, with only the default branch merged in since.
What this approval measured:
- Cross-lane approval at the reviewed head (see the
[MAC]/[DGX] APPROVEDreview on this PR). - Codex: MAC independent review APPROVED at the reviewed head (stands in for Codex + DGX per owner decision 2026-10-23); owner instructed merges 2026-10-23.
- CI at
6232099:ci=success, secret-scan=success.
This does not record a fresh, independent re-run of the tests.
BerryManifold
left a comment
There was a problem hiding this comment.
Approval at 6232099, recorded on the owner's instruction (2026-10-22, owner merge queue).
6232099 is the cross-lane-approved head 5782a76, with only the default branch merged in since.
What this approval measured:
- Cross-lane approval at the reviewed head (see the
[MAC]/[DGX] APPROVEDreview on this PR). - Codex: MAC independent review APPROVED at the reviewed head (stands in for Codex + DGX per owner decision 2026-10-23); owner instructed merges 2026-10-23.
- CI at
6232099:ci=success, secret-scan=success.
This does not record a fresh, independent re-run of the tests.
Board item: C1-V3-R · agent-runtime: read outcome/txHash/message on approve so capsules see "submitted, outcome unknown"
Why
citrate-core PR #366 (C1-V3) releases a payment that the member approved but whose broadcast had no known outcome. It sends that payment through the sidecar's approve route as
{id, outcome: "submitted_outcome_unknown", txHash, message}(resolve_head_outcome_unknowninsrc-tauri/src/hermes.rs). The sidecar read onlyid, so the decision record said Completed and the capsule was never told what happened.What changed
Data source (Rule 7): citrate-core's signature ceremony, through
hermes_resolve_outcome_unknown→POST /approvals/approve.outcome,txHashandmessagefields.outcomeis notsubmitted_outcome_unknown;txHashis not0x+ 64 hex;messageis empty, over 280 bytes, contains control characters, or leaves out the hash;txHash/messagearrive without anoutcome; or any outcome is sent on/approvals/reject(an unknown outcome is never a rejection).txHash: nullis accepted, which is what core sends when it has no hash.Err("ChainSendOutcomeUnknown: <core's message, verbatim>"). That is a non-final result, not success. The dispatcher is never consulted, so nothing gets sent twice.ApprovalGate::decide→GateDecision(the default mirrorsrequest, so existing gates keep their meaning).QueuedApprovalGate::requestnever returnsOkfor an unknown outcome.outcome_unknownand detailSubmitted · outcome unknown (tx 0x…), or(tx not known)when there is no hash. If the call was no longer waiting, the record still closes as outcome_unknown, because the transaction went out from core either way./approvalsshows that card withpriorOutcomeUnknown: {txHash, message}, and its summary ends with "an earlier identical request was submitted, outcome unknown (tx …); check it before approving again".citrate-agent-core::hitl):UnknownOutcome(validated constructor)approve_by_id_outcome_unknownsubmit_with_resolution→ResolutionApprovalOutcomePublic::SubmittedOutcomeUnknown;submit()returns false for itPendingView.prior_unknownTests (red first)
The red run was a compile failure (37 errors) of the new tests against
main, before any implementation.cargo test -p citrate-agent-core -p agent-sidecar: 879 → 898 passed, 0 failed, 7 ignored (+19).agent-sidecar/src/outcome_unknown_tests.rs(7): exact message reaches the submitter; the plain submit is not approved; the record says Submitted · outcome unknown; the null-hash path; id-only keeps today's behaviour; 13 malformed bodies refused with nothing resolved or recorded; a re-proposal references the earlier tx and is never auto-granted.hitl/outcome.rs(4) andhitl/mod.rs(5): validation, resolution, a head mismatch, a re-proposal skipping the grant, the bounded memory.capsule/mod.rs(2): the capsule getsChainSendOutcomeUnknown: <message>and the dispatcher is never called; the defaultdecidemirrorsrequest.prod_impls.rs(1): the queued gate passes the unknown outcome through, andrequest()is neverOk.cargo fmt -p agent-sidecar --checkis clean. The new and changed core files I could format in isolation were formatted.agent/corealready failscargo fmt --checkonmain, and I did not reformat unrelated code.cargo clippy --all-targetsfor both crates gives the same warning counts asmain; no new warnings.--no-verify: the pre-push hook fails because PyYAML is missing (unrelated to this change), so I ran fmt, clippy and tests by hand.Cross-repo (Rule 12)
No
[[drift]]entry is needed. citrate-core does not pin citrate-agent-runtime:scripts/build-hermes.shbuilds the sidecar binary from the sibling checkout's HEAD and records the rev next to the binary. There is no Cargo dependency and no manifest pin. Core #366 already sends these fields, so the wire contract matches with no core change.Gaps (not in this PR)
{id}(the normal success path), the sidecar has no eth-send dispatcher, because it holds no keys by design. A capsule's approved eth-send therefore still returnsChainSendDispatchFailed: no eth_send dispatcher configuredrather than the tx hash. Fixing that needs core to sendtxHashon a confirmed approval too, which is a separate item.ApprovalStateMachine.tla) does not model the new resolution. Behaviour on the existing transitions is unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_0148EsKJD57GeaM8JwAraU3N