checkpoint

package
v0.2.49 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: GPL-2.0, GPL-3.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const (
	FileAdded    = chktypes.FileAdded
	FileModified = chktypes.FileModified
	FileDeleted  = chktypes.FileDeleted
)

FileAdded and the following constants re-export the FileStatus values from the types sub-package for backward compatibility.

View Source
const (

	// FileEvents is the name of the per-chat append-only event log file.
	FileEvents = "events.jsonl"
)
View Source
const RestoreStageSuffix = ".vibekit-restore-*"

RestoreStageSuffix is the temp-file pattern used by both CheckoutFile and stageRestoreLocked for two-phase atomic writes. Single constant so cleanup tooling or tests that glob for orphaned staging files can reference it instead of hardcoding the pattern.

Variables

View Source
var ErrBlobCorrupt = errors.New("blob integrity check failed")

ErrBlobCorrupt signals that a blob's content hash does not match its filename (bitrot / truncated write / bad sector). Get fails closed on this so a corrupt blob can't be written over a good working-tree file during a restore.

View Source
var ErrBlobNotFound = errors.New("blob not found")

ErrBlobNotFound signals that a blob was requested but isn't in the store. Distinct from os.ErrNotExist so callers can tell "blob GC'd" apart from "filesystem error reading a blob that should exist".

View Source
var ErrBlobTooLarge = errors.New("blob too large")

ErrBlobTooLarge signals that a blob exceeds the read cap. Distinct from a generic error so the HTTP layer can map it to 413 without string matching.

View Source
var ErrLogCorrupt = errors.New("event log corrupt")

ErrLogCorrupt signals that the event log is in a corrupt or tampered state (e.g. symlink replacement). Distinct from a generic error so callers can classify the failure category.

View Source
var ErrPathEscape = errors.New("path escapes workdir")

ErrPathEscape signals that a workspace-relative path resolves outside the workdir boundary. Distinct from a generic error so callers can classify the failure without string matching.

View Source
var ErrTagNotFound = chktypes.ErrTagNotFound

ErrTagNotFound signals that a requested tag isn't in the event log. Separate from a generic error so the HTTP layer can map it to 404.

View Source
var ErrTransient = errors.New("transient checkpoint error")

ErrTransient signals a retryable I/O failure (disk full, permission denied temporarily, NFS stale handle). Callers can use errors.Is(err, ErrTransient) to distinguish "retry later" from permanent failures (ErrPathEscape, ErrTagNotFound, ErrLogCorrupt).

View Source
var ParseTag = chktypes.ParseTag

ParseTag validates and returns a Tag.

Functions

This section is empty.

Types

type ConflictBroadcaster

type ConflictBroadcaster func(chatID string, payload *ConflictPayload)

ConflictBroadcaster is the hook the Manager calls to fan conflict events out to SSE subscribers. Takes the payload by pointer so the hot path doesn't copy ~90 bytes per call.

type ConflictPayload

type ConflictPayload = chktypes.ConflictPayload

ConflictPayload is re-exported from the types sub-package for backward compatibility within this package.

type FileChange

type FileChange = chktypes.FileChange

FileChange is one entry returned by Diff. Re-exported from the types sub-package for backward compatibility.

type FileStatus

type FileStatus = chktypes.FileStatus

FileStatus is a typed string for diff result statuses. Re-exported from the types sub-package for backward compatibility.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns one chat's checkpoint log and drives all snapshot / restore / diff operations on behalf of that chat. Safe for concurrent use — every entry point serializes on m.mu.

func (*Manager) AdvanceTurn

func (m *Manager) AdvanceTurn(ctx context.Context, messageCount int) error

AdvanceTurn records a fresh user turn boundary in the event log. Called from cmdPrompt right before the agent starts a new turn so the tag allocation gets a clean "N" slot. messageCount is the persisted message count at the moment of the turn boundary.

func (*Manager) CheckoutFile

func (m *Manager) CheckoutFile(ctx context.Context, tag Tag, relPath string) error

CheckoutFile reverts a single file to its content at `tag`. Used by the per-file Undo button on tool-call cards. Does not touch other files, does not truncate the transcript. Two-phase (stage + rename) so a crash mid-write can't leave a corrupt file.

func (*Manager) Cleanup

func (m *Manager) Cleanup(ctx context.Context)

Cleanup removes this chat's event log + parent directory and forgets every cross-chat index entry owned by this chat. Blobs are NOT touched here — the blob GC pass reaps any orphans asynchronously.

Stale-reference semantics: after Cleanup, any caller holding a *Manager reference obtained from a prior Store.get on this chatID will see an empty state on the next method call (the log is gone, so ensureLoaded replays as a fresh chat — no error). Callers that need "was this chat deleted?" semantics should re-fetch through Store.get, not reuse stale Manager references. In practice the hub always goes through Store.get and never holds Manager pointers across Cleanup.

func (*Manager) Conflicts

