state

package
v1.108.0 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Index

Constants

View Source
const TrailerKey = "Chainloop-Trace-Sessions"

TrailerKey is the git-style trailer the commit-msg hook writes when AI sessions are attributed to staged files. The key is matched case-sensitively per git trailer convention.

Variables

This section is empty.

Functions

func FindRawSessionFile

func FindRawSessionFile(rawDir, sessionID string) (string, error)

FindRawSessionFile returns the path to the raw session file for sessionID within rawDir, or an error wrapping os.ErrNotExist when missing.

func NonGitDir

func NonGitDir(dir string) (string, error)

NonGitDir returns the out-of-tree directory that parents trace state for a working directory with no git repository, under <user cache>/chainloop/trace/.

The path is derived from dir alone so that hook subprocesses recompute it instead of inheriting it: a hook is a grandchild of `chainloop trace run` (run -> agent -> hook), and an agent that rewrites the environment would otherwise send the hook to a different directory where it would silently record nothing. The user cache dir is $HOME-derived, which survives that far better than $TMPDIR, and it keeps live session state away from /tmp reapers.

func NowTimestamp

func NowTimestamp() string

NowTimestamp returns the current UTC time in RFC3339 format.

func ParseSessionIDsFromTrailer

func ParseSessionIDsFromTrailer(message string) []string

ParseSessionIDsFromTrailer scans a commit message for `Chainloop-Trace-Sessions:` trailer lines and returns the sorted, deduped list of session IDs declared in the message. Lines must start with the trailer key (no leading whitespace) to match git's trailer parsing rules. Empty values and blank entries between commas are ignored.

func RawSessionPath

func RawSessionPath(rawDir, sessionID string) string

RawSessionPath returns the path a session's raw transcript is copied to inside rawDir. Readers and writers must both name the file through this helper: the sanitized ID it applies is not recoverable from the raw one.

func RawSubagentDir

func RawSubagentDir(rawDir, sessionID string) string

RawSubagentDir returns the directory a session's copied subagent transcripts live in inside rawDir. Same naming contract as RawSessionPath.

func SanitizeID

func SanitizeID(id string) string

SanitizeID strips path separators and traversal components from an identifier before using it in filesystem paths. Exported so providers in other packages compute the same on-disk path that the writers in this package emit; otherwise an ID containing "/" or ".." would write to one filename and be read from another.

A rewritten ID also gets a digest suffix, since distinct IDs ("a/x" and "a_x") would otherwise collapse onto one filename and merge two sessions' state. Separating the rewritten set is all that is needed: agent-assigned IDs are UUID-shaped and pass through untouched.

Returning an already-safe ID verbatim makes this idempotent, which callers depend on — GCOrphans compares filename-derived keys against sanitized ones, and LoadAllAILineAttributions feeds a filename straight back in. Always appending the digest would double-suffix those keys and match nothing.

func ValidSessionID

func ValidSessionID(id string) bool

ValidSessionID reports whether an agent-assigned session ID can safely name a file. IDs reach us from agent hook payloads and from commit-message trailers, and both are used to read the agent's own transcript directory, so anything that is not a single path element could escape it. Real IDs are UUID- or slug-shaped, so this rejects nothing legitimate.

Writers additionally pass IDs through SanitizeID, which keeps a rejected ID from escaping our own state directory even if a caller skips this check.

Types

type AILineAttribution

type AILineAttribution struct {
	SessionID string                                 `json:"session_id"`
	Files     map[string][]aicodingsession.LineRange `json:"files"`
}

AILineAttribution tracks which line ranges in which files a session modified.

type CommitRecord

type CommitRecord struct {
	SHA        string   `json:"sha"`
	Message    string   `json:"message"`
	SessionIDs []string `json:"session_ids,omitempty"`
	Timestamp  string   `json:"timestamp"`
	// Tracked marks the record as already attested by a previous successful
	// trace push. The pre-push hook uses it to skip pushing a redundant
	// attestation when no new AI-assisted commits have been recorded since
	// the last push (e.g. a bare `git push` with no new commits, or a
	// `git tag && git push --tags` over already-pushed commits).
	Tracked bool `json:"tracked,omitempty"`
}

CommitRecord holds metadata about a commit captured by the post-commit hook.

type SessionRecord

type SessionRecord struct {
	// SessionID is the agent-assigned identifier for this session.
	SessionID string `json:"session_id"`
	// Provider identifies the agent that produced this session (e.g. "claude-code", "cursor").
	// Empty on records written before multi-provider support; consumers should treat that as claude-code.
	Provider string `json:"provider,omitempty"`
	// AgentVersion is the agent runtime version reported at session-start
	// (e.g., Cursor's cursor_version). Empty for providers whose hook payload
	// does not carry the version. Recorded once on the first hook only.
	AgentVersion string `json:"agent_version,omitempty"`
	// Model is the model identifier reported at session-start (e.g.,
	// Cursor's "model" field). Empty for providers whose hook payload does
	// not carry the model. Recorded once on the first hook only.
	Model string `json:"model,omitempty"`
	// Active reports whether the session is ongoing at the time of record write.
	Active bool `json:"active"`
	// StartedAt is the RFC3339 timestamp of when tracking began for this session.
	StartedAt string `json:"started_at"`
}

