fix: race-proof command-turn lifecycle + non-wedging final outbox (production incident root causes)#8
Merged
Conversation
Merged by Gitmoot after policy gate passed.
Merged by Gitmoot after policy gate passed.
Merged by Gitmoot after policy gate passed.
Merged by Gitmoot after policy gate passed.
# Conflicts: # docs/answer_decision.md # src/tendwire/backends/herdr_decision.py # src/tendwire/backends/herdr_turns.py # src/tendwire/command_submission.py # tests/test_answer_decision.py
Upstream 9919e76 scoped final_ready head-of-line blocking by payload stable_key; the ordering_key column introduced here materializes the same owner identity at enqueue time (with orphan fallbacks), so the payload-JSON clauses are dropped as subsumed. dead_letter stays terminal for ordering per the incident verdict — the owner-isolation test now asserts a dead_letter final blocks no later finals at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
46abd8c stamps working_predecessor_turn_id only when the final's turn id differs from the command predecessor; adoption preserves the command row's id, so the pointer is correctly absent — assert that instead of the split-id flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR fixes the root causes of a multi-day production incident in the herdres↔tendwire bridge: completed agent replies were never delivered and one stuck inbound message blocked all Telegram ingress for 24h. A three-model research panel (Claude Fable, GPT-5.6-sol, Kimi K3) independently confirmed the same root causes with file:line receipts; the full verdict is reproduced below in condensed form.
Root causes fixed
source_turn_id-hash vsorigin_command_id-hash), linkage is frozen post-insert, and no sweeper/TTL existed — so theactivesubmission row was immortal and, because the turns list orders by insertion sequence only, it permanently shadowed the completed row. Downstream consumers suppressed every real final as "historical" and ingress trackers waited forever.poll_connector_outboxblocked laterfinal_readyrows behind ANY earlier row not in (delivered,superseded) — includingdead_letter,awaiting_ack,leased,deferred— globally per host+connector.awaiting_ackcould never be reclaimed (plan-commit cleared the lease fields the reclaimer scans). Attempt accounting couldn't distinguish real failures from ack-timeout reclaims, so slow-but-healthy plans dead-lettered.Changes
Turns lifecycle (schema v15):
send_startedtransaction, before pane I/O, with a pre-input-failure cleanup path (post-input failures stay uncertain by design).turn_id+list_sequence); the submission upsert adopts a matching recent observed row. Exact-normalized-text matching, recency-bounded (60s window), tombstone-aware, ambiguity fails closed to previous behavior.superseded_by_turn_id/superseded_at, list-filtered, never physically deleted) with grace/hard TTL sweeping (configurableTENDWIRE_TURN_CLAIM_HARD_TTL_SECONDS, default 24h) so downstream trackers always terminate. Rate-limited lazy sweep + scheduler-tick sweep.turn_id(supersession chains followed, cycle-safe) and a liveobserved_turn_state.used_donetracking, ambiguity skipped, traversal generation bumped once).Connector outbox (schema v16 + v17):
dead_letterandawaiting_ackare non-blocking for the root FIFO; ordering is scoped per worker via a new backfilledordering_keycolumn (unresolvable rows get per-roworphan:partitions — never a shared sentinel). Parts inherit their root's enqueue-time key; source-less recovery plans keep their FIFO place.awaiting_ackrows carry deadlines, extended on part-ACK progress; expired ones are reclaimed via plan-recover, skipping jobs with live leases; ack-timeout reclaims do not consume the delivery-failure budget.connector.renew/connector.releaseoperations; eager reclaim on the daemon tick behind a read-only due-check.Testing
Full suite green at each stage (2,811 passed / 2 skipped at head; the only recurring failure in our sandbox is a benchmark privacy-scan false positive on short PIDs, byte-identical file untouched). Review process: each PR went through multi-agent review with adversarial verification; every MUST-fix item carries a regression test that was mutation-verified (test fails against the pre-fix body). Known follow-ups (test-quality only) are noted in jerryfane#6's review comments.
Also merged: your four recent main fixes (
52dd236,46abd8c,d846741taken as-is; decisions-calibration files resolved to your live-verified versions). One deliberate supersession:9919e76scoped final head-of-line blocking by payload stable_key — this PR'sordering_keycolumn materializes the same owner identity at enqueue time (surviving worker re-keying, with per-row orphan partitions for unresolvable rows), so the payload-JSON clauses were dropped as subsumed. One semantic change relative to9919e76:dead_letterno longer blocks even its own owner's later finals (a poisoned reply must never wedge its worker; recovery goes through retry/supersede) — yourtest_dead_letter_final_blocks_only_its_stable_ownerwas adapted accordingly (nowtest_dead_letter_final_blocks_no_later_finals). Happy to revisit if you want dead_letter to stay blocking within an owner.Production notes
🤖 Generated with Claude Code