func (m *Manager) Conflicts(ctx context.Context) ([]ConflictPayload, error)

Conflicts returns every conflict_detected event recorded for this chat in chronological order. O(1) copy from the ring buffer maintained by state.apply — no log re-read.

func (*Manager) Diff

func (m *Manager) Diff(ctx context.Context, from, to Tag) ([]FileChange, error)

Diff returns per-file changes between two tags. Walks the event log for every file touched in (from, to], compares the stored blobs at the two endpoints, and returns a line-delta summary.

func (*Manager) OldestTag

func (m *Manager) OldestTag(ctx context.Context) string

OldestTag is the earliest available tag, or "" if no snapshots exist yet. O(1) via the sorted index in state.

func (*Manager) ReadBlob

func (m *Manager) ReadBlob(ctx context.Context, sha string) ([]byte, error)

ReadBlob returns the content of a blob for this chat. Exposed so the client can fetch "what another chat left on disk" without going through the filesystem. SHA is validated as hex to prevent path traversal; only blobs referenced by this chat's event log are returned — we refuse SHAs we don't recognize so chat A can't probe chat B's private blobs.

func (*Manager) ReferencedBlobs

func (m *Manager) ReferencedBlobs(ctx context.Context) []string

ReferencedBlobs returns a copy of all blob SHAs referenced by this chat's event log. Used by the GC to collect the referenced set from in-memory state instead of re-reading disk. Thread-safe.

func (*Manager) Restore

func (m *Manager) Restore(ctx context.Context, tag Tag) (int, error)

Restore rolls the workspace back to the state captured at `tag`. Only files snapshotted at tags after `tag` are touched — every other file in the workspace is preserved. Returns the message count watermark recorded at `tag` so the caller can truncate the chat transcript in sync.

Journaled two-phase commit:

  1. Stage every target file to a `.vibekit-restore` sibling (phase 1). Nothing destructive yet; any stage failure aborts with a clean workspace.
  2. Append `restore_started` so the log records "we are about to mutate". fsync guarantees this survives a crash.
  3. Phase 2: execute every rename / delete. A crash between renames leaves the workspace half-restored, but the journal tells us what to finish on next load.
  4. Append `restore_committed` on success.

Recovery (in ensureLoaded): if we see a `restore_started` without a matching `restore_committed` on replay, re-run the restore to that tag before accepting any new writes. Crashes during recovery are themselves idempotent — running the commit twice is harmless (renames either succeed or error with "already at target state").

func (*Manager) RestorePreview

func (m *Manager) RestorePreview(ctx context.Context, tag Tag) ([]string, error)

RestorePreview returns the list of files that would be touched by a Restore(tag) call. Used by the client to warn the user before the destructive action so they know whether in-flight editor buffers will be clobbered. Read-only; no log mutations.

NOTE: the preview is a point-in-time snapshot of state at call time. If the agent snapshots another file between preview and Restore, that file will be touched by Restore but will not appear in the preview. Today the client accepts that window; tighter race closure (e.g. passing the expected file set back to Restore and rejecting the call if it changed) is deferred until a concrete user problem motivates it.

func (*Manager) Snapshot

func (m *Manager) Snapshot(ctx context.Context, relPath string, newContent []byte, messageCount int) (Tag, error)

Snapshot captures the state of `relPath` just BEFORE an agent write lands on it AND the content it's about to write. The caller passes `newContent` so we can record the post-write SHA without a second disk read — that SHA feeds the cross-chat index's drift detector.

The write itself is NOT performed here. The caller writes `newContent` to disk after Snapshot returns.

type Ring

type Ring[T any] struct {
	// contains filtered or unexported fields
}

Ring is a generic fixed-size ring buffer. Append is O(1) unconditionally; oldest entries are silently overwritten when the buffer is full. Len() == cap means full.

func NewRing

func NewRing[T any](capacity int) *Ring[T]

NewRing creates a Ring with the given capacity.

func (*Ring[T]) Append

func (r *Ring[T]) Append(v T)

Append adds a value to the ring. O(1).

func (*Ring[T]) Len

func (r *Ring[T]) Len() int

Len returns the number of valid entries.

func (*Ring[T]) Slice

func (r *Ring[T]) Slice() []T

Slice returns a copy of all entries in chronological order.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the hub's entry point. One Store per vibekit process; internally it hands out one Manager per chatID and keeps the shared blob store + cross-chat index alive.

The 5-minute blob age gate (blobGCMinAge) provides the primary safety guarantee against removing in-flight blobs: a blob written now cannot be reaped for at least 5 minutes, which is far longer than the event append takes. No per-sweep lock is needed.

func NewStore

func NewStore(configDir, workDir string, onConf ConflictBroadcaster) *Store

NewStore builds a Store rooted at configDir with the given work tree. All chats share the same work tree, the same blob store, and the same cross-chat index. `onConf` is called every time a Snapshot detects drift; pass nil to disable broadcasting (tests). The cross-chat index starts empty and is populated lazily as Managers are accessed — each Manager's ensureLoaded applies its events to the shared index after replay, halving startup I/O compared to the prior eager-rebuild approach.