SessionRecord holds metadata about an AI coding agent session.

type Store

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

Store reads and writes the chainloop-trace state under a single parent directory: the .git directory inside a repository, or the out-of-tree directory `chainloop trace run` picks outside one. Which of the two it is decides whether git hooks can be installed and whether the directory itself is ours to remove, so the Store carries that rather than leaving callers to thread a second copy of the same path alongside it.

func Locate

func Locate() (store *Store, root string, err error)

Locate returns a Store over the directory that parents chainloop-trace state, plus the working-tree root. Inside a git repository those are the .git directory and the repo root; outside one, they are the NonGitDir of the nearest ancestor of cwd with an active `chainloop trace run`, and that ancestor. The returned Store reports which of the two it is via IsGit.

Constructing the Store here is deliberate: callers never hold a bare state path they could confuse with the repo root returned alongside it.

The non-git branch probes the run-active sentinel rather than the mere existence of a state directory, so one left behind by a crashed run is not bound to by an unrelated later invocation. Only ErrNotARepository reaches that branch: a broken .git means a repository is there and unusable, and binding such a clone to an ancestor's out-of-tree state would silently record its commits nowhere.

func NewGitStore

func NewGitStore(gitDir string) *Store

NewGitStore returns a Store over a repository's .git directory. Nothing is created or validated here.

func NewOutOfTreeStore

func NewOutOfTreeStore(dir string) *Store

NewOutOfTreeStore returns a Store over the directory that holds trace state outside a repository, as returned by NonGitDir.

func (*Store) AttestationStatePath

func (s *Store) AttestationStatePath() string

AttestationStatePath returns the path to the local attestation state file under <dir>/chainloop-trace/. This file isolates trace attestation state from the default chainloop state location.

func (*Store) ClearTraceRunActive

func (s *Store) ClearTraceRunActive() error

ClearTraceRunActive removes the trace-run-active sentinel.

func (*Store) CopyRawSessionFile

func (s *Store) CopyRawSessionFile(sessionID, srcPath string) error

CopyRawSessionFile copies a single session transcript file from srcPath to <dir>/chainloop-trace/raw/<sanitized-sessionID>.jsonl. Used by providers whose transcripts are single files (e.g. Cursor); Claude's multi-file layout (main + subagents) goes through CopySessionJSONL instead.

func (*Store) CopySessionJSONL

func (s *Store) CopySessionJSONL(sessionID, sourceDir string) error

CopySessionJSONL copies the Claude Code JSONL file to <dir>/chainloop-trace/raw/<id>.jsonl. It also copies subagent files if they exist.

func (*Store) DeleteFileSnapshot

func (s *Store) DeleteFileSnapshot(sessionID, filePath string)

DeleteFileSnapshot removes a file snapshot after it's been processed.

func (*Store) DeleteShellPreSignature

func (s *Store) DeleteShellPreSignature(sessionID string)

DeleteShellPreSignature removes the pre-command signature once processed.

func (*Store) Dir

func (s *Store) Dir() string

Dir returns the parent directory, one level above chainloop-trace/.

Only `trace run`'s cleanup needs it, to drop the empty out-of-tree directory after a non-git run. Inside a repository the same path is .git and must never be removed, so callers guarding on IsGit are the only legitimate users.

func (*Store) GCOrphans

func (s *Store) GCOrphans(liveSHAs map[string]bool) error

GCOrphans drops trace state whose underlying commit is no longer reachable from any local branch. liveSHAs is the union of SHAs reachable from every local branch tip; the caller computes it from the git client.

Two cleanups in sequence:

  1. Walk commits/, drop any record whose SHA isn't in liveSHAs (post-rebase orphan or commit on a deleted branch).
  2. Walk ai-lines/ and sessions/, drop any entry whose session ID is no longer referenced by a surviving CommitRecord.

An empty liveSHAs is treated as a pathological signal (no local branches readable, or the caller fed us garbage) and the GC short-circuits — wiping every record on a transient git error would be far worse than skipping a cycle of cleanup.

func (*Store) GitDir

func (s *Store) GitDir() string

GitDir returns the repository's .git directory, and "" outside a repository. Callers that install or remove git hooks must take the path from here, so a store that is not a .git directory cannot be mistaken for one.

func (*Store) InitTraceDir

func (s *Store) InitTraceDir() error

InitTraceDir creates the <dir>/chainloop-trace/ directory structure.

func (*Store) IsGit

func (s *Store) IsGit() bool

IsGit reports whether the state lives inside a repository's .git directory.

func (*Store) IsTraceInitialized

func (s *Store) IsTraceInitialized() bool

IsTraceInitialized checks whether trace has been initialized in this clone.

func (*Store) IsTraceRunActive

func (s *Store) IsTraceRunActive() bool

IsTraceRunActive reports whether a `chainloop trace run` session owns this store's trace state.

func (*Store) LoadAILineAttribution

func (s *Store) LoadAILineAttribution(sessionID string) *AILineAttribution

LoadAILineAttribution loads the AI line attribution for a session by consolidating all JSONL entries into a single AILineAttribution. Returns a zero-value struct if the file doesn't exist.

func (*Store) LoadAllAILineAttributions

func (s *Store) LoadAllAILineAttributions() ([]*AILineAttribution, error)

LoadAllAILineAttributions loads AI line attribution for all sessions that have data.

func (*Store) LoadAllCommitRecords

func (s *Store) LoadAllCommitRecords() ([]*CommitRecord, error)

LoadAllCommitRecords reads all commit records from <dir>/chainloop-trace/commits/.

func (*Store) LoadAllSessionRecords

func (s *Store) LoadAllSessionRecords() (map[string]*SessionRecord, error)

LoadAllSessionRecords reads every SessionRecord under <dir>/chainloop-trace/sessions/ into a map keyed by SessionID. A missing directory yields an empty (non-nil) map and no error; per-file decode errors are skipped so a single malformed record doesn't abort the load.

func (*Store) LoadFileSnapshot

func (s *Store) LoadFileSnapshot(sessionID, filePath string) ([]byte, error)

LoadFileSnapshot loads a previously stored file snapshot.

func (*Store) LoadSessionRecord

func (s *Store) LoadSessionRecord(sessionID string) (*SessionRecord, error)

LoadSessionRecord reads the SessionRecord for sessionID. Returns nil, nil when not found.

func (*Store) LoadShellPreSignature

func (s *Store) LoadShellPreSignature(sessionID string) (map[string]string, error)

LoadShellPreSignature loads the pre-command working-tree signature for a session.

func (*Store) LogFilePath

func (s *Store) LogFilePath() string

LogFilePath returns the path to the hook log file inside the trace directory.

func (*Store) MarkTraceInitialized

func (s *Store) MarkTraceInitialized() error

MarkTraceInitialized creates the sentinel file indicating trace is initialized.

func (*Store) MarkTraceRunActive

func (s *Store) MarkTraceRunActive() error

MarkTraceRunActive marks the trace state as belonging to a running `chainloop trace run` session. Removed by ClearTraceRunActive when the wrapped command exits.

func (*Store) RawSessionDir

func (s *Store) RawSessionDir() string

RawSessionDir returns the path to <dir>/chainloop-trace/raw/ for use as session dir in parsing.

func (*Store) RecordLineRanges

func (s *Store) RecordLineRanges(sessionID, filePath string, ranges []aicodingsession.LineRange) error

RecordLineRanges appends line ranges for a file to a session's attribution JSONL. An empty or nil ranges slice still records the file as touched (e.g., deletion-only edits).

func (*Store) RemoveTraceDir

func (s *Store) RemoveTraceDir() error

RemoveTraceDir removes the entire <dir>/chainloop-trace/ directory.

func (*Store) RemoveTraceInitialized

func (s *Store) RemoveTraceInitialized() error

RemoveTraceInitialized removes the sentinel file.

func (*Store) SaveCommitRecord

func (s *Store) SaveCommitRecord(rec *CommitRecord) error

SaveCommitRecord writes a commit record to <dir>/chainloop-trace/commits/<sha>.json.

func (*Store) SaveFileSnapshot

func (s *Store) SaveFileSnapshot(sessionID, filePath string, content []byte) error

SaveFileSnapshot stores a file's content before an AI edit.

func (*Store) SaveSessionRecord

func (s *Store) SaveSessionRecord(rec *SessionRecord) error

SaveSessionRecord writes a session record to <dir>/chainloop-trace/sessions/<id>.json.

func (*Store) SaveShellPreSignature

func (s *Store) SaveShellPreSignature(sessionID string, sig map[string]string) error

SaveShellPreSignature stores the working-tree signature captured before an agent-run shell command, so the post-command hook can diff against it. A single slot per session is used; concurrent shell calls in one turn overwrite it (see the parallel-shell limitation).

func (*Store) SessionRecordExists

func (s *Store) SessionRecordExists(sessionID string) bool

SessionRecordExists checks whether a session record file exists for the given session ID.

func (*Store) WipeTraceDir

func (s *Store) WipeTraceDir() error

WipeTraceDir clears the single-use data left over by a successful push: raw transcripts and per-edit file snapshots. It intentionally preserves commits/, ai-lines/, and sessions/ — those carry session attribution that survives across pushes (a rebase replays commits with the same trailers, so the next push can still attribute them). Bounded growth of the surviving subdirectories is the job of GCOrphans, called after the wipe.

Jump to

Keyboard shortcuts

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