feat(bin): add the Lavish answer-receipt lifecycle to process-event sources - #3471
feat(bin): add the Lavish answer-receipt lifecycle to process-event sources#3471npayette84 wants to merge 28 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains. No blocking failure remains. Reviews (4): Last reviewed commit: "no-mistakes(ci): recover a seam a live r..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72d41f5927
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| normalized=$(fm_procevent_path_normalize "$directory") || return 1 | ||
| [ "$canonical" = "$normalized" ] | ||
| ancestor=${normalized%/*} |
There was a problem hiding this comment.
Inspect symlinks before collapsing parent components
Captain, when the configured state path contains a symlink followed by .. (for example /safe/link/../state, where link targets /redirect/child), lexical normalization removes link before the ancestor walk, although the OS resolves the path to /redirect/state. fm_procevent_private_directory_valid therefore accepts a non-system symlinked ancestor whenever the final target has acceptable ownership and mode, allowing external staging and captures to be redirected despite the new boundary; retain and compare the physical path or walk the original components before collapsing ...
Useful? React with 👍 / 👎.
| [ "$unhandled" -eq 0 ] \ | ||
| || die "cannot arm: $unhandled unhandled captured result(s) for this artifact remain; reconcile or handle them first" |
There was a problem hiding this comment.
Serialize the unhandled check with receipt reset
When the same artifact is re-armed while its existing poll completes, a capture can become unhandled after this scan but before the receipts record is removed below. The old runner can journal received, then cmd_arm deletes that journal and registers the fresh poll, leaving the durable result without the receipt row required by applying and complete; perform the scan and reset under the source-level synchronization that orders capture and registration.
Useful? React with 👍 / 👎.
| submission=$(perl_rows submission "$file" 2>/dev/null) || return 1 | ||
| rows=0 | ||
| if [ -n "$submission" ]; then | ||
| rows=$(printf '%s' "$submission" | cut -f3) |
There was a problem hiding this comment.
Keep feedback-only endings armed for their receipt
When a final Lavish response contains only a supported feedback[N]{...} block, perl_rows submission returns no output because it parses only prompts[N], so this path leaves rows=0 and classifies the result as terminal without requiring a received journal event. The runner then retires immediately even though result_has_queued_content and cmd_read both recognize that feedback as real captain-supplied content, recreating the missing-receipt behavior this lifecycle is intended to fix.
Useful? React with 👍 / 👎.
| case "$claim_state" in | ||
| 1|3) ;; | ||
| *) fm_procevent_source_lock_release "$id"; continue ;; |
There was a problem hiding this comment.
Recover outcomes not owned by the current live claim
After a runner dies with a captured generation and staged receipt outcome, a direct start can reclaim that dead claim and establish a new live long-poll claim before reconciliation. This check sees only that some claim for the source is live and skips recovery, even though the staged generation note belongs to the previous claim; publish_result then continues declining the old capture while the replacement poll may block indefinitely, withholding both its receipt and handler wake until another source event arrives.
Useful? React with 👍 / 👎.
|
Speaking as Kun's firstmate: first-time fork CI approved after a real diff review (no HEAD Contract-class: new-default. Unconfigured Lavish process-event sources always present Received / Saved / Applying / Complete / Already-received through managed-poll Security: no disguised workflow RCE; receipts mode Help / waiting on author (not the captain yet):
VISION.md per-rule
Do not merge. Do not close #3438 from this pass (author/pipeline said it stays until the replacement is the accepted path). |
72d41f5 to
93574ff
Compare
The captain's observed failure: after pressing Send in Lavish, the page showed no acknowledgement, chat stayed silent while processing ran, and the review soon said the agent was not listening - because the runner retired the source on the terminal verdict and no poll ever presented anything. Captured answers were indistinguishable from lost ones. Extend the managed process-event path with a receipt seam and make the Lavish adapter own a visible answer-receipt lifecycle: - fm-procevent.sh gains one generic adapter seam: after durable capture and any keyed-answer feed, and holding the per-source boundary so a concurrent reconcile can never publish a capture the seam is about to acknowledge, the runner calls receipt <id> <seq> <result> <outcome>; the outcome file states exactly what the keyed-answer intake returned. Missing command or failure changes nothing. - fm-procevent-lavish.sh arms the managed poll (still the plain published blocking poll shape - no timeout, no timer, no second owner), which presents the newest recorded truth through --agent-reply each time it arms, and journals per-generation facts in state/procevent/<id>.receipts (layout owned by the runner, bytes by the adapter): Received only after capture, Saved only the intake's verdict with rejected rows reported as counts, Already received for an exact content-digest replay, Applying and Complete only from the handler's explicit commands, and delivery only when a completed poll displayed an uncovered receipt. An ended review's terminal verdict is withheld until that delivery, so retirement can never outrun the acknowledgement the captain sees; a missing session retires with its receipt queued and undeliverable. arm resets the lifecycle, retire and sweep-home clean the record with the registration. - The published destructive-poll loss limitation is restated, not weakened: the receipt proves what reached firstmate, never what the source delivered to the browser. - docs/configuration.md owns the seam and lifecycle contract, process-event-sources owns the handler procedure, the verification record gains the live poll-interface measurements, and tests/fm-procevent-lavish-receipt.test.sh plus the updated Send & End regression prove ordering, partial saves, replays, delivery failure and retry, restart recovery, review end, retirement, and duplicate or concurrent notifications.
Twelve screenshots of the committed lifecycle running against a live lavish-axi 0.1.53 session - success, partial-save, duplicate, and completion states at laptop width, emulated mobile width, and 200% zoom - plus the demo artifact (driver hook neutral by default) and a manifest that states each state's receipt text, the WCAG-AA contrast facts, sha256 for every image, and the exact steps to reproduce every claimed state. No code changes.
…seam A concurrent reconcile's publish_pending could observe a capture in the instant between the durable rename and the receipt seam's handled marking, publishing one spurious announcement for a generation the seam was about to acknowledge (reproduced in the receipt suite as a second distinct wake for a Send & End review). The runner now holds the per-source boundary from the durable capture itself through the seam; publication still runs after release under its own acquisition, exactly as before. The seam's own acquire-under-flag path is unchanged. Also note the post-evidence fix in the evidence manifest, and drop the suite's debugging instrumentation.
The documentation-audience check classifies every tracked *.md/*.txt surface; a per-candidate evidence manifest is task evidence, not a maintained prose surface, so it ships extensionless.
… loop managed-poll exec'd lavish-axi directly, so a registered listener never received the transient-interruption retries the published poll contract gained after this branch's base: the first interrupt was captured as a result instead of being retried quietly, and a failed delivery attempt's empty output could be captured as an empty generation because the function's trailing guard reset its exit status. Extract the retry loop into run_poll, route both poll surfaces through it, and preserve each attempt's exit status so the runner's no-result branch still applies.
A runner killed between its durable capture and the adapter-owned receipt seam left a captured generation the adapter never saw. Reconcile republished that capture, so the round reached the handler with its acknowledgement silently skipped, and a later empty ended poll could retire the review before the captain ever saw the receipt for it. The runner now records, under the same per-source hold it already holds from capture through the seam, that the seam had its one chance at a generation, and reconcile gives that chance to any captured generation still missing it - before it publishes anything or starts a replacement runner, so the seam still speaks for the poll that produced the capture. Recovery hands over an empty intake outcome, because a crashed generation can prove its capture and not what the keyed-answer intake returned, and never re-feeds that intake, so a recovered round states Received, never Saved, and applies no answer twice.
A runner killed after its keyed-answer intake returned but before the receipt seam ran left that round's verdict staged on disk and unjournaled. Reconcile's seam recovery always handed the adapter a fresh empty outcome, so the Lavish record gained Received and permanently lost `saved X of Y` for that round: the captain's visible acknowledgement silently dropped a state no later poll could restore. The runner now stages a generation note beside the outcome naming the exact source and sequence that outcome speaks for, and recovery hands the seam that recorded verdict when the note matches the round being recovered. A generation whose runner died before the intake returned still gets an empty outcome, because no verdict was recorded; the intake is never re-fed either way, so no answer can be applied twice. The note, not the staging file name, is what binds an outcome to a round, since a source id may itself contain the separator.
… harden the receipt path Three corrections from post-publication review: - The keyed-answer intake is external and can hang on the backlog, and it ran while the runner held the per-source lock, wedging every reconcile, publication, and retirement for that source. Release the lock after the durable capture and the staged outcome/generation note, run the intake unlocked, and re-acquire for the seam. Ordering is preserved by making the staged generation note the publication marker: publish_result declines a generation whose seam is still owed, and recover_receipt_seams reclaims the seam only once the runner's claim is gone, so a live runner is never double-seamed and a dead runner's generation is still recovered before publication. - feed_keyed_answers ignored the adapter's own exit status, so an answer extractor that failed mid-stream could still journal a verified save over rows nobody vouches for; state the outcome as fed with an incomplete quality instead. The outcome is also written to a staging name and renamed into place, so recovery after a mid-write death reads a whole verdict or none, and the staging name joins the claim's cleanup set. - Explicit retirement now takes the adapter's receipts lock, in the established order, before unlinking the record, so a writer holding that lock cannot recreate it after the removal.
- managed-poll builds the receipt snapshot and journals its armed record under one receipts lock, so handler state appended between the two cannot be hidden by a stale snapshot marking a newer count delivered. - cmd_terminal no longer treats an empty journal as proof that a final submission carried nothing: terminal requires a completely parsed result with no receipt-worthy submission, so a refused parse or a failed append keeps the source armed instead of retiring silently. - arming refuses while captured results of an earlier session on the same artifact remain unhandled, so the reset cannot orphan their Applying and Complete from the received rounds they speak for. - comment-only submissions are journaled and acknowledged like any other round: the submission summary carries the row count, the seam journals on receipt-worthy rows rather than choice and message counts alone, and the visible receipt says the comment was received instead of claiming zero answers. - perl_rows refuses a block that declares more rows than it delivers, so a truncated response can never be summarized into a false acknowledgement; malformed rows stay tolerated row-wise, as the read path already tolerates them. - a replay whose own outcome proves a new effect (the intake closed an answer this time) is presented and saved as a new action instead of being hidden behind an actionless already-received. - the convergence test waits for the live capture's seam marker before counting journal events, removing a nondeterministic read.
…and receipts lock order
…t, add receipt regressions
…ging mode, fix ancestor docs
… let the receipt suite run without installed lavish-axi The module header still described one unbroken hold from durable capture through the seam; the actual invariant after the lock-scope change is the staged outcome/generation note that publish_result checks, with the hold released for the external feed and re-acquired for the seam. State that in the header, including the incomplete-quality verdict. The receipt suite's adapter invocations now carry the stub directory in PATH, so arm and managed-poll's presence check finds a lavish-axi on CI runners where the real binary is not installed; the suite previously died at its first scenario there. Verified by running the suite with the real lavish-axi removed from PATH.
…lug staging leaks
…pty-block terminal
…base branch The rebase target added fm_procevent_state_root_resolve, which resolves the state root to its physical directory once and validates that directory, and it ships tests requiring a home reached through an arbitrary symlinked ancestor to register, claim, capture, and publish normally. This branch's own two ancestor commits solved the same macOS temp-dir problem the other way, by refusing every symlinked ancestor except the system /tmp and /var aliases, so they were dropped during the rebase. Their leftovers went with them: the regression asserting a redirected state root is refused (the base branch now asserts the opposite), and the extension-bindings sentence describing the refusal, restored to the base branch's wording. The resolve-then-validate contract is already documented in docs/configuration.md's process-event section.
93574ff to
3518164
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3518164b28
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| sleep 0.1 | ||
| done | ||
| if [ "$receipts_lock_taken" -eq 1 ]; then | ||
| rm -f -- "$(fm_procevent_receipts_path "$STATE" "$id")" |
There was a problem hiding this comment.
Preserve receipts until captured results are handled
Captain, if fm-procevent.sh retire is called after a Lavish result is captured but before its handler runs applying and complete, this removes the receipt journal while leaving the captured result intact. Both handler commands then fail because their required received event has disappeared, so explicitly stopping future polls prevents completion of the durable receipt lifecycle for already-captured work; retain the journal while unhandled generations remain, or defer its cleanup to handling/home sweep.
Useful? React with 👍 / 👎.
| if [ "$replay" != '-' ] && [ "${head_line#fed }" != "$head_line" ]; then | ||
| [ "${closed:-0}" -gt 0 ] && replay='-' |
There was a problem hiding this comment.
Distinguish idempotent closes from newly applied answers
Captain, when the captain resubmits an exact answer that was successfully applied earlier, the keyed-answer intake reports that matching durable resolution as closed: again (fm-captain-hold.sh lines 732-740). This condition therefore clears the replay marker even though the second intake changed nothing, and the page falsely presents the duplicate as another received-and-saved action; use an outcome that distinguishes an idempotent close from a newly applied close before overriding Already received.
Useful? React with 👍 / 👎.
A runner that dies after staging its receipt outcome leaves that verdict under its own claim token. The ordinary recovery route is a replacement runner, whose claim acquisition reclaims the dead claim but inherits only the source - never the older generation's owed seam. Recovery deferred to any live claim, so a replacement sitting in a managed poll kept that round unacknowledged and its capture declined by publication for as long as it polled, which a managed poll can do indefinitely. Recovery now reads the staged verdict before deciding: a live or terminal-marked claim keeps only the generation staged under its own token, and a verdict named for a superseded token is recovered while the replacement still polls. An unreadable claim proves nothing about ownership and is still left alone.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4af198ef46
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if [ -z "$outcome" ] || [ -z "$live_token" ] \ | ||
| || [ "$outcome" = "$(staging_file "$id" "$live_token.rcpt")" ]; then | ||
| fm_procevent_source_lock_release "$id" | ||
| continue |
There was a problem hiding this comment.
Recover unstaged captures despite a replacement claim
Captain, if the original runner dies after durable capture but before creating its .rcpt.output, a direct start can install a replacement claim that is still blocked in its poll; this condition then skips recovery solely because outcome is empty, while publish_result has no staged marker to suppress the old generation's wake. The old capture can therefore be handled without its receipt seam ever running, and the replacement poll may remain live indefinitely. Fresh evidence beyond the earlier staged-outcome report is that the new token comparison only handles generations that reached outcome staging; recover a captured generation that demonstrably predates the live claim even when no outcome exists.
AGENTS.md reference: AGENTS.md:L119-L119
Useful? React with 👍 / 👎.
| if [ "$rows" -gt 0 ] && ! has_event "$id" received "$seq"; then | ||
| journal_append "$id" \ | ||
| "$(printf 'received\t%s\t%s\t%s\t%s\t%s\t%s' "$seq" "$epoch" "$choices" "$messages" "$digest" "$replay")" || true | ||
| submission_line=received |
There was a problem hiding this comment.
Do not acknowledge failed journal writes
Captain, when a receipt append fails, such as from a transient I/O or permission error, this path still sets submission_line=received and ultimately returns success; the runner then records the generation as receipted, preventing recovery from retrying it. For final feedback, the current capture stays armed because terminal cannot find its received event, but the replacement poll's next empty ended capture sees zero journaled rounds and retires the source, permanently omitting the captain's receipt; propagate journal failures or otherwise retain the owed seam until the facts are durable.
Useful? React with 👍 / 👎.
Intent
Ship the post-publication review corrections for Firstmate's Lavish answer-receipt lifecycle PR (#3438) to the upstream firstmate repo through the no-mistakes pipeline, updating the SAME PR, ending at green checks that are never merged by this worker.
Context: the branch fm/lavish-receipt-upstream already carries the four selected receipt commits from the captain-authorized best-of-two selection (candidate A, selected head e433cf76b83ce28ea7756b323cc9c03a4e85a3f0, common base 12f6613), the two upstream-compatibility fixes, and the prior pipeline's own review/test/document/ci fix commits (through 05b363a, which the PR's green report covered). Every prior pipeline fix commit must be kept; nothing on the branch may be reset or rewritten.
On top of that, this run validates four new commits (1ec0a5f, 7061be0, df549ff plus the test-determinism wait folded into df549ff) that resolve the PR's post-publication review threads. All 13 review threads were verified against the exact head and answered with evidence; two were already corrected by prior pipeline commits (94e28ce recovery of the seam a dead runner owed; 05b363a recovery reading the dead generation's own staged verdict); the other eleven are corrected by these commits:
Accepted behavior that must be preserved: visible Received / Saved / Applying / Complete / Already received states appear only after their durable facts, duplicates are idempotent and visibly already-received, one visible receipt per submission round (agent-reply only on the first delivery attempt of a round, bare quiet retries), remote text stays untrusted data, the per-source boundary orders capture against publication, receipt records outlive automatic retirement until the sweep boundary, and the published-poll loss limitation stays as documented. The evidence under .evidence/ remains valid: choice/message rounds' visible text is unchanged, and the comment-only wording covers a case that previously displayed no receipt at all; regenerate final-head evidence only if a pipeline fix changes the already-evidenced visible output.
Verification expectations (all confirmed green on this exact head immediately before the run; they must stay green): tests/fm-procevent-lavish-receipt.test.sh, tests/fm-procevent.test.sh, tests/fm-procevent-when.test.sh, tests/fm-captain-hold-lifecycle.test.sh, tests/fm-classify-decision-key.test.sh, tests/fm-bearings-board.test.sh, tests/fm-documentation-audiences.test.sh, shellcheck on changed shell scripts, and CI=true bash bin/fm-lint.sh.
Delivery: process every gate through completion, push to the existing PR branch fm/lavish-receipt-upstream so #3438 is updated (same PR, never a new one), and require green checks. Never merge the PR; merge authority stays with Firstmate and the captain. Ask-user findings are escalated, never self-answered.
Delivery update per the captain's instruction: this run opens the REPLACEMENT PR for the superseded #3438, which stays unmerged and will be closed as superseded once the replacement is green. Two CI failures on the old PR are accounted for: (1) Behavior portable serial 1 failed because the receipt suite's arm/managed-poll invocations could not find lavish-axi on CI runners; fixed on this head by carrying the suite's stub directory in PATH for adapter invocations, verified by running the suite with the real lavish-axi removed from PATH. (2) The PR-must-be-raised-via-no-mistakes enforcement failed because the old PR body attestation bound to a superseded head (12d24c7); this run's push and pr steps raise the replacement with a fresh attestation bound to the current head, which is the required fix. Process every gate, push the new branch, open the replacement PR against upstream, require green checks. Never merge.
What Changed
bin/fm-procevent.sh,bin/fm-procevent-lib.sh). The runner stages the intake outcome beside a generation note under the per-source boundary, releases that boundary so the keyed-answer intake runs unlocked, then re-acquires a fresh hold to callreceiptand records a per-generation marker; publication declines a generation whose staged outcome the seam has not seen, reconcile recovers the seam a dead runner owed (including that generation's staged verdict) before publishing or starting a replacement, andretiretakes the adapter's receipts lock before unlinking. Ancestor-symlink validation for private procevent directories accepts only the OS/tmpand/varaliases and their/privatetargets.bin/fm-procevent-lavish.shjournals a per-source<id>.receiptsrecord and presents it through the published poll's--agent-replysurface: Received, Saved, Applying, Complete, Already received, plus newreceipt,applying,complete, andreceipt-textcommands. Comment-only rounds are acknowledged as the written comment they were. A final submission stays armed until its parse is complete,armrefuses while captured generations of the artifact are unhandled,perl_rowsrejects a prompt block declaring more rows than it delivers, and a replay whose own outcome proves a new effect is saved as a new action.tests/fm-procevent-lavish-receipt.test.sh(1197 lines) covering each visible state, recovery, and idempotence, withSend & Endregressions extended intests/fm-procevent.test.sh; docs updated indocs/configuration.md,docs/verification/process-event-sources.md,AGENTS.md, and the process-event-sources skill; live visual evidence checked in under.evidence/.Risk Assessment
✅ Low: The delta under re-review is two narrow, correct hardening fixes (a stale-staging removal that provably cannot destroy any consumable verdict, and a umask-scoped private creation) plus one genuine behavior regression, and a full pass over the surrounding capture/seam/recovery/publication ordering, lock ordering, and sweep boundaries surfaced no substantiated defect or intent contradiction.
Testing
Ran the seven suites the intent requires to stay green plus the two evidence-producing manual verifications; everything passed and the worktree is clean. Beyond the suites, I drove the real adapter through six submission rounds and captured what the captain actually sees in the review page: the comment-only acknowledgement ("received your written comment at ...") for a round that previously displayed no receipt at all, the already-received replay line, the incomplete-save and partial-save wordings, Applying then Complete appearing only after the handler's own calls, and a prompt block declaring three rows while delivering one being refused with nothing acknowledged — with the durable 0600 receipts journal behind every line. I also reproduced the CI-runner condition that broke the superseded PR by deleting the real lavish-axi from PATH; the receipt suite passes 22/22 there, confirming the stub-in-PATH fix. Separately, with a fixture adapter hung forever inside the keyed-answer intake, reconcile and retire both completed in two seconds and the owed verdict and generation note survived for recovery, which demonstrates the headline claim that a hung backlog cannot wedge the source. The four visible strings the committed .evidence manifest claims still reproduce byte-for-byte on this head, so that evidence needed no regeneration. The one thing I could not confirm is the intent's ancestor-symlink refusal, which this head deliberately does not implement.
Evidence: Same receipt text as a rendered HTML artifact
Source: Same receipt text as a rendered HTML artifact
Evidence: End-to-end receipt lifecycle transcript (visible text after every round)
Source: End-to-end receipt lifecycle transcript (visible text after every round)
----- what the captain sees in the review page ----- Round 1: received 1 answer and a message at 2026-09-02 07:12 UTC; saved 1 of 1 at 2026-09-02 07:12 UTC; complete at 2026-09-02 07:12 UTC. Round 2: received your written comment at 2026-09-02 07:12 UTC. Round 3: already received at 2026-09-02 07:12 UTC (identical to round 1); no new action. Round 4: received 1 answer at 2026-09-02 07:12 UTC; its saving report was incomplete at 2026-09-02 07:12 UTC - firstmate follows up in chat. Round 5: received 2 answers at 2026-09-02 07:12 UTC; saved 0 of 2 at 2026-09-02 07:12 UTC (2 not saved - firstmate follows up in chat). --------------------------------------------------- (Round 6 declared three prompt rows and delivered one: the parse was refused, so it is deliberately absent.) receipts record (mode 0600): fm-lavish-receipt.v1 received | 1 | 1788333162 | 1 | 1 | 1eec87129a01414b | - saved | 1 | 1788333162 | 1 | 0 | ok applying | 1 | 1788333163 complete | 1 | 1788333163 received | 2 | 1788333164 | 0 | 1 | 2db9393bf4bc48b3 | - received | 3 | 1788333165 | 1 | 1 | 1eec87129a01414b | 1 saved | 3 | 1788333165 | 0 | 0 | ok received | 4 | 1788333166 | 1 | 0 | 416a9dc1a648b807 | - saved | 4 | 1788333166 | 0 | 0 | incomplete received | 5 | 1788333166 | 2 | 0 | 3b7f89d6ba53b032 | - saved | 5 | 1788333167 | 0 | 2 | okEvidence: A hung keyed-answer intake does not wedge reconcile or retire
Source: A hung keyed-answer intake does not wedge reconcile or retire
registered source feed-src with an adapter whose keyed-answer intake blocks forever the runner is now blocked INSIDE the keyed-answer intake (generation note staged): state/procevent/.feed-src..claim.r6TQmc-8258.rcpt.output.gen while it is blocked there, ask the runner to reconcile the same source: reconcile exit=0 after 2s <- not wedged by the intake and retire the same source while the intake is still blocked: retire exit=0 after 2s <- not wedged by the intake claim after retire : reaped staged verdict : preserved - its receipt seam is still owed generation note : preserved - recovery still needs itEvidence: Receipt suite green with the real lavish-axi removed from PATH (old PR's CI failure #1)
Source: Receipt suite green with the real lavish-axi removed from PATH (old PR's CI failure #1)
$ PATH=<no lavish-axi> CI=true bash tests/fm-procevent-lavish-receipt.test.sh ... 22 scenarios ... ok - explicit retirement preserves a verdict its receipt seam still owes all Lavish receipt tests passed EXIT=0Evidence: Reproduction script for the end-to-end captain view
Source: Reproduction script for the end-to-end captain view
Evidence: Reproduction script for the unlocked-intake check
Source: Reproduction script for the unlocked-intake check
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 3 issues found → auto-fixed ✅
docs/extension-bindings.md:187- Intent conformance: the intent marks as REQUIRED "fm-procevent-lib.sh: ancestor-symlink validation restored for private procevent directories - any symlinked ancestor component is rejected except the operating system's own /tmp and /var aliases and their /private targets, so macOS temp-dir homes validate while arbitrary ancestor redirection fails. (Thread: Preserve validation of non-system ancestor symlinks.)" That behavior is absent from this head. Commit ef5a306 deliberately drops it: it reverts the extension-bindings sentence to the base wording (docs/extension-bindings.md:187) and deletes the tests/fm-procevent.test.sh regression that asserted a redirected state root is refused, on the grounds that the rebase base's fm_procevent_state_root_resolve (bin/fm-procevent-lib.sh:602) supersedes it. I verified the base helper resolves the state root through any symlinked ancestor and validates only the resulting physical directory, and that the base branch ships tests requiring a home reached through an arbitrary symlinked ancestor to register, claim, capture, and publish normally. So arbitrary ancestor redirection now succeeds rather than fails, and the named review thread is closed differently than the intent states. The rebase reasoning looks technically sound, but reversing a criterion the intent marks required is the captain's call, not the reviewer's.bin/fm-procevent.sh:1207- recover_receipt_seams stages its empty recovery outcome at a PID-derived path with no reaper, so a leaked file plus PID reuse silently and permanently skips the receipt seam. If a reconcile is killed between(umask 077; : > "$staged")(bin/fm-procevent.sh:1208) and the trailingrm -f -- "$staged", the file$REG/.<id>.reconcile.<pid>.rcpt.outputsurvives: remove_staged_generation and the claim reclaim in fm-procevent-lib.sh both key on the claim token, cmd_sweep_home removes only .source/.runner/*.receipts, and staged_receipt_outcome skips it because it has no .gen note. A later reconcile whose PID collides then fails[ ! -e "$staged" ], leaves bothstagedandoutcomeempty, skips the wholeif [ -n "$outcome" ]block, and never calls fm_procevent_mark_receipt_seam - so the generation is retried and skipped on every reconcile forever. For a lavish source this is user-visible: noreceivedevent is journaled, so cmd_terminal'shas_event "$id" received "$seq" || return 1(bin/fm-procevent-lavish.sh:829) never passes and an ended review stays armed and polling indefinitely. Fix:rm -f -- "$staged"before creating it (that name can never collide with a live claim token), or give the staging name a unique mktemp-style suffix.bin/fm-procevent-lavish.sh:271- journal_append creates the private receipts record with a bare redirect plus a best-effort chmod:printf '%s\n' "$RECEIPT_SCHEMA" > "$f"followed bychmod 0600 "$f" 2>/dev/null || true. Under a permissive caller umask (0022, or 0000 from a daemon) the file is created 0644/0666, and if the chmod fails it stays group/other-readable permanently; $REG itself may legitimately be 0755 because fm_procevent_private_directory_valid only refuses group/other write. Exposure in the window is small (only the schema line), but this contradicts the module's own convention - see the umask comment in feed_keyed_answers (bin/fm-procevent.sh:472, "neither may depend on the caller's umask for its privacy") and the existing "handled marker is private under a permissive caller umask" regression in tests/fm-procevent.test.sh. Use(umask 077; printf '%s\n' "$RECEIPT_SCHEMA" > "$f")and treat a chmod failure as an error rather than|| true.🔧 Fix: clear stale reconcile staging, create receipts record private
✅ Re-checked - no issues remain.
bin/fm-procevent-lib.sh:639- The intent's first required bullet states that fm-procevent-lib.sh rejects any symlinked ancestor component for private procevent directories except the OS's own /tmp and /var aliases, so "arbitrary ancestor redirection fails". Head 48651c2 does not do this: fm_procevent_state_root_resolve() resolves the state root to its physical path and validates that physical directory (ownership, no group/world write, target not itself a symlink), accepting any symlinked ancestor. Commit ef5a306 dropped the refusal deliberately, because the rebase base branch ships tests/fm-procevent.test.sh's "a home reached through a symlinked ancestor runs its sources normally", which asserts the opposite and which passes on this head. So the thread's macOS concern is satisfied, but the stated refusal of arbitrary redirection is not implemented. Restoring it would fail the upstream base branch's own test, so this needs the captain's decision rather than a test-phase fix.bash tests/fm-procevent-lavish-receipt.test.sh— 22/22 scenarios, exit 0bash tests/fm-procevent.test.sh— 64/64 scenarios, exit 0 (includes the new unlocked-intake and Send&End receipt regressions, and the base branch's symlinked-ancestor home scenario)bash tests/fm-procevent-when.test.sh— exit 0bash tests/fm-captain-hold-lifecycle.test.sh— exit 0bash tests/fm-classify-decision-key.test.sh— exit 0bash tests/fm-bearings-board.test.sh— exit 0bash tests/fm-documentation-audiences.test.sh— exit 0env -i PATH=<symlink farm of ~/.local/bin minus lavish-axi>:/opt/homebrew/bin:/usr/bin:/bin CI=true bash tests/fm-procevent-lavish-receipt.test.sh— 22/22, exit 0; reproduces the CI-runner condition behind the old PR's "Behavior portable serial 1" failure and shows it fixedManual end-to-end captain view: drovebin/fm-procevent-lavish.sh source-id / receipt / applying / complete / receipt-textthrough six real submission rounds in a scratch FM_HOME and captured the verbatim visible receipt after each one (receipt-lifecycle-demo.sh)Manual lock-freedom check: fixture adapter blocking forever inside the keyed-answer intake, thenbin/fm-procevent.sh reconcileandbin/fm-procevent.sh retire feed-srcagainst the same source while it is still blocked (unlocked-intake-demo.sh)Cross-checked the four visible receipt strings claimed in the committed.evidence/MANIFEST(success, partial, duplicate, completion) against this head's realreceipt-textoutput — all reproduce byte-for-byte, so the committed live screenshots remain validRendered and screenshotted the head's verbatim receipt text in the Lavish conversation-panel context viachrome-devtools-axi open+screenshotat 1280x800 (headless session, stopped afterwards)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.