func (*Store) AdvanceTurn

func (s *Store) AdvanceTurn(ctx context.Context, chatID api.ChatID, messageCount int)

AdvanceTurn delegates to Manager.AdvanceTurn (pass-through — logic lives in Manager). Errors are logged but not returned because the prompt path shouldn't stall on a checkpoint failure — the checkpoint exists to serve the user, not gate them.

func (*Store) CheckoutFile

func (s *Store) CheckoutFile(ctx context.Context, chatID api.ChatID, tag Tag, relPath string) error

CheckoutFile reverts a single file to its content at `tag` (pass-through — logic lives in Manager).

func (*Store) Cleanup

func (s *Store) Cleanup(ctx context.Context, chatID api.ChatID)

Cleanup removes chatID's event log + parent dir and evicts the manager from cache. Index entries owned by the chat are forgotten inside Manager.Cleanup. Blobs survive — the GC pass reaps orphans.

Serializes with runGCOnce via gcLock: a chat delete walks the same event logs runBlobGC collects from, so wiping a chat mid- sweep could drop its blob references from the referenced-set and let the sweep reap blobs that were live until microseconds ago. Both Cleanup and Snapshot take gcLock.RLock so neither can interleave with the GC's exclusive Lock() sweep — Cleanup mutates the referenced-blob set from the GC's perspective just as Snapshot does, only in the opposite direction.

func (*Store) Conflicts

func (s *Store) Conflicts(ctx context.Context, chatID api.ChatID) ([]ConflictPayload, error)

Conflicts returns all conflict_detected events for a chat (pass-through — logic lives in Manager).

func (*Store) Diff

func (s *Store) Diff(ctx context.Context, chatID api.ChatID, from, to Tag) ([]FileChange, error)

Diff returns per-file changes between two tags (pass-through — logic lives in Manager).

func (*Store) OldestTag

func (s *Store) OldestTag(ctx context.Context, chatID api.ChatID) Tag

OldestTag returns the earliest available tag for chatID, or "" (pass-through — logic lives in Manager).

func (*Store) OwnerOf added in v0.2.8

func (s *Store) OwnerOf(ctx context.Context, relPath string) (api.ChatID, bool)

OwnerOf returns the chat whose agent most recently wrote relPath — the owner of the path's checkpoint lineage — or ok=false when no chat tracks it. The editor-save capture uses this to route a manual save into the one timeline whose restores and undos consult the path. The first call warms the cross-chat index from disk: managers load lazily, so a cold process would otherwise report "no owner" for everything until chats happened to be accessed.

func (*Store) ReadBlob

func (s *Store) ReadBlob(ctx context.Context, chatID api.ChatID, sha string) ([]byte, error)

ReadBlob returns blob content for a chat-scoped SHA (pass-through — logic lives in Manager).

func (*Store) Restore

func (s *Store) Restore(ctx context.Context, chatID api.ChatID, tag Tag) (int, error)

Restore rolls the workspace back to `tag` (pass-through — logic lives in Manager).

func (*Store) RestorePreview

func (s *Store) RestorePreview(ctx context.Context, chatID api.ChatID, tag Tag) ([]string, error)

RestorePreview returns paths a Restore would mutate (pass-through — logic lives in Manager).

func (*Store) Snapshot

func (s *Store) Snapshot(ctx context.Context, chatID api.ChatID, relPath string, newContent []byte, messageCount int) (Tag, error)

Snapshot captures the pre-write content of relPath (pass-through — logic lives in Manager). The gcLock.RLock coordination lives inside Manager.Snapshot itself so this wrapper is a thin pass-through.

func (*Store) StartBackgroundTasks

func (s *Store) StartBackgroundTasks(ctx context.Context)

StartBackgroundTasks kicks off the periodic blob GC. Runs one immediate sweep, then every blobGCInterval. Idempotent — a second call while a gcLoop is already running is a no-op, so defensive double-calls from lifecycle refactors don't fork a second ticker and double the scheduled work. Split out from NewStore so unit tests can construct a Store without accidentally starting a goroutine. The context is used for cancellation of in-flight GC I/O on shutdown.

func (*Store) Stop

func (s *Store) Stop()

Stop halts the background GC goroutine and waits for it to finish. Called from Hub.Shutdown. Safe to call even if StartBackgroundTasks was never invoked.

type Tag

type Tag = chktypes.Tag

Tag is a validated checkpoint tag. Re-exported from the types sub-package for backward compatibility within this package.

Directories

Path Synopsis
Package gc implements the blob garbage collector for the checkpoint subsystem.
Package gc implements the blob garbage collector for the checkpoint subsystem.
Package types defines checkpoint domain types shared between the checkpoint implementation and its consumers (api, hub).
Package types defines checkpoint domain types shared between the checkpoint implementation and its consumers (api, hub).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL