Skip to content

fix(sessions): stop repeating permanent ingest and hook failures - #4577

Merged
ScriptedAlchemy merged 11 commits into
masterfrom
fix/ingest-convergence-commit-messages
Oct 28, 2026
Merged

ScriptedAlchemy merged 11 commits into
masterfrom
fix/ingest-convergence-commit-messages

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Why

#4567 was merged independently while its writable repair branch was being verified. Its Pi completion shortcut can miss preserved-mtime rewrites, invalid headers can bypass durable reconciliation, and a process-lifetime hook retry cap can suppress recovery from transient prerequisites. This PR follows that merge with the reviewed canonical source-evidence and replay corrections.

Changes

  • Replace Pi's stat-only completion shortcut with the existing canonical rewrite witness, exact covered extent, post-admission witness check, and scoped compare-and-swap authority. Older-mtime replacements and stale concurrent writers remain correct.
  • Reconcile invalid headers through the durable authority with bounded content evidence. Unchanged malformed inputs remain visibly partial; changed or transiently unavailable inputs remain retryable.
  • Recheck queued project scope and bind the accepted header to the first complete frame of the exact admitted JSONL prefix before rewrite, cursor, coverage, or capture mutations. Cold, resumed, cached, deferred, and EOF reads carry or establish that proof; absent proof refuses admission. Existing durable resume fingerprints remain byte-for-byte unchanged.
  • Keep hook replay backoff owned by the consumer and scoped by host/event. Pending obligations stay visible and retryable; the canonical ledger owns settlement or expiry. Remove the terminal 16-attempt suppression and handle nested unavailable/backpressured orchestration outcomes.
  • Preserve the concurrent rebase onto current master and adapt its obsolete RefusalMemo-only regression to the actual Codex admission path. Matching, foreign, or unknown legacy memo generations and missing fingerprints cannot skip uncovered records. The canonical cursor advances, remains stable on replay, and changes generation and fingerprint after a same-length content rewrite.
  • Preserve the deterministic-refusal regressions and add real-file, scoped-CAS, interruption, header-race, and recovery-after-20-attempt coverage.

Validation

  • All four tests against the actual production ResumeDigest module passed: existing durable hash bytes, newline/chunk boundaries, resumed proof retention, and header changes beyond the 1,024-byte native identity window. The small path-imported harness uses the real implementation and existing sha2 dependency.
  • Normal maintained commit hooks, pinned Rust formatting, and whitespace checks passed.
  • All 60 generated BUILD files are current.
  • Root and independent source review approved the production repair, including fresh/resumed/zero-frame mutation ordering and hook recovery.
  • The concurrent rebase's production files were verified byte-identical to the reviewed integration. The final follow-up changes only the real admission regression; the peer commit and every production blob remain intact.
  • Hosted CI must compile and execute the Pi, admission, and hook integration regressions before merge. The local full native dependency build had already exceeded memory and has no reusable asupersync artifact.

Devin Review

@changeset-bot

changeset-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 407e798

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

devin-ai-integration[bot]

This comment was marked as resolved.

ScriptedAlchemy and others added 6 commits October 27, 2026 02:48
Deterministic per-item failures were re-driven forever: the in-process
source ledger forgot every converged source at each daemon restart, the
cover-past cursor advance was the only convergence mechanism for refused
records (and a non-retryable advance failure re-failed the whole file
every pass), and owed hook replay work was redriven every sweep with no
backoff or cap.

- pi: durable per-path completion and header-quarantine memos keyed by
  (len, sub-second mtime); an in-process discovery skip set for
  malformed headers; unchanged converged files never re-enter admission
- shared JSONL admission (pi, codex, kimi, vibe, cursor): a durable,
  generation-scoped refusal high-water memo acts as a never-regressing
  secondary frontier; a non-retryable cover-past advance failure now
  advances in-memory with one warning instead of failing the file;
  repeats below the memo frontier log at debug
- hook v2 replay: per-envelope exponential backoff (30 s to 1 h) and an
  in-process terminal cap after 16 attempts, with one terminal warning
  naming the envelope; the durable ledger is untouched

Fixes the non-converging loop family behind #4481 and #4564.

Co-authored-by: spa5k <79936503+spa5k@users.noreply.github.com>
- refusal memo floor only applies with a resume fingerprint, so the
  scanner validates the skipped prefix; a rewritten file diverges into
  committed-checkpoint recovery instead of skipping changed bytes
- the floor re-baselines the in-memory cursor so later frames advance
  contiguously; the cover-past fallback warns once per pass
- path memos bind the unix inode, closing the equal-length
  timestamp-preserving replacement case
- header quarantine (in-process and durable) records only deterministic
  InvalidSessionHeader failures; SessionHeaderUnavailable stays retryable

Co-authored-by: spa5k <79936503+spa5k@users.noreply.github.com>
The parse-offset writer is monotonic on mtime, and a prefix fingerprint
is unordered: a later refusal with a smaller fingerprint value wrote as
a no-op and stranded the durable floor behind the in-memory one. The
memo now persists via the exact compare-and-swap replace, which orders
on nothing but the (generation, offset, fingerprint) pair it carries.

The coverage-fallback once-per-pass flag was inverted: mem::take reset
it to false on every frame, warning per frame after all. mem::replace
sets it, keeping the warn to the first fallback of the pass.

Co-authored-by: spa5k <79936503+spa5k@users.noreply.github.com>
A get_parse_offset read failure is not an absent row: fabricating a
zero expected value CAS-fails against a real row and buries the read
failure inside a replace conflict. The read failure now keeps the
in-memory mark, surfaces its outcome, and defers the persist.

