Skip to content

fix: make failed public follow-ups deliverable - #67

Merged
kunchenguid merged 4 commits into
mainfrom
fm/tasks-axi-failed-relation-deliverable-r1
Sep 22, 2026
Merged

kunchenguid merged 4 commits into
mainfrom
fm/tasks-axi-failed-relation-deliverable-r1

Conversation

@kunchenguid

Copy link
Copy Markdown
Owner

Intent

"yes" - approving option A for this tasks-axi problem:

In tasks-axi public-followup, a promised-final obligation whose required work relation ends failed (for example a parked or failed task on an expected pr-merged or report-ready final) is accepted as a failed relation but never becomes deliverable: relationLanded/isPublicFollowupReady treat failed as terminal only when the expected type is failure-outcome, and beginDelivery refuses pending-work, so the owed public reply is stuck with no supported path (hit on a real obligation; the operator had to post manually and waive).

Reproduction evidence (real scripts, tasks-axi 0.2.5): seed a pr-merged promised-final bound to a work relation, emit a failed outcome for that work (error_code deliverable), consume, then deliver. Observed: consume prints no "ready" line; delivery state stays pending-work; the relation reads {"state":"failed","last":"failed"}; deliver exits 1 with "obligation ... is still waiting on its bound work; nothing to deliver yet"; nothing is posted. "Parked" has no typed outcome (work outcome types are pr-merged, report-ready, local-main, failed, superseded), so a parked lane can only report failed.

Option A: a failed required relation is terminal and deliverable for any expected type, using the accepted failed event's public_safe_outcome as the honest text, so the existing consume -> ready -> deliver path works unchanged. Add regression tests in tasks-axi.

What Changed

  • Treat failed required work relations as terminal and delivery-ready for every expected-final type when their failure deliverables are safe.
  • Upgrade legacy pending-work obligations affected by the old readiness rule to ready during normalization while preserving validation for other stale states.
  • Document the failure-delivery behavior and add readiness, migration, supersession, and end-to-end delivery regression coverage.

Risk Assessment

✅ Low: The change is narrowly scoped, preserves legacy validation outside the authorized migration, and covers both the end-to-end failure path and persisted-record upgrade behavior.

Testing

Targeted public-followup tests passed, and live CLI scenarios confirmed failed pr-merged and report-ready relations become deliverable using their public-safe outcomes, legacy pending-work records self-heal, and unrelated stale pending-work records still fail closed.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A promised pr-merged relation fails, becomes ready, and completes the normal delivery flow with its honest public-safe outcome ✅ pass live Failed pr-merged relation through completed delivery
A failed report-ready relation is also terminal and can begin delivery ✅ pass live Failed report-ready relation becomes deliverable
A 0.2.5-style failed relation persisted as pending-work reads as ready and can begin delivery ✅ pass live Legacy state upgrade and stale-state rejection boundary
A landed relation hand-edited to stale pending-work is not silently repaired ✅ pass live Legacy state upgrade and stale-state rejection boundary shows VALIDATION_ERROR and exit_status 2
Evidence: Failed pr-merged relation through completed delivery

Source: Failed pr-merged relation through completed delivery

$ tasks-axi public-followup add ... --json
{"ok":true,"action":"public-followup.add","id":"public-live-ab","delivery":"intent"}
$ tasks-axi public-followup bind-work ... --json
{"ok":true,"action":"public-followup.bind-work","delivery":"pending-work","relation":"bound"}
$ tasks-axi public-followup work-event ...failed... --json
{"ok":true,"action":"public-followup.work-event","delivery":"ready","relation":"failed","outcome":"failed","error_code":"deliverable","public_safe_outcome":"The promised fix could not be completed because the worker failed."}
$ tasks-axi public-followup ready --json
{"count":1,"ids":["public-live-ab"]}
$ tasks-axi public-followup begin-delivery ... --json
{"ok":true,"action":"public-followup.begin-delivery","delivery":"delivery-posting","attempt":1}
$ tasks-axi public-followup record-delivery ... --json
{"ok":true,"action":"public-followup.record-delivery","task_state":"done","delivery":"posted","public_safe_outcome":"The promised fix could not be completed because the worker failed."}
Evidence: Legacy state upgrade and stale-state rejection boundary

Source: Legacy state upgrade and stale-state rejection boundary

SCENARIO: 0.2.5-style failed relation persisted as pending-work self-heals on read
$ tasks-axi public-followup list --json
{"count":1,"id":"public-live-ab","delivery":"ready","relation":"failed","outcome":"failed"}
$ tasks-axi public-followup ready --json
{"count":1,"ids":["public-live-ab"]}
$ tasks-axi public-followup begin-delivery ... --json
{"ok":true,"delivery":"delivery-posting","relation":"failed"}

