Documentation
¶
Overview ¶
Package checkpoint defines the provider-neutral suspend/resume primitive for DriftlessAF agents: a serializable Envelope that captures everything needed to rebuild a paused conversation, a Suspension error value that travels out of an executor's Execute loop the same way workqueue's requeue errors do, and a Store contract (with compare-and-swap Token semantics) for durably parking exactly one envelope per {identity}/{key}.
The package is a pure primitive: it knows nothing about human questions, the workqueue, or any specific LLM provider. The three provider SDK payloads are carried opaquely in Envelope.ProviderState as json.RawMessage so a microVM checkpointer or a GCS-backed store can reuse the exact same envelope shape.
Overview ¶
The core types compose into a suspend/park/wake/resume lifecycle:
- Envelope: versioned, serializable capture of a paused conversation — provider identity, config digest, turn budget, pending tool calls, and the raw provider request payload.
- Suspension: an error-shaped carrier for an Envelope. Executors return &Suspension{...} like any other error and callers extract it with AsSuspension, so no executor interface ever grows a method for pausing.
- Store: the durable home for parked envelopes, with Load returning a CAS Token that Delete uses as a claim-once primitive (ErrTokenMismatch signals a lost race).
- FrameAnswer / FramedAnswers: wrap human answers in distinctive delimiters, substitute a placeholder for empty answers, and cap length on a UTF-8 boundary before they are injected as tool results.
Fail-Closed Validation ¶
Validation happens at both ends of the pause. Envelope.Validate rejects an unpairable or unreplayable envelope at suspend time — including one with no remaining turn budget, which could never pass the resume gate — before a checkpoint is persisted and a human spends time answering. ValidateForResume gates the wake side: version, provider, model, and config digest (see DigestJSON) must all match the live executor — the digest is required, so an empty digest on either side fails closed rather than vacuously matching — turn budget must remain, and any park-time Deadline must not have passed; drift surfaces as ErrConfigDrift so callers rebuild from scratch instead of resuming against stale state.
Usage ¶
An executor suspends by returning a Suspension from its Execute loop:
return &checkpoint.Suspension{...} // typically via NewAskHumanSuspension
The reconciler at the top extracts it, parks the envelope, and requeues:
if s, ok := checkpoint.AsSuspension(err); ok {
if err := store.Save(ctx, key, &s.Envelope); err != nil {
return err
}
// ask the human s.Question, then requeue to wake later.
}
On wake, the resumer claims the envelope with the CAS token and replays it:
env, tok, ok, err := store.Load(ctx, key) // validate with checkpoint.ValidateForResume, frame the human answer // with checkpoint.FramedAnswers, then claim via store.Delete(ctx, key, tok).
Store implementations live in the memstore (in-memory), jsonlstore (append-only local file), and future GCS-backed subpackages; all are held to the same contract by the storetest conformance suite.
Example (StoreRoundTrip) ¶
Example_storeRoundTrip demonstrates the park/claim half of the lifecycle against the in-memory Store: Save parks the envelope, Load returns it with a CAS token, and Delete with that token claims it exactly once.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"chainguard.dev/driftlessaf/agents/checkpoint"
"chainguard.dev/driftlessaf/agents/checkpoint/memstore"
)
func main() {
ctx := context.Background()
store := memstore.New()
env := &checkpoint.Envelope{
Version: checkpoint.EnvelopeVersion,
Provider: checkpoint.ProviderAnthropic,
ReconcilerKey: "org/repo#42",
RunID: "run-1",
ProviderState: json.RawMessage(`{"model":"claude-fable-5"}`),
}
if err := store.Save(ctx, env.ReconcilerKey, env); err != nil {
fmt.Println("error:", err)
return
}
loaded, tok, ok, err := store.Load(ctx, env.ReconcilerKey)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("parked:", ok, loaded.RunID)
// The first Delete with the token wins the claim; a second attempt with
// the now-stale token loses the race.
fmt.Println("claimed:", store.Delete(ctx, env.ReconcilerKey, tok))
fmt.Println("second claim lost:",
errors.Is(store.Delete(ctx, env.ReconcilerKey, tok), checkpoint.ErrTokenMismatch))
}
Output: parked: true run-1 claimed: <nil> second claim lost: true
Index ¶
- Constants
- Variables
- func DigestJSON(v any) (string, error)
- func FrameAnswer(s string, maxBytes int) string
- func QuestionFromPending(calls []PendingToolCall) string
- func ValidateForResume(env Envelope, provider, model, liveDigest string, now time.Time) error
- type Envelope
- type FramedAnswer
- type PendingToolCall
- type Store
- type Suspension
- type Token
Examples ¶
Constants ¶
const ( ProviderAnthropic = "anthropic" ProviderOpenAI = "openai" ProviderGoogle = "google" )
Provider identifiers stamped into Envelope.Provider by the executors and matched fail-closed on resume. Wakers that route on provider (a dispatcher picking which executor to rebuild) share these instead of re-typing the strings.
const DefaultAnswerMaxBytes = 16384
DefaultAnswerMaxBytes caps a framed human answer on the resume path. One policy value for the whole lifecycle: FrameAnswer applies it inside the executors' Resume, which own framing (callers pass answers raw).
const EnvelopeVersion = 1
EnvelopeVersion is the current schema version stamped into new envelopes. Bump it when a backwards-incompatible field change lands so a Store can refuse or migrate stale objects.
const ReasonAwaitingHumanAnswer = "awaiting human answer"
ReasonAwaitingHumanAnswer is the Envelope.Reason for a suspension triggered by a held-out ask-human tool call.
const StatusAwaitingHumanAnswer = "awaiting_human_answer"
StatusAwaitingHumanAnswer is the status value in the synthetic tool result recorded for the suspend call itself (the call is intercepted, never dispatched, so the transcript needs a placeholder result to stay paired).
Variables ¶
var ErrConfigDrift = errors.New("checkpoint: config drift; rebuild from scratch")
ErrConfigDrift signals that a suspended envelope cannot be safely resumed because the live executor configuration no longer matches the one that produced it (a Provider, Model, or ConfigDigest mismatch on wake). Callers treat it as "rebuild from scratch" rather than resuming against stale state: a resumer returns it wrapped, and a waker (PR9) extracts it with errors.Is to fall back to a fresh run and delete the stale checkpoint.
var ErrTokenMismatch = errors.New("checkpoint: CAS token mismatch")
ErrTokenMismatch is returned by Store.Delete when the supplied Token no longer matches the stored object's current CAS handle: the object was re-saved, already deleted, or never existed. It is the signal a concurrent waker uses to detect it lost a claim race.
Functions ¶
func DigestJSON ¶
DigestJSON returns a stable "sha256:<hex>" digest of v's JSON encoding, for stamping Envelope.ConfigDigest. On wake, a resume compares the stored digest against a freshly computed one over the current configuration; a mismatch means the agent's config drifted under the pause and the run must be rebuilt from scratch rather than resumed against stale state. Include every input whose drift must block a resume — model parameters, tool definitions, and the provider SDK version — so all drift funnels into this single enforced gate instead of a family of parallel field checks.
The digest is only as stable as encoding/json's field ordering (struct field order, sorted map keys), which is deterministic for a fixed Go type. Feed it a struct, not an arbitrary map with volatile key insertion, for a meaningful comparison across process restarts.
Example ¶
ExampleDigestJSON demonstrates stamping and re-deriving a config digest: the digest is stable for identical configuration and changes when any resume-relevant field drifts.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/checkpoint"
)
func main() {
type config struct {
Model string
MaxTurns int
}
parked, err := checkpoint.DigestJSON(config{Model: "claude-fable-5", MaxTurns: 12})
if err != nil {
fmt.Println("error:", err)
return
}
live, err := checkpoint.DigestJSON(config{Model: "claude-fable-5", MaxTurns: 12})
if err != nil {
fmt.Println("error:", err)
return
}
drifted, err := checkpoint.DigestJSON(config{Model: "claude-fable-5", MaxTurns: 6})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("stable:", parked == live)
fmt.Println("drift detected:", parked != drifted)
}
Output: stable: true drift detected: true
func FrameAnswer ¶
FrameAnswer prepares a raw human answer for injection as a tool_result: it strips any embedded delimiter strings, substitutes a placeholder for an empty answer, caps the body to maxBytes on a UTF-8 boundary (maxBytes <= 0 disables the cap), and wraps the result in distinctive delimiters. Framing is what keeps a paused agent from treating arbitrary human input as trusted instructions, and the empty-substitution is what keeps the resumed provider request from carrying an empty tool_result.
Delimiter stripping is what makes the frame a boundary: without it, an answer containing the literal closing delimiter would end the frame early and smuggle the rest of the answer outside it, where the model reads it as top-level instructions. Stripping loops until no occurrence remains, so a nested payload cannot reassemble a delimiter out of the removed pieces.
Example ¶
ExampleFrameAnswer demonstrates framing a raw human answer for injection as a tool result: delimited, and never empty on the wire.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/checkpoint"
)
func main() {
fmt.Println(checkpoint.FrameAnswer("Yes, go ahead.", 0))
fmt.Println(checkpoint.FrameAnswer(" ", 0))
}
Output: <<<BEGIN HUMAN ANSWER>>> Yes, go ahead. <<<END HUMAN ANSWER>>> <<<BEGIN HUMAN ANSWER>>> (the human did not provide an answer) <<<END HUMAN ANSWER>>>
func QuestionFromPending ¶
func QuestionFromPending(calls []PendingToolCall) string
QuestionFromPending extracts the human-facing question text from the first pending tool call whose input carries a "question" string property (the ask-human tool convention, see questionInputKey). It returns "" when no pending call carries one, so callers can fall back to their own prompt.
func ValidateForResume ¶
ValidateForResume is the fail-closed gate every executor Resume runs before replaying a parked envelope: the schema version, provider, model, and config digest must all match the live executor, turn budget must remain, and any park-time Deadline must not have passed at now. The digest is required on both sides — an empty envelope or live digest is rejected rather than vacuously matching another empty string, so the gate cannot be silently disabled by a caller that never stamped one. Mismatches return ErrConfigDrift (wrapped) so the caller rebuilds from scratch rather than resuming against stale state; an exhausted budget or expired deadline is a plain error — there is nothing valid to rebuild toward.
Example ¶
ExampleValidateForResume demonstrates the fail-closed gate on the wake side: a parked envelope only resumes against the exact executor configuration that produced it (any drift surfaces as ErrConfigDrift) and only before its park-time Deadline.
package main
import (
"errors"
"fmt"
"time"
"chainguard.dev/driftlessaf/agents/checkpoint"
)
func main() {
env := checkpoint.Envelope{
Version: checkpoint.EnvelopeVersion,
Provider: checkpoint.ProviderAnthropic,
Model: "claude-fable-5",
ConfigDigest: "sha256:cfg",
RemainingTurns: 4,
Deadline: time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC),
}
// The live executor matches the parked envelope: resume may proceed.
fmt.Println("match:", checkpoint.ValidateForResume(env,
checkpoint.ProviderAnthropic, "claude-fable-5", "sha256:cfg",
time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)))
// The model drifted under the pause: rebuild from scratch instead.
err := checkpoint.ValidateForResume(env,
checkpoint.ProviderAnthropic, "claude-fable-6", "sha256:cfg",
time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC))
fmt.Println("drift:", errors.Is(err, checkpoint.ErrConfigDrift))
// The wake arrived after the envelope's deadline: fail closed.
err = checkpoint.ValidateForResume(env,
checkpoint.ProviderAnthropic, "claude-fable-5", "sha256:cfg",
time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC))
fmt.Println("expired:", err != nil)
}
Output: match: <nil> drift: true expired: true
Types ¶
type Envelope ¶
type Envelope struct {
// Version is the envelope schema version (see EnvelopeVersion).
Version int `json:"version"`
// Provider identifies the executor backend ("anthropic", "openai",
// "google") that produced ProviderState.
Provider string `json:"provider"`
// Model is the model id in effect when the conversation suspended.
Model string `json:"model"`
// SDKVersion records the provider SDK version in effect at park time, for
// diagnostics and telemetry. It is not compared on wake: SDK drift is
// enforced through ConfigDigest — executors include the SDK version in the
// digested configuration so a drifted binary trips the one fail-closed
// gate rather than a second parallel check.
SDKVersion string `json:"sdk_version,omitempty"`
// ConfigDigest is DigestJSON over the resume-relevant configuration,
// including the provider SDK version. A mismatch on wake signals "rebuild
// from scratch" rather than resume. The digest is required: Validate
// rejects an envelope without one at park time and ValidateForResume
// rejects an empty digest on either side, so the drift gate can never be
// vacuously satisfied.
ConfigDigest string `json:"config_digest,omitempty"`
// ReconcilerKey is the workqueue key the suspended run belongs to; combined
// with the executor identity it names the single parked object.
ReconcilerKey string `json:"reconciler_key"`
// RunID disambiguates successive runs of the same key. It is a field of the
// single per-{identity}/{key} object, NOT part of the storage path, so
// Load(key) is never ambiguous.
RunID string `json:"run_id"`
// Turn is the 0-based conversation turn at which the suspension fired.
Turn int `json:"turn"`
// RemainingTurns is the overall turn budget still available across resumes,
// so a conversation cannot loop forever by suspending and waking. It must
// be positive to park: Validate rejects an exhausted budget at suspend
// time, mirroring the ValidateForResume turn gate, so an envelope that
// could never wake is never persisted.
RemainingTurns int `json:"remaining_turns"`
// Reason is a short machine-readable reason for the suspension.
Reason string `json:"reason,omitempty"`
// PendingToolCalls are the unanswered held-out ask-human calls from the
// suspended turn (usually one; more only when the turn issued several).
// Dispatched siblings never appear here — their real results were already
// in ProviderState when the turn quiesced (see PendingToolCall).
PendingToolCalls []PendingToolCall `json:"pending_tool_calls,omitempty"`
// ProviderState is the full provider request payload (e.g. a serialized
// anthropic.MessageNewParams) carried verbatim as raw JSON.
//
// The envelope's neutrality stops at this field: the SCHEMA is one format
// for every executor, but the conversation inside ProviderState is bound
// to the exact Provider and Model that produced it, and ValidateForResume
// fails closed on any mismatch (including a model version bump). This is
// deliberate — provider transcripts are not losslessly translatable:
// Anthropic thinking blocks carry model-bound signatures, Gemini thinking
// mode requires the thoughtSignature history replayed verbatim, and tool
// call identifier schemes differ per provider. A cross-model "resume" that
// silently transplanted a transcript would replay unverifiable state.
//
// TODO(DEV-2247): support continuing on a different model. One model's
// conversation can never be handed to another model directly — it will not
// verify or replay. The workable approach: start a brand-new run on the
// target model and feed it the parked conversation as plain text, together
// with the pending question and the human's answer. Everything needed for
// that already lives in ProviderState, so no envelope schema change is
// required.
ProviderState json.RawMessage `json:"provider_state,omitempty"`
// LoopState is executor-loop bookkeeping (turn index, cache-tail state, ...)
// carried opaquely so each executor owns its own shape.
LoopState json.RawMessage `json:"loop_state,omitempty"`
// StateRef optionally points at externally stored large state (e.g. a GCS
// object) when the payload is too big to inline.
StateRef string `json:"state_ref,omitempty"`
// TraceID is the originating trace, so the resumed run can link back to it.
TraceID string `json:"trace_id,omitempty"`
// Deadline is the wall-clock time after which the envelope must not be
// resumed — ValidateForResume rejects an envelope whose Deadline has
// passed (fail-closed on wake). Zero means no deadline.
Deadline time.Time `json:"deadline,omitempty"`
}
Envelope is the provider-neutral, serializable capture of a suspended agent conversation. It carries enough state to reconstruct the request on resume without holding a live process across the wait.
func (*Envelope) Clone ¶
Clone returns a deep copy of the envelope so a Store can hand back a value that is safe against later caller mutation (and vice versa). The raw-JSON and slice fields are copied; nil stays nil to preserve byte-for-byte equality.
func (*Envelope) Validate ¶
Validate reports whether the envelope is complete enough to park: an unpairable, unreplayable, or unverifiable envelope must fail at suspend time — before a checkpoint is persisted and a human spends time answering — not at resume. That includes an exhausted turn budget: ValidateForResume rejects RemainingTurns <= 0, so an envelope parked without budget could never wake, and the two gates must agree.
type FramedAnswer ¶
type FramedAnswer struct {
// ID is the pending tool call's provider identifier.
ID string
// Name is the pending tool call's tool name.
Name string
// Text is the framed answer body (delimited, capped, empty-substituted).
Text string
}
FramedAnswer is a human answer framed for injection, paired to the pending tool call it answers. The executor maps it into its provider message shape.
func FramedAnswers ¶
func FramedAnswers(pending []PendingToolCall, answers map[string]string, maxBytes int) ([]FramedAnswer, error)
FramedAnswers frames one answer per pending tool call, in envelope order. A pending call with no supplied answer gets the explicit empty-answer placeholder rather than being skipped, so no provider request ever carries an unanswered tool call. maxBytes <= 0 applies DefaultAnswerMaxBytes.
Pairing an answer to every pending call cannot fabricate a result for a privileged tool: only held-out ask-human calls can be pending — dispatched siblings quiesce to real transcript results before park (see PendingToolCall) — and FrameAnswer delimits every body as quoted human text, so the model never reads it as native output of the named tool.
Example ¶
ExampleFramedAnswers demonstrates answering every pending tool call from an envelope: a call with no supplied answer gets the explicit placeholder so no provider request ever carries an unanswered tool call.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/checkpoint"
)
func main() {
pending := []checkpoint.PendingToolCall{
{ID: "toolu_01", Name: "ask_human"},
{ID: "toolu_02", Name: "sibling_tool"},
}
framed, err := checkpoint.FramedAnswers(pending, map[string]string{
"toolu_01": "Ship it.",
}, 0)
if err != nil {
fmt.Println("error:", err)
return
}
for _, fa := range framed {
fmt.Printf("%s (%s):\n%s\n", fa.ID, fa.Name, fa.Text)
}
}
Output: toolu_01 (ask_human): <<<BEGIN HUMAN ANSWER>>> Ship it. <<<END HUMAN ANSWER>>> toolu_02 (sibling_tool): <<<BEGIN HUMAN ANSWER>>> (the human did not provide an answer) <<<END HUMAN ANSWER>>>
type PendingToolCall ¶
type PendingToolCall struct {
// ID is the provider-assigned tool call identifier (anthropic tool_use id,
// openai tool_call id, genai FunctionCall.ID). Persisted, never re-derived.
ID string `json:"id"`
// Name is the tool name, retained for diagnostics/telemetry only.
Name string `json:"name"`
// InputJSON is the raw tool input as the model emitted it.
InputJSON json.RawMessage `json:"input_json,omitempty"`
}
PendingToolCall records a tool call that was issued by the model but whose result has not yet been produced. Because suspension fires post-quiesce — every dispatched sibling handler in the turn has finished and its real result is already in ProviderState's transcript — the only calls that can be pending are the held-out ask-human call(s) from the suspended turn. Executors MUST NOT park a dispatched tool's call as pending: on resume every pending call is paired with framed human-answer text (see FramedAnswers), which would fabricate a result for a tool that never ran. The ID is the provider's own tool_use / function-call identifier and MUST be persisted verbatim: resume pairs the answer tool_result back to this exact ID rather than re-deriving it from Name, which would break as soon as a turn issues two calls to one tool.
type Store ¶
type Store interface {
// Save durably writes env under key, overwriting any existing envelope and
// advancing its CAS generation. Save is unconditional: claim-once ordering
// is expressed with Load + Delete, not with Save.
Save(ctx context.Context, key string, env *Envelope) error
// Load returns the envelope stored under key together with its current CAS
// Token. The bool is false (with a nil envelope, zero Token, and nil error)
// when no envelope exists for key.
Load(ctx context.Context, key string) (*Envelope, Token, bool, error)
// Delete removes the envelope under key only if tok still matches the
// stored object's current generation, returning ErrTokenMismatch otherwise
// (including when the key is already gone). This is the claim primitive:
// exactly one caller holding a given Token wins the delete.
Delete(ctx context.Context, key string, tok Token) error
}
Store is the durable home for suspended envelopes. Implementations park at most one envelope per key (one pending suspension per {identity}/{key}); the RunID lives inside the Envelope rather than in the key so Load is never ambiguous.
The contract is deliberately small so both a local dev store and an AEAD-sealed GCS store satisfy it, and so PR9's orchestration can build claim-once wake semantics purely from Load + Delete CAS.
type Suspension ¶
type Suspension struct {
// Envelope is the serializable capture of the paused conversation.
Envelope
// Question is an optional human-facing prompt describing what the agent is
// waiting on. The question/answer lifecycle itself lives above this package
// (checkpoint stays a pure primitive); this field is only a convenience
// carrier so a Suspension can round-trip the prompt text.
Question string `json:"question,omitempty"`
}
Suspension is the error value an executor returns when a conversation pauses mid-run to be resumed later. It carries the full Envelope needed to rebuild the request, plus an optional human-facing Question.
Modeled on workqueue's requeue-error idiom: the executor returns a Suspension like any other error, every metaagent/bot wrapper propagates it untouched (early-return on err != nil), and the reconciler at the top extracts it with AsSuspension. Because it travels as an ordinary error, no exported executor Interface ever has to grow a method to express "I paused".
func AsSuspension ¶
func AsSuspension(err error) (*Suspension, bool)
AsSuspension reports whether err is (or wraps) a *Suspension and, if so, returns it. It mirrors workqueue.GetRequeueOptions' errors.As-based extraction so a Suspension can surface through arbitrarily wrapped error chains.
func NewAskHumanSuspension ¶
func NewAskHumanSuspension(provider, model, configDigest string, turn, maxTurns int, call PendingToolCall, providerState, loopState json.RawMessage, traceID string) *Suspension
NewAskHumanSuspension assembles the Suspension an executor returns when the model calls its held-out ask-human tool: it stamps the schema version, clamps the remaining-turns budget (turn is 0-based, so turn+1 turns are consumed), records the suspend call as the sole pending tool call, and derives the human-facing Question from the call's input. Executors supply only the provider-typed pieces: the marshaled ProviderState/LoopState and their own config digest. A suspension fired on the final turn yields RemainingTurns 0; Validate rejects that envelope at park time (there is no budget left to resume into), so the run fails before a human is asked.
Example ¶
ExampleNewAskHumanSuspension demonstrates the suspend half of the lifecycle: an executor assembles a Suspension when the model calls its held-out ask-human tool, returns it as an ordinary error, and the reconciler at the top extracts it with AsSuspension.
package main
import (
"encoding/json"
"fmt"
"chainguard.dev/driftlessaf/agents/checkpoint"
)
func main() {
s := checkpoint.NewAskHumanSuspension(
checkpoint.ProviderAnthropic, "claude-fable-5", "sha256:cfg",
3, 12,
checkpoint.PendingToolCall{
ID: "toolu_01ABC",
Name: "ask_human",
InputJSON: json.RawMessage(`{"question":"Should I force-push?"}`),
},
json.RawMessage(`{"model":"claude-fable-5","max_tokens":1024}`),
json.RawMessage(`{"turn":3}`),
"trace-abc",
)
// The envelope is validated before it is parked, so an unreplayable
// checkpoint fails at suspend time rather than at resume.
fmt.Println("parkable:", s.Validate())
// The Suspension travels out of the executor as an ordinary error, through
// any number of fmt.Errorf wrappers...
err := fmt.Errorf("executing turn 3: %w", error(s))
// ...and the caller at the top extracts it.
got, ok := checkpoint.AsSuspension(err)
fmt.Println("suspended:", ok)
fmt.Println("question:", got.Question)
fmt.Println("remaining turns:", got.RemainingTurns)
}
Output: parkable: <nil> suspended: true question: Should I force-push? remaining turns: 8
func (*Suspension) Error ¶
func (s *Suspension) Error() string
Error implements the error interface. The pointer receiver is deliberate: callers must return &Suspension{...} so AsSuspension can extract it via errors.As, mirroring workqueue's *requeueError.
type Token ¶
type Token struct {
Generation int64 `json:"generation"`
}
Token is an opaque compare-and-swap handle minted by a Store on Load. A caller passes the same Token back to Delete to claim-and-remove an envelope atomically; if the object changed in between, Delete fails with ErrTokenMismatch. Generation is the store-specific version counter (a GCS object generation, or a local monotonic counter). The zero Token is invalid.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package jsonlstore provides an append-only, JSONL-file checkpoint.Store for local development.
|
Package jsonlstore provides an append-only, JSONL-file checkpoint.Store for local development. |
|
Package memstore provides an in-memory checkpoint.Store for tests and single-process demos.
|
Package memstore provides an in-memory checkpoint.Store for tests and single-process demos. |
|
Package storetest provides a reusable conformance suite for checkpoint.Store implementations, in the style of workqueue/conformance.
|
Package storetest provides a reusable conformance suite for checkpoint.Store implementations, in the style of workqueue/conformance. |