Co-authored-by: spa5k <79936503+spa5k@users.noreply.github.com>
Co-authored-by: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com>
Use canonical rewrite witnesses and scoped compare-and-swap for Pi
completion and invalid-header decisions. Keep quarantined coverage partial,
respect scan interruption, and recheck the current project at dequeue.

Bind Pi header scope to the admitted JSONL prefix before any mutation.
Retain first-frame evidence in the existing prefix digest without changing
durable fingerprints, including resumed and zero-frame pages.

Keep hook replay deferrals visible and retryable for the consumer lifetime.
Let the canonical ledger settle or expire work and prune retired gate state.
Add real-path regressions for rewrites, stale CAS, scope races, interruption,
and recovery after more than sixteen unavailable producer attempts.
@ScriptedAlchemy
ScriptedAlchemy force-pushed the fix/ingest-convergence-commit-messages branch from c4ba0e2 to 34f8eb2 Compare October 27, 2026 02:49
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) October 27, 2026 02:50
ScriptedAlchemy and others added 5 commits October 26, 2026 20:12
Exercise legacy refusal offset generations and missing fingerprints
through the current Codex admission seam. Verify committed coverage
advances, remains stable on replay, and changes generation after an
in-place rewrite.

Replace the stale RefusalMemo-only test whose production type was removed
by the canonical cursor cutover.
Clippy's assert_is_empty rejects assert!(…is_empty()); assert_eq shows
the unexpected failures when the check trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/tracedecay-sessions/src/runtime/hosts/pi.rs
#	crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs
Production registers a project's hook orchestrator once a complete
code-index generation exists, so admitted work owed to an unmounted
orchestrator is waiting, not failing. Keep it owed with the same backoff,
but publish it as a new hook_replay `waiting` status; genuine admission
failures still publish `failed`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +175 to +180
/// The spools drained, but producer work they admitted waits for the
/// project's hook orchestrator to mount. The work stays owed and runs
/// once it does; nothing has failed.
Waiting {
waiting: Vec<StatusHookReplayFailureV1>,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Waiting status breaks SDK status reads

When replay reports waiting, StatusHookReplayV1 has no matching variant in the generated SDK schema. The SDK decoder rejects the entire project status response.

Learn more

The TypeScript SDK decodes status responses against its generated StatusResultV1 schema. That schema references a StatusHookReplayV1 definition with four variants, and decodeCanonicalSchema rejects any other status. A project with owed orchestration work now sends a fifth variant, so an otherwise healthy status read fails decoding. The repository requires regenerating wire contracts after changing a Rust type that crosses the public API boundary.

Example: An admitted edit remains pending before the orchestrator mounts. The daemon sends hook_replay: {"status":"waiting","waiting":[...]}; an SDK status request rejects this response instead of returning the project's status.

Recommended fix: Regenerate and commit the canonical TypeScript SDK status schema and types using the dashboard contract generation workflow, then verify contracts:check and exercise a waiting status through the SDK decoder.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +541 to +545
Some(Some(sweep)) if !sweep.failures.is_empty() => StatusHookReplayV1::Failed {
failures: sweep.failures.clone(),
},
Some(Some(sweep)) if !sweep.waiting.is_empty() => StatusHookReplayV1::Waiting {
waiting: sweep.waiting.clone(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Failed sweeps conceal waiting producer work

If one spool fails while another host awaits orchestration, hook_replay_status returns only sweep.failures. The owed work disappears from project status until every unrelated spool failure clears.

Learn more

drain_all_hosts visits every host and can add failures for one spool while redrive_pending_work adds waits for another. A published sweep retains both lists, but the Failed branch returns only failures. Clients cannot observe the otherwise valid pending obligations while any unrelated failure remains.

Example: Claude's pending edit awaits the orchestrator and Kimi's receipt spool cannot be read. The sweep records one waiting entry and one failure; status reports Kimi's failure and says nothing about Claude's owed work.

Recommended fix: Extend the failed wire variant to include its waiting entries (or use a shared aggregate status shape), regenerate dependent contracts, and test mixed failure/waiting sweeps.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +113 to +117
HookV2AdmissionOutcomeV1::Admitted {
orchestration: HookOrchestrationAdmissionV1::Unavailable,
..
} => Some(AWAITING_ORCHESTRATOR),
HookV2AdmissionOutcomeV1::Unavailable => Some("was unavailable"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Broken orchestrators appear to be waiting

When a mounted orchestrator stops accepting work, unresolved_pending_work classifies its Unavailable result as awaiting mount. The runtime also returns Unavailable after task failure, so project status hides the failure.

Learn more

HookOrchestrationAdmissionV1::Unavailable does not identify why the orchestrator refused admission. admit_registered_hook_orchestration returns it when no runtime is registered, while admit_with_cancellation also returns it for a registered runtime whose task owner failed or stopped accepting work. The classifier maps both to a healthy wait, and the Waiting wire comment promises that work will run after mounting, even though a mounted failed runtime does not remount in that path.

Example: A project's orchestrator task panics and the next hook is admitted to the ledger. Redrive receives nested Unavailable; project status says waiting rather than identifying the failed orchestrator.

Recommended fix: Preserve a distinct no-registration outcome from the orchestration authority and mark only that state as awaiting mount; report registered-runtime failures as failures while leaving their obligations retryable.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Included in the integration PR #4589, which runs CI once for all open PRs. This PR will be marked merged automatically when #4589 lands.

@ScriptedAlchemy
ScriptedAlchemy merged commit 545d954 into master Oct 28, 2026
9 of 15 checks passed
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