ADVERSARIAL SCENARIO: landed relation hand-edited to pending-work still fails closed
$ tasks-axi public-followup list --json
error: "public_followup.delivery.state is stale: work is ready"
code: VALIDATION_ERROR
help[1]: Repair the typed public-followup data before retrying
exit_status: 2
Evidence: Failed report-ready relation becomes deliverable

Source: Failed report-ready relation becomes deliverable

$ tasks-axi public-followup work-event ...failed report relation... --json
{"expected":"report-ready","delivery":"ready","relation":"failed","outcome":"failed","error_code":"parked","public_safe_outcome":"The report work was parked and did not produce a report."}
$ tasks-axi public-followup ready --json
{"count":1,"ids":["public-report-ab"]}
$ tasks-axi public-followup begin-delivery ... --json
{"ok":true,"delivery":"delivery-posting"}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (2) ✅
  • 🚨 src/public-followup.ts:1445 - The new readiness rule makes previously valid 0.2.5 records unreadable. A non-failure promise with a failed relation was persisted as delivery.state="pending-work"; after upgrading, relationLanded returns true, then normalization rejects that same record as stale at src/public-followup.ts:1107. Because backlog parsing normalizes metadata, ready, duplicate event consumption, and delivery all fail before they can repair the state. The regression at test/commands/public-followup.test.ts:406 only creates the event under the new implementation, so it does not cover the persisted failing sequence. Preserve or migrate the formerly valid pending-work representation so existing obligations can reach ready and delivery.

🔧 Fix applied.
2 errors still open:

  • 🚨 src/public-followup.ts:1445 - The new readiness rule makes previously valid 0.2.5 records unreadable. A non-failure promise with a failed relation was persisted as delivery.state="pending-work"; after upgrading, relationLanded returns true, then normalization rejects that same record as stale at src/public-followup.ts:1107. Because backlog parsing normalizes metadata, ready, duplicate event consumption, and delivery all fail before they can repair the state. The regression at test/commands/public-followup.test.ts:406 only creates the event under the new implementation, so it does not cover the persisted failing sequence. Preserve or migrate the formerly valid pending-work representation so existing obligations can reach ready and delivery.
  • 🚨 src/public-followup.ts:1107 - The round 1 fix self-heals every pending-work record for which readiness derives true, rather than only the authorized pre-fix shape with a failed relation. For example, a hand-edited record containing a normally landed matching event but stale delivery.state="pending-work" now silently becomes ready instead of failing closed, masking drift that was never produced by the old failed-relation rule. Narrow this branch to readiness attributable to the newly accepted non-failure expected type plus failed relation transition; retain validation for other pending-work/ready mismatches.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A promised pr-merged relation fails, becomes ready, and completes the normal delivery flow with its honest public-safe outcome ✅ pass live Failed pr-merged relation through completed delivery
A failed report-ready relation is also terminal and can begin delivery ✅ pass live Failed report-ready relation becomes deliverable
A 0.2.5-style failed relation persisted as pending-work reads as ready and can begin delivery ✅ pass live Legacy state upgrade and stale-state rejection boundary
A landed relation hand-edited to stale pending-work is not silently repaired ✅ pass live Legacy state upgrade and stale-state rejection boundary shows VALIDATION_ERROR and exit_status 2
  • pnpm exec vitest run test/commands/public-followup.test.ts test/public-followup.test.ts
  • Live CLI: add, bind, consume a failed pr-merged relation, query ready, begin delivery, and record delivery
  • Live CLI: consume a failed report-ready relation, query ready, and begin delivery
  • Live CLI: read a simulated 0.2.5 failed relation persisted as pending-work, then begin delivery
  • Live CLI adversarial check: read a landed relation hand-edited to pending-work and verify VALIDATION_ERROR with exit 2
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

… for any expected type

A promised-final obligation whose required work relation ends failed was
accepted but never became deliverable: relationLanded/isPublicFollowupReady
only treated `failed` as terminal when expected_final.type was
failure-outcome, so a pr-merged or report-ready promise whose work failed
(or was parked, which has no typed outcome) got stuck in pending-work with
no supported path to deliver the owed public reply.

relationLanded now treats a failed relation as terminal and deliverable for
any expected type, reusing the same failure-deliverable safety check
already enforced at accept time (validateWorkEventContract), so the
existing consume -> ready -> deliver path works unchanged and the honest
text is the accepted failed event's public_safe_outcome.
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