Skip to content

AgentCheckpoint has no production writer: grant binding and the continuation entry never activate outside tests #4043

Description

@chernistry

Found while re-sweeping #3649's remaining scope. Sibling to #3834/#3835/#3836 — those tested and merged the grant-binding and continuation-entry mechanism; this issue is about the fact that nothing in production ever exercises it.

Problem

AgentCheckpoint (src/bernstein/core/persistence/agent_checkpoint.py:136) is never constructed for writing anywhere in src/. Every hit for its constructor is a read, deserializing an existing file:

src/bernstein/core/persistence/agent_checkpoint.py:192:    return AgentCheckpoint(**json.loads(path.read_text()))
src/bernstein/core/persistence/agent_checkpoint.py:210:            candidate = AgentCheckpoint(**json.loads(path.read_text()))
src/bernstein/core/persistence/agent_checkpoint.py:247:            orphans.append(AgentCheckpoint(**json.loads(checkpoint_path.read_text())))

save_checkpoint (agent_checkpoint.py:171) — the only function that writes one — has zero callers in src/. scan_orphaned_checkpoints (agent_checkpoint.py:222) has zero callers anywhere. The module's only production consumer is src/bernstein/cli/commands/resume_cmd.py, which imports find_checkpoint_for_task and is_checkpoint_recoverable — both read-only — and never imports save_checkpoint.

Consequence: #3834's grant-comparison logic and #3835's continuation-entry logic (once #3835 lands) are both correct and unit-tested in isolation, but on a real run find_checkpoint_for_task always returns None — there is no checkpoint to check, because nothing ever wrote one with the new fields (or at all). The grant-binding feature #3649 asks for does not activate outside tests.

