Skip to content

Record the pause capture scope in LocalSnapshotInfo - #812

Merged
Haven Xia (HavenXia) merged 1 commit into
agent-substrate:mainfrom
dberkov:pause-content-scope
Aug 8, 2026
Merged

Record the pause capture scope in LocalSnapshotInfo#812
Haven Xia (HavenXia) merged 1 commit into
agent-substrate:mainfrom
dberkov:pause-content-scope

Conversation

@dberkov

Copy link
Copy Markdown
Collaborator

Part of #791 (PR 2 of the implementation plan).

Adds LocalSnapshotInfo.content_scope and records the template's onPause scope in FinalizePausedStep. A later suspend of a PAUSED actor needs to know what the local pause snapshot actually captured, to validate the requested scope conversion before the actor leaves PAUSED — even if the template's onPause is edited while the actor sits paused. The snapshot's on-node manifest (#810) remains the authoritative record at upload time; this control-plane copy is for early validation only.

  • ateapipb proto: SnapshotContentScope content_scope = 3 on LocalSnapshotInfo + regen
  • FinalizePausedStep: sets ContentScope from SnapshotsConfig.OnPause when recording LocalSnapshotInfo
  • Unset for actors paused before this field existed (consumers fall back to the template's onPause)

Tests:

  • New TestFinalizePausedStep_RecordsContentScope (OnPause Data/Full/unset→Full, through a real store with a worker)
  • TestFinalizePausedStep_WorkerGone now seeds the template the step reads
  • TestPauseActor: expected ContentScope added — which exposed a latent test bug: the custom snapshot_prefix comparer was asymmetric (strings.HasPrefix(y, x) only); go-cmp requires Comparers to be symmetric and probes them with swapped arguments, and the new field shifted the comparison order so the probe finally landed on it. Fixed by matching the prefix in both directions.

Verified end-to-end on a GKE deployment: a paused actor's Valkey record now carries "contentScope": "SNAPSHOT_CONTENT_SCOPE_FULL" in localSnapshotInfo.

go test -race ./cmd/ateapi/..., go vet, gofmt clean. Independent of #810; no dependencies.

🤖 Generated with Claude Code

Add LocalSnapshotInfo.content_scope and record the template's onPause scope
at pause finalization. A later suspend of a PAUSED actor (agent-substrate#791) needs to know
what the local snapshot actually captured to validate scope conversion before
the actor leaves PAUSED, even if the template's onPause is edited while the
actor sits paused; the snapshot's on-node manifest remains the authoritative
record at upload time.

Also fixes a latent asymmetry in TestPauseActor's snapshot_prefix comparer
that go-cmp's symmetry probe started tripping once the new field shifted the
comparison order.

Part of agent-substrate#791
@HavenXia
Haven Xia (HavenXia) merged commit ca81df9 into agent-substrate:main Aug 8, 2026
11 checks passed
Julian Gutierrez Oschmann (juli4n) pushed a commit that referenced this pull request Aug 8, 2026
Part of #791 (PR 3 of the [implementation
plan](#791 (comment))).

Two behavior-preserving refactors needed before suspending a PAUSED
actor, which has no worker assignment:

**Dialer** — extract `dialAteletOnNode` from `DialForWorker`'s tail
(byNode index lookup → single-atelet check → per-pod-UID connection
cache → pinned-mTLS dial) and expose `DialForNode(nodeName)` for actors
whose state is pinned to a node without a worker pod (a PAUSED actor's
local snapshot). A node with no atelet in the informer cache returns the
new sentinel `ErrNoAteletOnNode` — documented as retryable (atelet
restarting / informer lag), distinct from the crash-worthy
`ErrWorkerPodNotFound`. `DialForWorker` behavior is unchanged apart from
error wrapping in the zero-atelets case.

**FinalizeSuspendedStep** — hoist finalization (ActorSnapshot record,
`LatestSnapshot`, the `SUSPENDED` flip, clearing
`InProgressSnapshot`/`WorkerAssignment`/`LocalSnapshotInfo`) out of the
`if assignment != nil` worker-freeing branch so it always runs.
Previously, a SUSPENDING actor with no assignment was silently skipped —
the workflow reported success while the actor stayed SUSPENDING forever.
Unreachable today (`CallAteletSuspendStep`'s prerequisite crashes
nil-assignment actors first), but load-bearing for the paused-origin
suspend, where no worker exists by design.

Tests:
- `TestDialForNode`: no atelet on node → `ErrNoAteletOnNode`; two
atelets → error; happy path asserts pod-UID-pinned credentials and
connection caching.
- `TestFinalizeSuspendedStep_NoAssignment`: SUSPENDING actor with
`LocalSnapshotInfo` and an in-progress URI but no assignment →
SUSPENDED, ActorSnapshot created at the URI, `LatestSnapshot` set,
in-progress fields and `LocalSnapshotInfo` cleared.

`go test -race ./cmd/ateapi/...` (incl. the functional suite covering
the running-path suspend end-to-end), `go vet`, `gofmt` clean.
Independent of #810 and #812.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

2 participants