Skip to content

fix(sessions): recover writer authority and verify publication joins - #4388

Draft
ScriptedAlchemy wants to merge 23 commits into
masterfrom
fix/4372-writer-authority-epoch-reopen
Draft

ScriptedAlchemy wants to merge 23 commits into
masterfrom
fix/4372-writer-authority-epoch-reopen

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Losing the daemon authority record could reopen a store with a writer epoch below its durable checkpoint, leaving observation admission retrying generic infrastructure failures. Read the exact shard/incarnation checkpoint before publication, retain its epoch floor, and retire the refused physical attachment on the blocking executor. Retry once when no live graph owner retains the binding; otherwise return typed WriterAuthorityBehindPersisted while preserving live leases. Ledger fences remain typed and rejected writes roll back.

Named Hermes inline transcript callbacks join their own pinned source frontier. Success requires an active temporal generation, an applied relation receipt, and its expected graph watermark. Pending resets, retired generations, and future frontiers cannot supply publication proof. Another session's failure cannot reject a proven publication; the requested session's failed or unproven projection remains unavailable. Source sweeps and unproven provider identities retain the aggregate publication contract.

Pending joins finish advancing bounded discovery pages before waiting for derived-summary admission or the next historical window. This fixes the complete-receipt/zero-backlog batch-join stall without changing deadlines or work budgets. Preserve the exact registry idle-completion assertions through rekey and retirement.

Validation:

  • The reconciled implementation passed 57 focused Bazel tests: 48 scheduler/publication tests, 3 store-publication tests, and 6 MCP join tests. Coverage includes mixed successful/failed sessions, receipt/watermark/reset checks, batch-join progress, owner isolation, cancellation, rekeying, and recovery.
  • Strict Bazel Clippy (-D warnings) passed for session-runtime, session-temporal-store, MCP, root unit, root session integration, and root MCP integration targets before the subsequent master integration.
  • The current head preserves the peer integration commit 1b1764b4276be8c35396c68d6f8f9dd50095ede4 and commits d41885d4/a9324cea, and includes master through cd9fde234d (including cache-output revalidation). The focused results above do not establish validation of this final combined tree.

The idle-notification helper is compiled under the same test/test-helper configurations as its callers, addressing the prior strict-Clippy unused-function failure. Full hosted CI is explicitly dispatched on 810bed1e23ff7d3ec0f651e4d28bc765266d38db: CI run 39762409083. This PR remains a draft pending that validation; ordinary draft skip-green checks are insufficient.

Fixes #4372

@changeset-bot

changeset-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 810bed1

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

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review October 21, 2026 00:03
devin-ai-integration[bot]

This comment was marked as resolved.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Pushed 6b9c4e2d96cea73b6d553b4e1fbd347396805755 to address the two concrete recovery blockers.

Both persisted-checkpoint reads use the async exact-SQL path. A refused physical attachment is drained, closed, and joined on the existing blocking executor before retry; close failures propagate.

For graph-first stores, a live graph publication's immutable binding cannot be replaced safely while owners or operation leases retain it. Recovery now retains the persisted epoch floor and fails once with typed WriterAuthorityBehindPersisted, preserving existing leases. It does not perform the same doomed physical open twice. After canonical owner/client release and graph close, reopening allocates a shared binding above the checkpoint and can commit. That follows #4372's explicit “adopts the persisted epoch, or fails once with a typed error” contract.

Added a real GraphDbRegistry owner/client + SQLite-checkpoint regression covering the refusal, continued validity of old leases, release/close, and successful reopen/write. Existing reopen coverage also checks native-worker wait telemetry. Pinned rustfmt and diff checks pass; native tests remain for CI.

The PR description now names //crates/tracedecay-cli:core_cli_suite for the isolated daemon journey. The previous blocked command selected a binary target, so it was not evidence that this journey had run.

devin-ai-integration[bot]

This comment was marked as resolved.

@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) October 22, 2026 00:45

@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 2 new potential issues.

3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +834 to +846
let frontier = access
.session_temporal_publication_result(session_id)
.await
.map(|publication| publication.observed_through())
.map_err(|error| {
tracing::warn!(%session_id, %error, "session publication target unavailable");
})
.ok();
entry.insert(frontier);
}
let Some(frontier) = frontiers.get(session_id).copied().flatten() else {
let _ = join.completion.send(false);
continue;

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.

🟡 Transient reads fail session publication joins

When a publication read fails, capture_projection_targets rejects the named join before the worker projects the session. A transient read failure makes a successful Hermes ingest return temporal_refresh_unavailable.

Learn more

A named join first reads the session's output-producing observation frontier, then the worker projects that frontier. A failed snapshot or query becomes None, and the sender immediately resolves false; the worker never gets a chance to retry the read or project that join. The publication read in settle_projection_joins likewise converts a read failure into a negative result after the pass. The caller reports a retryable unavailable hook result despite a potentially successful write.

Example: A Hermes callback admits a message for session s, but a temporary database read error occurs while capturing s's frontier. Its join returns false immediately, even if the worker can read and publish s on its next pass.

Recommended fix: Retain named joins when their frontier or publication read fails transiently, requeue a bounded projection attempt, and resolve them only on proof, a definitive failure, cancellation, or the caller's deadline. Preserve an explicit distinction between read errors and a successful read with no receipt.

Devin Review


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

Comment on lines 1042 to 1045
/// Join the worker's idle boundary, including background work. Callers
/// proving a rekey handoff still need that boundary; live transcript
/// publication uses the separate projection joins above.
pub async fn wake_and_wait_until_idle(&self, timeout: std::time::Duration) -> bool {

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.

🟡 Idle join has no production caller

wake_and_wait_until_idle has no caller, including the rekey handoff it describes. This leaves a second, unused publication-wait contract in production.

Learn more

The repository requires production APIs to have real callers and forbids staging dead code. The new idle-boundary API is unused throughout the workspace; current production publication calls use the projection join instead. Unlike that join, this method also waits for background work, so retaining it creates a distinct, unexercised contract.

Example: Rekeying a project calls rekey_project, which joins retired supervisors directly; it never calls this idle join.

Recommended fix: Remove the unused method unless a real production handoff needs its idle-boundary semantics. If one does, wire that caller and test its behavior under pending background work.

Devin Review


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

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as draft October 22, 2026 00:49
auto-merge was automatically disabled October 22, 2026 00:49

Pull request was converted to draft

@ScriptedAlchemy ScriptedAlchemy changed the title fix(store): converge reopened writers on persisted authority fix(sessions): recover writer authority and join live publication Oct 22, 2026
@ScriptedAlchemy ScriptedAlchemy changed the title fix(sessions): recover writer authority and join live publication fix(sessions): recover writer authority and verify publication joins Oct 22, 2026

This branch has not been deployed

No deployments
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.

Daemon rejects every project write with a stale writer authority epoch after reopening a store with stuck projections

1 participant