Separately, src/bernstein/core/tasks/suspension.py (#2552) already implements a more mature suspend/resume system for a different checkpoint concept — Merkle-chained journal rows (record_task_suspension_row), HMAC audit-chain receipts (record_task_suspension), and its own continuity verification (verify_suspension_continuity, suspension.py:1366) that does part of what #3649's ContinuationEntry is trying to do. Whether agent_checkpoint.py's heartbeat-based crash recovery is still live scope, or has been superseded by suspension.py without anyone updating this module's docstring, is not established anywhere.

Proposal

Decide between:

(a) Find or add the minimal correct production call site that constructs an AgentCheckpoint — with role, grant_hash, parent_run_id, chain_head_at_suspend populated — at the moment a real agent's work is actually suspended, and calls save_checkpoint. suspension.py's park_task (suspension.py:878) is the most likely existing suspend entrypoint to check first, since it already calls record_task_suspension_row at exactly the moment this issue is looking for.

(b) Determine that agent_checkpoint.py's crash-recovery path is superseded by suspension.py (#2552) and document that explicitly — a module docstring correction, not a new subsystem — so #3834/#3835/#3836 are understood as "correct and tested in isolation, not wired to a live run" rather than silently assumed active.

Why this shape

Building a full heartbeat-writer from scratch is not evening-sized and risks duplicating suspension.py's already-shipped continuity mechanism for a second, parallel checkpoint concept. A comment-only outcome is legitimate here if the decision comes back "superseded" — this issue's job is to force the decision with evidence, not presume the answer either way.

Scope

Does NOT include: redesigning suspension.py, building general heartbeat/crash-recovery infrastructure beyond the single suspend call site (a), or resolving #3834/#3835/#3836's own remaining acceptance criteria (tracked separately, unaffected by this issue's outcome).

Brief for a coding agent
GOAL: Decide whether AgentCheckpoint (agent_checkpoint.py) still needs a
production writer, and either add the minimal one or document that the
module is superseded. Read before guessing — this repo has two competing
suspend/resume subsystems and picking the wrong one wastes the work.

## Read first, in this order

1. src/bernstein/core/persistence/agent_checkpoint.py:1-25 (module docstring)
   and :171-256 (save_checkpoint, load_checkpoint, find_checkpoint_for_task,
   scan_orphaned_checkpoints) — confirms the read/write surface described
   above.

2. src/bernstein/cli/commands/resume_cmd.py:75-116 (prepare_resume) — the
   only production caller, and it is read-only:
   `find_checkpoint_for_task` at line 108, `is_checkpoint_recoverable` at
   line 110. No `save_checkpoint` import anywhere in this file.

3. src/bernstein/core/tasks/suspension.py:1-40 (module docstring — read the
   whole thing, it explains the Merkle-chained-journal-row design) and
   :878 (`park_task`) — the likely real suspend entrypoint in the *other*
   subsystem. Also read :1008 (`decide_resume`), :1061 (`resume_task`),
   :1366 (`verify_suspension_continuity`) to see how much of #3649's
   continuation-entry ask this module already does for its own checkpoint
   type.

4. `gh pr view 3723 --repo sipyourdrink-ltd/bernstein --json body --jq .body`
   — the merged PR that built the grant-binding mechanism this issue found
   has no production entry point. It does not mention suspension.py at all —
   worth confirming with its author (or the commit history) whether the two
   systems' relationship was considered.

## Current missing shape

`git grep -n "AgentCheckpoint(" origin/main -- 'src/**'` returns exactly 3
hits, all inside agent_checkpoint.py itself, all deserializing JSON — never
constructing a fresh instance for writing. Verify this yourself; if a writer
has been added since this issue was filed, the fix might already be partial
or complete.

## What currently depends on this (not "call sites" in the usual sense —
## nothing calls a writer that doesn't exist; these are the consumers
## waiting on one)

- resume_cmd.py:108, :110 — currently always see `find_checkpoint_for_task`
  return `None` in production, so `is_checkpoint_recoverable` never runs on
  a real checkpoint.
- #3835 (append the continuation entry) has nowhere to fire in production
  until a suspend-time writer exists — its own unit tests are unaffected
  since they construct checkpoints directly, but its end-to-end value is
  zero until this issue resolves.

## Existing helper to reuse

If (a): reuse `save_checkpoint` (agent_checkpoint.py:171) itself as the
write primitive — it already calls `write_atomic_json`
(persistence/atomic_write.py) correctly. Do not hand-roll a second JSON
writer. Compute the four grant fields with `compute_grant_hash`
(agent_checkpoint.py:48), which already exists and is tested.

If (b): mirror how `suspension.py`'s own module docstring documents its
design (read it — it's a good model for a documentation-only PR) rather
than inventing new prose conventions.

## Test matrix

If (a):

| Test | Protects | File |
|---|---|---|
| `test_suspend_writes_checkpoint_with_populated_grant_fields` | the real suspend path produces a checkpoint.json with non-empty role/grant_hash/parent_run_id/chain_head_at_suspend | wherever the suspend call site's own tests live — depends on what (a) turns out to touch, find it before writing a new file |
| `test_resume_after_real_suspend_finds_and_validates_checkpoint` | find_checkpoint_for_task + is_checkpoint_recoverable see a real, non-None checkpoint end to end | same |

If (b): no new test is required for a documentation change; instead confirm
#3833's existing tests (tests/unit/test_grant_bound_checkpoint.py) still
pass unchanged, since a docs-only PR must not touch behavior.

## The trap

Don't assume "no writer" means "build a cron-like heartbeat poller from
scratch." Check whether `suspension.py`'s `park_task` already fires at
exactly the lifecycle moment this issue wants (task suspension) — if so,
the fix may be as small as calling `save_checkpoint` from the same call
site that already calls `record_task_suspension_row`, not inventing a new
lifecycle hook. Conversely, don't assume they're interchangeable just
because both fire "at suspend" — `suspension.py`'s checkpoint concept and
`agent_checkpoint.py`'s `AgentCheckpoint` are different shapes with
different fields; read both fully before concluding either way.

## Decision

(a) vs (b) above. This is the one thing that must be decided, not guessed —
guessing wrong means either wiring a writer that immediately becomes a
second, redundant suspend-tracking system next to `suspension.py`, or
leaving #3834/#3835/#3836's already-reviewed, already-tested logic
permanently unreachable from a real run. State the decision and the
evidence for it in the PR.

## Verify

    git -C . grep -n "AgentCheckpoint(" origin/main -- 'src/**'
    uv run pytest tests/unit/test_agent_checkpoint.py tests/unit/test_grant_bound_checkpoint.py -v
    uv run mypy src/bernstein/core/persistence/agent_checkpoint.py

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-welcomeSuitable for AI agent contributorscoreenhancementNew feature or requestlineageLineage / provenance / supply-chainreplayDeterministic replay and time-travelsecuritySecurity hardeningsize/mup-for-grabsListed on up-for-grabs.net — no commitment, low friction

    Type

    No type

    Projects

    Status
    In Progress

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions