session

package
v0.11.5 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// SessionStateDirName is the directory name for session state files within git common dir.
	SessionStateDirName = "entire-sessions"

	// StaleSessionThreshold is the duration after which an ended session is considered stale
	// and will be automatically deleted during load/list operations.
	StaleSessionThreshold = 7 * 24 * time.Hour

	// StuckActiveThreshold is the duration after which an ACTIVE session with no
	// interaction is considered stuck (used by "entire doctor" and "entire status").
	StuckActiveThreshold = 1 * time.Hour
)
View Source
const MaxLastPromptRunes = 100

MaxLastPromptRunes is the maximum rune length for LastPrompt stored in session state.

Variables

This section is empty.

Functions

func ApplyTransition added in v0.4.5

func ApplyTransition(ctx context.Context, state *State, result TransitionResult, handler ActionHandler) error

ApplyTransition applies a TransitionResult to state: sets the new phase unconditionally, then executes all actions. Common actions (UpdateLastInteraction, ClearEndedAt) always run regardless of handler errors so that bookkeeping fields stay consistent with the new phase. Strategy-specific handler actions stop on the first error; subsequent handler actions are skipped but common actions continue. Returns the first handler error, or nil.

func ClearGitCommonDirCache added in v0.4.6

func ClearGitCommonDirCache()

ClearGitCommonDirCache clears the cached git common dir. Useful for testing when changing directories.

func GetGitCommonDir added in v0.6.1

func GetGitCommonDir(ctx context.Context) (string, error)

GetGitCommonDir returns the .git common directory for the current working directory, absolute. In a regular checkout this is .git/; in a worktree, it's the main repo's .git/ (not .git/worktrees/<name>/). Result is cached per working directory.

The resolution lives in gitdir, which also owns the *os.Root over the same directory — this stays as the name 19 call sites already import. It used to be one of two hand-rolled copies of the same git subprocess, the other in strategy with no cache at all.

func MermaidDiagram added in v0.3.13

func MermaidDiagram() string

MermaidDiagram generates a Mermaid state diagram from the transition table. The diagram is derived by calling Transition() for representative context variants per phase/event, so it stays in sync with the implementation.

func TruncatePromptForStorage added in v0.9.0

func TruncatePromptForStorage(prompt string) string

TruncatePromptForStorage collapses whitespace and truncates a user prompt for storage in State.LastPrompt. LastPrompt is a display/preview field only — the full prompt is preserved in the session transcript — so bounding it keeps the state file and `--json` output small without losing recoverable data.

Types

type Action added in v0.3.13

type Action int

Action represents a side effect that should be performed after a transition. The caller is responsible for executing these -- the state machine only declares them.

const (
	ActionCondense               Action = iota // Condense session data to permanent storage
	ActionCondenseIfFilesTouched               // Condense only if FilesTouched is non-empty
	ActionDiscardIfNoFiles                     // Discard session if FilesTouched is empty
	ActionWarnStaleSession                     // Warn user about stale session(s)
	ActionClearEndedAt                         // Clear EndedAt timestamp (session re-entering)
	ActionUpdateLastInteraction                // Update LastInteractionTime
)

func (Action) String added in v0.3.13

func (a Action) String() string

String returns a human-readable name for the action.

type ActionHandler added in v0.4.5

type ActionHandler interface {
	HandleCondense(state *State) error
	HandleCondenseIfFilesTouched(state *State) error
	HandleDiscardIfNoFiles(state *State) error
	HandleWarnStaleSession(state *State) error
}

ActionHandler defines strategy-specific side effects for state transitions. The compiler enforces that every strategy-specific action has a handler.

type CondensationAttempt added in v0.10.3

type CondensationAttempt struct {
	CheckpointID    id.CheckpointID `json:"checkpoint_id"`
	RecoveryPending bool            `json:"recovery_pending,omitempty"`
}

CondensationAttempt records the durable intent for an in-progress condensation. RecoveryPending is set only when doctor must first look for a checkpoint written before attempt IDs existed.

type Event added in v0.3.13

type Event int

Event represents something that happened to a session.

const (
	EventTurnStart    Event = iota // Agent begins working on a prompt
	EventTurnEnd                   // Agent finishes its turn
	EventGitCommit                 // A git commit was made (PostCommit hook)
	EventSessionStart              // Session process started (SessionStart hook)
	EventSessionStop               // Session process ended (SessionStop hook)
	EventCompaction                // Agent compacted context mid-turn (PreCompress hook)
)

func (Event) String added in v0.3.13

func (e Event) String() string

String returns a human-readable name for the event.

type Kind added in v0.6.1

type Kind string

Kind identifies the purpose of a session. Empty means "normal" (legacy sessions + every session that isn't a review). Callers must not rely on Kind being set unless they specifically want to branch on it.

Kind is a discriminator — it distinguishes review variants at a per-session granularity. The checkpoint-level HasReview flag remains an umbrella that any review-kind session should set (so future review kinds like manual review can be added without changing summary-shape).

const (
	// KindAgentReview tags a session created by `entire review` (agent-driven
	// review). Future review kinds (e.g., manual review) should be defined as
	// distinct Kind values AND added to Kind.IsReview so the checkpoint's
	// HasReview umbrella flag keeps covering them.
	KindAgentReview Kind = "agent_review"

	// KindAgentInvestigate tags a session created by `entire investigate`
	// (agent-driven investigation). A session is review OR investigate, not
	// both — Kind is single-valued. Future investigate kinds should be added
	// to Kind.IsInvestigate so the checkpoint's HasInvestigation umbrella
	// flag keeps covering them.
	KindAgentInvestigate Kind = "agent_investigate"

	// KindImported tags a checkpoint created by `entire import` from a
	// pre-existing agent transcript. Imported checkpoints are read-only and
	// commit-less; they live on the v1 metadata branch and push like any other
	// checkpoint.
	KindImported Kind = "imported"
)

func (Kind) IsImported added in v0.9.0

func (k Kind) IsImported() bool

IsImported reports whether this Kind is a read-only session reconstructed by `entire import` from a pre-existing transcript. Imported sessions are exempt from lifecycle management (staleness, orphan cleanup) and are not resumable. Centralized here so those call sites don't couple to the string literal across packages.

func (Kind) IsInvestigate added in v0.7.0

func (k Kind) IsInvestigate() bool

IsInvestigate reports whether this Kind counts as "an investigation happened" for the purpose of CheckpointSummary.HasInvestigation. Extend this when adding new investigate-kind Kind values so the umbrella flag stays accurate without string-literal coupling across packages.

func (Kind) IsReview added in v0.6.1

func (k Kind) IsReview() bool

IsReview reports whether this Kind counts as "a review happened" for the purpose of CheckpointSummary.HasReview. Extend this when adding new review-kind Kind values (e.g. KindManualReview) so the umbrella flag stays accurate without string-literal coupling across packages.

type NoOpActionHandler added in v0.4.5

type NoOpActionHandler struct{}

NoOpActionHandler is a default ActionHandler where all methods are no-ops. Embed this in handler structs to only override the methods you need.

func (NoOpActionHandler) HandleCondense added in v0.4.5

func (NoOpActionHandler) HandleCondense(_ *State) error

func (NoOpActionHandler) HandleCondenseIfFilesTouched added in v0.4.5

func (NoOpActionHandler) HandleCondenseIfFilesTouched(_ *State) error

func (NoOpActionHandler) HandleDiscardIfNoFiles added in v0.4.5

func (NoOpActionHandler) HandleDiscardIfNoFiles(_ *State) error

func (NoOpActionHandler) HandleWarnStaleSession added in v0.4.5

func (NoOpActionHandler) HandleWarnStaleSession(_ *State) error

type Phase added in v0.3.13

type Phase string

Phase represents the lifecycle stage of a session.

const (
	PhaseActive Phase = "active"
	PhaseIdle   Phase = "idle"
	PhaseEnded  Phase = "ended"
)

func PhaseFromString added in v0.3.13

func PhaseFromString(s string) Phase

PhaseFromString normalizes a phase string, treating empty or unknown values as PhaseIdle for backward compatibility with pre-state-machine session files.

func (Phase) IsActive added in v0.3.13

func (p Phase) IsActive() bool

IsActive reports whether the phase represents an active agent turn.

type State

type State struct {
	// SessionID is the unique session identifier
	SessionID string `json:"session_id"`

	// CLIVersion is the version of the CLI that created this session
	CLIVersion string `json:"cli_version,omitempty"`

	// BaseCommit is the commit the session's pending work sits on top of.
	// Initially set to HEAD when the session starts, then moved to HEAD when it
	// moves (commit, pull, rebase, reset) and after condensation.
	BaseCommit string `json:"base_commit"`

	// WorktreePath is the absolute path to the worktree root
	WorktreePath string `json:"worktree_path,omitempty"`

	// WorktreeID is the internal git worktree identifier (empty for main worktree)
	// Derived from .git/worktrees/<name>/, stable across git worktree move
	WorktreeID string `json:"worktree_id,omitempty"`

	// AdoptedIntoWorktreePath marks a source-side tombstone left behind after
	// `entire session adopt` moves this session into another repository/worktree.
	// Hook TurnStart must not reactivate tombstoned source records, otherwise the
	// same session ID can diverge in two session stores.
	AdoptedIntoWorktreePath string `json:"adopted_into_worktree_path,omitempty"`

	// AdoptedIntoWorktreeID is the target worktree ID paired with
	// AdoptedIntoWorktreePath when available.
	AdoptedIntoWorktreeID string `json:"adopted_into_worktree_id,omitempty"`

	// Branch is the git branch HEAD pointed at the last time this session took a
	// turn. Captured on each turn start so it tracks branches created or renamed
	// after the session began. Empty when HEAD was detached or for sessions
	// recorded before this field existed (callers derive it from commit trailers
	// as a fallback). Lets `entire resume` map a stopped session back to its
	// branch without the user remembering it.
	Branch string `json:"branch,omitempty"`

	// StartedAt is when the session was started
	StartedAt time.Time `json:"started_at"`

	// EndedAt is when the session was explicitly closed by the user.
	// nil means the session is still active or was not cleanly closed.
	EndedAt *time.Time `json:"ended_at,omitempty"`

	// Phase is the lifecycle stage of this session (see phase.go).
	// Empty means idle (backward compat with pre-state-machine files).
	Phase Phase `json:"phase,omitempty"`

	// Kind tags the session's purpose. Empty for normal agent sessions;
	// set to KindAgentReview when the session was started by `entire review`.
	Kind Kind `json:"kind,omitempty"`

	// ReviewSkills is the snapshot of configured review skills at session start.
	// Preserved so checkpoint metadata records which skills were run. May be
	// empty when a review was attached post-hoc and skills were not declared;
	// ReviewPrompt is the ground truth in that case.
	ReviewSkills []string `json:"review_skills,omitempty"`

	// ReviewPrompt is the actual text of the review request — the composed
	// prompt sent to the agent (spawn path) or the session's first user
	// prompt (attach path). Always populated when Kind is a review kind.
	ReviewPrompt string `json:"review_prompt,omitempty"`

	// InvestigateRunID is the 12-hex-char ID of the parent investigation
	// run when Kind is an investigate kind. Multiple sessions across rounds
	// share this ID so the loop driver can correlate them. Empty for
	// non-investigate sessions.
	InvestigateRunID string `json:"investigate_run_id,omitempty"`

	// InvestigateTopic is the human-readable topic the investigation was
	// asked to investigate. Snapshot at session start so checkpoint
	// metadata records what the agent was investigating. Only meaningful
	// when Kind is an investigate kind.
	InvestigateTopic string `json:"investigate_topic,omitempty"`

	// TurnID is a unique identifier for the current agent turn.
	// Lifecycle:
	//   - Generated fresh in InitializeSession at each turn start
	//   - Shared across all checkpoints within the same turn
	//   - Used to correlate related checkpoints when a turn's work spans multiple commits
	//   - Persists until the next InitializeSession call generates a new one
	TurnID string `json:"turn_id,omitempty"`

	// TurnCheckpointIDs tracks all checkpoint IDs condensed during the current turn.
	// Lifecycle:
	//   - Set in PostCommit when a checkpoint is condensed for an ACTIVE session
	//   - Consumed in HandleTurnEnd to finalize all checkpoints with the full transcript
	//   - Cleared in HandleTurnEnd after finalization completes
	//   - Cleared in InitializeSession when a new prompt starts
	//   - Cleared when session is reset (ResetSession deletes the state file entirely)
	TurnCheckpointIDs []string `json:"turn_checkpoint_ids,omitempty"`

	// LastInteractionTime is updated on agent-interaction events (TurnStart,
	// TurnEnd, SessionStop, Compaction) but NOT on git commit hooks.
	// Used for stale session detection in "entire doctor".
	LastInteractionTime *time.Time `json:"last_interaction_time,omitempty"`

	// CaptureDegradedAt records when the session's most recent turn degraded
	// capture because a worktree status scan breached its budget (new-file
	// detection skipped, or the checkpoint itself skipped). Set at turn end,
	// cleared by the next turn whose scans stay within budget. Surfaced as a
	// warning by `entire status`.
	CaptureDegradedAt *time.Time `json:"capture_degraded_at,omitempty"`

	// StepCount is the number of checkpoints/steps created in this session.
	// JSON tag kept as "checkpoint_count" for backward compatibility with existing state files.
	StepCount int `json:"checkpoint_count"`

	// CheckpointTranscriptStart is the transcript line offset where the current
	// checkpoint cycle began. Set to 0 at session start, updated to current
	// transcript length after each condensation. Used to scope the transcript
	// for checkpoint condensation: "everything since last checkpoint".
	CheckpointTranscriptStart int `json:"checkpoint_transcript_start,omitempty"`

	// TokenTranscriptStart is the transcript position up to which this
	// session's tokens are already stored in a checkpoint. Carry-forward and
	// adopt move CheckpointTranscriptStart back to 0 so the next checkpoint
	// shows the whole conversation; they never reset this offset, so that
	// checkpoint still counts only new tokens. It moves back only when a
	// condensation or attach finds the transcript shorter (rewritten or
	// compacted) and records its new end. Nil in state written before the
	// field existed; NormalizeAfterLoad fills it. Read it through TokenStart.
	TokenTranscriptStart *int `json:"token_transcript_start,omitempty"`

	// CheckpointTranscriptSize is the byte size of the transcript at last condensation.
	// Used for fast "has new content?" checks in PostCommit: compare the git blob size
	// against this value without reading the full transcript content.
	CheckpointTranscriptSize int64 `json:"checkpoint_transcript_size,omitempty"`

	// TranscriptOffsetPending records that the turn-end advance of
	// CheckpointTranscriptStart could not run because a late-transcript agent
	// (agent.LateTranscriptWriter, e.g. Antigravity) had not flushed its
	// transcript by the Stop hook. Everything the flush will eventually write
	// is already condensed, so the next mid-turn condensation resolves the
	// pending advance against the by-then-flushed file — without this, prompts
	// and the scoped transcript for the next checkpoint would start inside the
	// previous (already-condensed) turn, attributing the wrong prompt to it.
	TranscriptOffsetPending bool `json:"transcript_offset_pending,omitempty"`

	// Deprecated: CondensedTranscriptLines is replaced by CheckpointTranscriptStart.
	// Kept for backward compatibility with existing state files.
	// Use NormalizeAfterLoad() to migrate.
	CondensedTranscriptLines int `json:"condensed_transcript_lines,omitempty"`

	// UntrackedFilesAtStart tracks files that existed at session start (so they are
	// not attributed to the session)
	UntrackedFilesAtStart []string `json:"untracked_files_at_start,omitempty"`

	// FilesTouched tracks files modified/created/deleted during this session
	FilesTouched []string `json:"files_touched,omitempty"`

	// TouchedFileHashes records the content a turn-end step left for each file
	// it changed: repo-relative path → git blob hash (hex) of the worktree file
	// at that turn end, with clean filters applied as `git add` would. A path
	// the step deleted maps to "" (a recorded deletion). A later step
	// overwrites an earlier one's entry. Entries exist only for paths in
	// FilesTouched; a FilesTouched path with no entry has no recorded content
	// (hashing failed, a symlink, or it arrived via a task record or per-tool
	// hook), and commit-time decisions fall back to its name.
	//
	// Commit hooks compare a committed or staged blob hash with this map to
	// tell "the human committed what the agent wrote" from "the human replaced
	// it". It replaced the shadow-branch snapshot those decisions used to
	// read, and records no content.
	TouchedFileHashes map[string]string `json:"touched_file_hashes,omitempty"`

	// LastCheckpointID is the checkpoint ID from the most recent condensation.
	// Used to restore the Entire-Checkpoint trailer on amend and to identify
	// sessions that have been condensed at least once. Cleared on new prompt.
	LastCheckpointID id.CheckpointID `json:"last_checkpoint_id,omitempty"`

	// CondensationAttempt is saved before a persistent checkpoint write so a
	// retry after process death keeps both the intended ID and recovery mode.
	CondensationAttempt *CondensationAttempt `json:"condensation_attempt,omitempty"`

	// FullyCondensed indicates this session has been condensed and has no remaining
	// carry-forward files. PostCommit skips fully-condensed sessions entirely.
	// Set after successful condensation when no files remain for carry-forward
	// and the session phase is ENDED. Cleared on session reactivation (ENDED →
	// ACTIVE via TurnStart, or ENDED → IDLE via SessionStart) by ActionClearEndedAt.
	FullyCondensed bool `json:"fully_condensed,omitempty"`

	// AttachedManually indicates this session was imported via
	// `entire session attach` rather than being captured by hooks during
	// normal agent execution.
	AttachedManually bool `json:"attached_manually,omitempty"`

	// ContextInjectionDecided records that the once-per-session model-context
	// injection (e.g. the `entire trail` pointer) has been handled for this
	// session, so the dispatcher does not re-inject on later turns. Set on the
	// first normal turn regardless of whether anything was injected: the prompt
	// path reads only clone-local cached trail enablement, and a missing/stale
	// false cache fails closed (miss the hint) rather than retrying/spamming.
	// Review/investigate sessions leave this false because they skip injection.
	ContextInjectionDecided bool `json:"context_injection_decided,omitempty"`

	// AgentType identifies the agent that created this session (e.g., "Claude Code", "Codex", "Cursor")
	AgentType types.AgentType `json:"agent_type,omitempty"`

	// ModelName is the LLM model used in this session (e.g., "claude-sonnet-4-20250514", "gpt-4o").
	// Set from hook data when the agent provides it.
	ModelName string `json:"model_name,omitempty"`

	// Token usage tracking (accumulated across all checkpoints in this session).
	//
	// DECISION: SubagentTokens is "latest snapshot wins", not summed. Subagent
	// usage arrives as a cumulative-since-session-start total (each subagent
	// transcript is re-read from line 0 every call), so accumulateTokenUsage
	// replaces rather than adds it (see cmd/entire/cli/strategy). Tradeoff: if
	// the main transcript resets or rotates mid-session (compaction writing a
	// fresh file, or a resume that truncates), a subsequent snapshot can be
	// SMALLER than a previous one, so this session-wide total regresses
	// (undercounts) for the rest of the session. This is accepted: undercounting
	// after a transcript reset is preferable to the multiplicative overcount the
	// summing approach produced, and the alternative (a session-wide high-water
	// mark) would mask genuine subagent-transcript cleanup. Checkpoint deltas do
	// not share this exposure — CheckpointTokenUsage.SubagentTokens is derived as
	// (this total - SubagentTokensBaseline) and floored at 0 by clampSubtract, so
	// a shrunk snapshot yields 0, never a negative or stale delta.
	TokenUsage *agent.TokenUsage `json:"token_usage,omitempty"`

	// CheckpointTokenUsage tracks hook-provided token usage since the last condensation.
	// This is checkpoint-scoped; TokenUsage remains the session-wide total.
	CheckpointTokenUsage *agent.TokenUsage `json:"checkpoint_token_usage,omitempty"`

	// SubagentTokensBaseline is a snapshot of TokenUsage.SubagentTokens captured
	// at the last condensation reset. Subagent token usage is always re-read
	// from the start of each subagent transcript (agent IDs are discovered from
	// the full main transcript so subagents spawned before the checkpoint
	// window are still found), so it arrives as a cumulative-since-session-start
	// total rather than a per-checkpoint delta. This baseline lets
	// CheckpointTokenUsage.SubagentTokens be rescoped to "since last
	// condensation" via SubtractTokenUsage instead of re-adding the same
	// cumulative total on every checkpoint.
	SubagentTokensBaseline *agent.TokenUsage `json:"subagent_tokens_baseline,omitempty"`

	// SubagentTokensBaselineComplete records whether the baseline is exact.
	SubagentTokensBaselineComplete *bool `json:"subagent_tokens_baseline_complete,omitempty"`

	// SkillEvents records explicit native skill signals observed during this session.
	// Stored as sidecar metadata so consumers can collapse skill-related transcript events
	// without mutating the raw agent transcript.
	//
	// This grows for the life of the session and is deliberately uncapped. It is
	// also the durable half of the exactly-once contract for skill telemetry:
	// extraction re-derives events from transcript offset 0 on every pass and
	// dedupes against this ledger (strategy.appendNewSkillEvents), so an event
	// whose entry never reached disk is announced twice. Trimming it therefore
	// re-enables double-reporting for exactly the long sessions a cap would
	// target.
	//
	// The cost is real but bounded, and it is paid on EVERY MutateSessionState —
	// i.e. every hook, including PostToolUse — because state is read and written
	// whole. Measured JSON round-trip: 0 events / 106 B / 1.6us; 10 / 6.0 KB /
	// 42us; 50 / 29.7 KB / 201us; 200 / 119 KB / 785us, i.e. ~594 B per event
	// and sub-millisecond at any realistic N. The steady-state dedupe rebuild is
	// cheap by comparison: 13us at 200 existing events.
	//
	// So a 100 KB session state is expected, not a leak. If the envelope ever
	// does need shrinking, the move is a narrower ledger — persist only the
	// dedupe keys (~40 B/event) and keep the full events transient — not a
	// truncation, which would break exactly-once.
	SkillEvents []agent.SkillEvent `json:"skill_events,omitempty"`

	// CommitCondensedSignalCheckpointID is the checkpoint ID of the last
	// cli_commit_condensed telemetry signal this session snapshotted, the
	// durable half of that signal's at-most-once contract. A `git commit
	// --amend` re-runs PostCommit with the SAME trailer checkpoint ID (an
	// ACTIVE session re-condenses unconditionally), so without this ledger one
	// logical commit is counted twice in both halves of the miss-rate ratio.
	// Persisted by the same state save that gates the emit, so a failed save
	// retries cleanly; a crash between save and emit loses the row, which is
	// the signal's accepted best-effort posture.
	CommitCondensedSignalCheckpointID string `json:"commit_condensed_signal_checkpoint_id,omitempty"`

	// Hook-provided session metrics (for agents like Cursor that report via hooks)
	SessionDurationMs int64 `json:"session_duration_ms,omitempty"`
	SessionTurnCount  int   `json:"session_turn_count,omitempty"`
	ContextTokens     int   `json:"context_tokens,omitempty"`
	ContextWindowSize int   `json:"context_window_size,omitempty"`

	// PromptWindowBase is the SessionTurnCount value at the start of the current
	// checkpoint window. The number of prompts attributed to the next checkpoint is
	// SessionTurnCount - PromptWindowBase (floored at 1 when written). It is only
	// advanced (deferred reset) the next time a turn is counted after a checkpoint
	// was written, so two checkpoints with no prompt between them report the same
	// count. Zero-value safe on old state files: base 0 ⇒ window = SessionTurnCount,
	// i.e. "all prompts so far" (correct first-checkpoint semantics).
	PromptWindowBase int `json:"prompt_window_base,omitempty"`

	// PromptWindowResetPending indicates a checkpoint was just written and the
	// window base must be re-anchored to the current SessionTurnCount the next time
	// a turn is counted. Deferred so back-to-back checkpoints share a count.
	PromptWindowResetPending bool `json:"prompt_window_reset_pending,omitempty"`

	// Deprecated: TranscriptLinesAtStart is replaced by CheckpointTranscriptStart.
	// Kept for backward compatibility with existing state files.
	TranscriptLinesAtStart int `json:"transcript_lines_at_start,omitempty"`

	// TranscriptIdentifierAtStart is the last transcript identifier when the session started.
	// Used for identifier-based transcript scoping (UUID for Claude, message ID for Gemini).
	TranscriptIdentifierAtStart string `json:"transcript_identifier_at_start,omitempty"`

	// TranscriptPath is the path to the live transcript file (for mid-session commit detection)
	TranscriptPath string `json:"transcript_path,omitempty"`

	// AgentHome is the agent's home directory (for example CLAUDE_CONFIG_DIR or
	// CODEX_HOME) whose session stores held TranscriptPath when it was last set
	// at session initialization or turn start, in whichever spelling (as the
	// environment sets it, or canonical) contains TranscriptPath. A home set by
	// an earlier turn is kept while its stores still hold TranscriptPath. It is
	// "" when the agent has no home layout or the transcript lies in none of
	// the home's stores. It grants no trust: readers pass it to
	// agent.ResolveTrustedHome before relying on it.
	AgentHome string `json:"agent_home,omitempty"`

	// LastPrompt is the most recent user prompt for this session (truncated for display).
	// Updated on every turn start (UserPromptSubmit). JSON tag kept as "first_prompt"
	// for backward compatibility with existing state files.
	LastPrompt string `json:"last_prompt,omitempty"`

	// Owner fingerprints the process that owns this session's agent turn,
	// captured at each turn start via proclive.ResolveOwner. It lets liveness
	// checks detect an ACTIVE session whose agent has exited (clean /exit,
	// crash, kill, terminal close, reboot) without a SessionStop hook firing —
	// see OwnerExited. nil for legacy sessions or when the owner couldn't be
	// resolved, in which case liveness falls back to the StuckActiveThreshold
	// timeout. Only meaningful on Owner.Host.
	Owner *proclive.Identity `json:"owner,omitempty"`

	// TaskRecords tracks subagents dispatched by this session — the durable
	// pointer ledger for subagent work. See TaskRecord.
	TaskRecords []TaskRecord `json:"task_records,omitempty"`

	// SubagentInventory retains Codex child identities independently of task
	// records so follow-up turns remain discoverable after materialization.
	SubagentInventory []SubagentInventoryEntry `json:"subagent_inventory,omitempty"`
	// SubagentLedgerVersion advances on a new child identity or non-empty turn.
	SubagentLedgerVersion uint64 `json:"subagent_ledger_version,omitempty"`
	// SubagentInventoryComplete distinguishes exact empty from legacy unknown.
	SubagentInventoryComplete *bool `json:"subagent_inventory_complete,omitempty"`
}

State represents the state of an active session. This is stored in .git/entire-sessions/<session-id>.json

func (*State) AddTaskRecord added in v0.10.3

func (s *State) AddTaskRecord(task TaskRecord)

AddTaskRecord records a subagent launch, replacing any existing entry with the same ToolUseID. Dedup by ToolUseID: a retried or duplicate launch event must not create two records for the same task, which would make RemoveTaskRecord leave a stale entry behind after the first is cleared.

func (*State) AdvanceCheckpointWindow added in v0.11.5

func (s *State) AdvanceCheckpointWindow(pos int)

AdvanceCheckpointWindow moves both transcript offsets to pos after a condensation stored the transcript and its tokens up to pos. A condensation that could not read the transcript reports pos 0 and stored no transcript tokens, so the token offset stays put rather than moving back and letting the next checkpoint count everything again.

func (*State) AdvanceDisplayWindow added in v0.11.5

func (s *State) AdvanceDisplayWindow(pos int)

AdvanceDisplayWindow moves only CheckpointTranscriptStart to pos, for paths that change what a checkpoint shows without storing tokens: the turn-end advances past already-condensed content, and carry-forward's restart at 0. The token offset is pinned first so an unset one cannot follow CheckpointTranscriptStart through TokenStart's fallback.

func (*State) BeginCondensationAttempt added in v0.10.3

func (s *State) BeginCondensationAttempt(checkpointID id.CheckpointID)

BeginCondensationAttempt records a checkpoint ID before its persistent write.

func (*State) ClearCondensationAttempt added in v0.10.3

func (s *State) ClearCondensationAttempt()

ClearCondensationAttempt completes or abandons the pending condensation.

func (*State) ClearLegacyTranscriptOffsets added in v0.7.8

func (s *State) ClearLegacyTranscriptOffsets()

ClearLegacyTranscriptOffsets clears deprecated transcript offset fields so callers that intentionally reset CheckpointTranscriptStart do not re-persist stale legacy state.

func (*State) CompleteTaskRecord added in v0.10.3

func (s *State) CompleteTaskRecord(toolUseID string, completedAt time.Time) bool

CompleteTaskRecord marks the record for toolUseID as consumed exactly once: it sets CompletedAt and returns true, or returns false — a no-op — when no record exists for toolUseID or it was already completed (CompletedAt already non-zero). This is the exactly-once completion guard equivalent to the old claim-and-remove semantics: whichever caller observes true proceeds with the capture, a racing duplicate sees false and skips.

The data fields (DeclaredTranscriptPath, Files, TokenUsage) are NOT set here. They are populated separately by the producer after successful extraction, via direct field mutation on the claimed record within the same MutateSessionState closure that called CompleteTaskRecord — see strategy.CompleteTaskRecord, the producer-facing wrapper that pairs the claim with the attach inside one lock.

func (*State) EnsureTaskRecord added in v0.11.0

func (s *State) EnsureTaskRecord(task TaskRecord) bool

EnsureTaskRecord adds a follow-up record only after an earlier completed record was materialized and removed. Existing unmaterialized content wins, but missing launch metadata is enriched for stop-before-start delivery.

func (*State) FinalizeSubagentTurn added in v0.11.0

func (s *State) FinalizeSubagentTurn(agentID, turnID string) bool

FinalizeSubagentTurn marks an observed turn finalized exactly once.

func (*State) FindSubagentInventory added in v0.11.0

func (s *State) FindSubagentInventory(agentID string) *SubagentInventoryEntry

FindSubagentInventory returns an entry that aliases state. Callers must use it only inside their current MutateSessionState closure.

func (*State) FindTaskRecord added in v0.10.3

func (s *State) FindTaskRecord(toolUseID string) *TaskRecord

FindTaskRecord returns a pointer to the record for toolUseID, or nil if none exists. The pointer aliases the slice element, so callers must not retain it across a mutation that could reallocate TaskRecords (e.g. AddTaskRecord, RemoveTaskRecord).

func (*State) FindTaskRecordByAgentID added in v0.11.4

func (s *State) FindTaskRecordByAgentID(agentID string) *TaskRecord

FindTaskRecordByAgentID returns the record whose AgentID is agentID, or nil. An empty agentID matches nothing. Used where the completing event names the subagent but not its tool_use_id (Claude Code's SubagentStop).

A live (uncompleted) record wins over a completed one, should two records ever share an agent ID before condensation; a completed match is returned only when no live one exists, so a duplicate stop still reaches the exactly-once guard and is skipped. (Claude Code continues a subagent with SendMessage, which reuses the agent ID without a new Agent call, so it does not create a second record.)

func (*State) HasPendingWork added in v0.11.5

func (s *State) HasPendingWork() bool

HasPendingWork reports whether this session holds work no checkpoint has captured yet: a turn-end step since the last condensation (StepCount), files still awaiting a commit (FilesTouched, including carry-forward), or subagent task records.

It is the single answer to "is there anything here to condense or lose?" — lifecycle cleanup, doctor, the session sweeper, and condensation all ask it, and must not re-derive it from the parts. (Shadow-branch existence used to stand in for it; nothing writes git objects at turn end anymore.)

It deliberately ignores FullyCondensed: a fully condensed session can still carry task records (a background subagent finishing after its session ended). Callers that must leave fully condensed sessions alone — doctor's ENDED classification, the zombie sweep, IsCondensableEndedSession — check FullyCondensed themselves, as they always have.

func (*State) HasTaskContent added in v0.10.3

func (s *State) HasTaskContent() bool

HasTaskContent reports whether this session carries pending subagent task content: any task record — live (transcript-so-far still needs capturing) or completed-unmaterialized (awaiting condensation) — counts.

func (*State) IsEnded added in v0.10.1

func (s *State) IsEnded() bool

IsEnded reports whether this session has been finalized — the canonical "no longer a live session" predicate.

Both halves matter and neither implies the other in practice: Phase is what the state machine sets, EndedAt is what the finalizing write stamps, and a state file can carry one without the other (a legacy record, or a partial write). Callers that filter for active sessions must agree on this rule, so it lives here rather than being re-spelled at each site.

func (*State) IsStale added in v0.4.6

func (s *State) IsStale() bool

func (*State) IsStuckActive added in v0.5.4

func (s *State) IsStuckActive() bool

IsStale returns true when a session hasn't seen interaction for longer than StaleSessionThreshold. Falls back to StartedAt when LastInteractionTime is nil (sessions created before interaction tracking was added). IsStuckActive returns true if the session is in ACTIVE phase but has not had any interaction for longer than StuckActiveThreshold. Falls back to StartedAt when LastInteractionTime is nil, so brand-new sessions are not falsely flagged.

func (*State) LiveTaskRecords added in v0.10.3

func (s *State) LiveTaskRecords() []TaskRecord

LiveTaskRecords returns the records not yet completed (CompletedAt zero) — i.e. still in flight. In-flight consumers (the SessionEnd sweep's "any in-flight work?" check) must use this rather than the raw TaskRecords slice: since CompleteTaskRecord no longer removes a record, TaskRecords mixes live records with already-completed ones awaiting materialization.

func (*State) NeedsCondensationRecovery added in v0.10.3

func (s *State) NeedsCondensationRecovery() bool

NeedsCondensationRecovery reports whether doctor must reconcile a checkpoint written before attempt IDs existed.

func (*State) NormalizeAfterLoad added in v0.3.13

func (s *State) NormalizeAfterLoad(ctx context.Context)

NormalizeAfterLoad applies backward-compatible migrations to state loaded from disk. Call this after deserializing a State from JSON.

func (*State) OwnerExited added in v0.7.8

func (s *State) OwnerExited() bool

OwnerExited reports true when this session's owning agent process is gone — exited cleanly, crashed, was killed, or the machine rebooted — without a SessionStop hook firing. Unlike IsStuckActive (a time-based heuristic), this is detected immediately, regardless of how recently the session interacted. It returns false when liveness is Unknown (no owner recorded, cross-host state, or an unsupported platform) so behavior degrades to the StuckActiveThreshold timeout.

It deliberately covers IDLE as well as ACTIVE. An agent that finishes its last turn and then quits leaves the session IDLE, so gating on ACTIVE alone missed precisely the sessions left behind by agents with no session-end hook (Codex before 0.146) or killed before that hook could run: they lingered as "active" in `entire status` until StaleSessionThreshold deleted the state file outright, discarding pending checkpoint work instead of condensing it. Only already-finalized sessions are excluded — see IsEnded.

func (*State) OwnerLiveness added in v0.7.8

func (s *State) OwnerLiveness() proclive.Liveness

OwnerLiveness reports the liveness of this session's recorded owner process. It returns proclive.LivenessUnknown when no owner was recorded (legacy sessions, or sessions where the owner couldn't be resolved), so callers can fall back to the time-based IsStuckActive heuristic.

func (*State) PendingCondensationID added in v0.10.3

func (s *State) PendingCondensationID() id.CheckpointID

PendingCondensationID returns the checkpoint ID reserved for an in-progress condensation, or the empty ID when no attempt is pending.

func (*State) RebaselineSubagentTokens added in v0.9.0

func (s *State) RebaselineSubagentTokens()

RebaselineSubagentTokens snapshots the current cumulative subagent total (TokenUsage.SubagentTokens) into SubagentTokensBaseline so the next checkpoint window's CheckpointTokenUsage.SubagentTokens is rescoped to "since this re-baseline" rather than re-reporting the full cumulative subagent total.

The invariant is: every site that starts a fresh checkpoint window by clearing CheckpointTokenUsage MUST also re-baseline. Callers: the condensation reset helper (resetCheckpointWindow) and attach. Session adoption deliberately does not: it continues the source's token window.

func (*State) RecordSubagentStop added in v0.11.0

func (s *State) RecordSubagentStop(agentID, turnID string) bool

RecordSubagentStop records a provisional stop. Stops can arrive before starts, so the same mutation also preserves a pending task record. A late start enriches that placeholder through EnsureTaskRecord.

func (*State) RegisterSubagent added in v0.11.0

func (s *State) RegisterSubagent(agentID, turnID string) bool

RegisterSubagent observes a stable child identity and optionally one child turn. Only a first child or first non-empty turn invalidates cached totals.

func (*State) RemoveTaskRecord added in v0.10.3

func (s *State) RemoveTaskRecord(toolUseID string)

RemoveTaskRecord clears the record for toolUseID, if present. No-op when no record matches. Retained for tests and any caller that genuinely wants to discard a record outright — ordinary completion should use CompleteTaskRecord instead, which keeps the record for the materializer.

func (*State) RequireCondensationRecovery added in v0.10.3

func (s *State) RequireCondensationRecovery()

RequireCondensationRecovery keeps legacy orphan reconciliation enabled for the current attempt. It has no effect when no attempt is pending.

func (*State) SetTokenStart added in v0.11.5

func (s *State) SetTokenStart(pos int)

SetTokenStart records that tokens up to pos are stored in a checkpoint. It always assigns a fresh pointer so shallow copies of a state never share it.

func (*State) TokenStart added in v0.11.5

func (s *State) TokenStart() int

TokenStart returns the transcript position the next checkpoint counts tokens from. See TokenTranscriptStart.

func (*State) UpdateSubagentTranscriptPaths added in v0.11.0

func (s *State) UpdateSubagentTranscriptPaths(agentID, declaredPath, resolvedPath string) bool

UpdateSubagentTranscriptPaths enriches an already-observed child's path metadata. Resolution is not an inventory observation, so it deliberately does not advance SubagentLedgerVersion or invalidate token coverage.

type StateStore

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

StateStore provides low-level operations for managing session state files.

StateStore is a primitive for session state persistence. It is NOT the same as the Sessions interface - it only handles state files in .git/entire-sessions/, not the full session data which includes checkpoint content.

Use StateStore directly in strategies for performance-critical state operations. Use the Sessions interface (when implemented) for high-level session management.

func NewStateStore

func NewStateStore(ctx context.Context) (*StateStore, error)

NewStateStore creates a new state store. Uses the git common dir to store session state (shared across worktrees).

func NewStateStoreForWorktree added in v0.10.3

func NewStateStoreForWorktree(ctx context.Context, worktreeRoot string) (*StateStore, error)

NewStateStoreForWorktree returns the state store for the repository at worktreeRoot, independent of the process's working directory. Callers that operate on a repo passed as an argument (e.g. agent import) must use this: the CWD-resolved NewStateStore writes session state into whatever repo the process happens to run in, which is how test fixtures once leaked into a developer's real .git/entire-sessions and hijacked commit linking.

func NewStateStoreWithDir

func NewStateStoreWithDir(stateDir string) *StateStore

NewStateStoreWithDir creates a new state store with a custom directory. This is useful for testing.

func (*StateStore) Clear

func (s *StateStore) Clear(ctx context.Context, sessionID string) error

Clear removes the session state file for the given session ID.

func (*StateStore) List

func (s *StateStore) List(ctx context.Context) ([]*State, error)

List returns all session states, deleting any that have gone stale.

func (*StateStore) ListReadOnly added in v0.11.0

func (s *StateStore) ListReadOnly(ctx context.Context) ([]*State, error)

ListReadOnly returns every persisted session without deleting or hiding stale records.

`entire status` is the caller this exists for: asking what is happening must not change what is happening. Cleanup stays with the callers that own it — doctor and the session sweeper — which keep using List.

func (*StateStore) Load

func (s *StateStore) Load(ctx context.Context, sessionID string) (*State, error)

Load loads the session state for the given session ID. Returns (nil, nil) when session file doesn't exist or session is stale (not an error condition). Stale sessions (ended longer than StaleSessionThreshold ago) are automatically deleted.

func (*StateStore) RemoveAll

func (s *StateStore) RemoveAll() error

RemoveAll removes the entire session state directory. This is used during uninstall to completely remove all session state.

func (*StateStore) Save

func (s *StateStore) Save(ctx context.Context, state *State) error

Save saves the session state atomically.

type SubagentInventoryEntry added in v0.11.0

type SubagentInventoryEntry struct {
	AgentID                string   `json:"agent_id"`
	DeclaredTranscriptPath string   `json:"declared_transcript_path,omitempty"`
	ResolvedTranscriptPath string   `json:"resolved_transcript_path,omitempty"`
	ObservedTurnIDs        []string `json:"observed_turn_ids,omitempty"`
	FinalizedTurnIDs       []string `json:"finalized_turn_ids,omitempty"`
}

type TaskRecord added in v0.10.3

type TaskRecord struct {
	// ToolUseID is the Task tool invocation's tool_use_id — the same ID used
	// to key TaskMetadataDir. Dedup key for AddTaskRecord. For a Claude Code
	// Workflow agent it is the agent ID instead: those agents have no tool call
	// of their own (the Workflow's tool_use_id is shared by every agent in the
	// run), so the record is keyed by the subagent, and AgentID equals it.
	ToolUseID string `json:"tool_use_id"`

	// AgentID is the subagent identifier (tool_response.agentId at launch
	// time), used to resolve the subagent's own transcript file.
	AgentID string `json:"agent_id,omitempty"`

	// StartedAt is when the background launch was observed.
	StartedAt time.Time `json:"started_at"`

	// Background marks a record created at a background launch, which runs
	// until a later SubagentStop instead of completing at launch. Its stop
	// captures no worktree scan, so another background task overlapping its
	// run makes new untracked files ambiguous between the two.
	Background bool `json:"background,omitempty"`

	// SubagentType and TaskDescription are captured at launch time because
	// SubagentStop payloads carry no tool_input — a Final-path capture has no
	// way to derive them from the event itself (ParseSubagentTypeAndDescription
	// yields empty strings on a nil ToolInput). The Final handler reads these
	// from the record to label the task step, falling back to the event's own
	// fields only when the record is absent.
	SubagentType    string `json:"subagent_type,omitempty"`
	TaskDescription string `json:"task_description,omitempty"`

	// DeclaredTranscriptPath is the subagent transcript path an agent's stop
	// hook declared (e.g. Claude Code's agent_transcript_path, or the
	// equivalent Codex/Cursor field) — the path #2058 says must not be lost
	// between mid-turn capture and condensation-time materialization. Empty
	// when no declared path was available; the materializer falls back to
	// ResolveAgentTranscriptPath in that case.
	DeclaredTranscriptPath string `json:"declared_transcript_path,omitempty"`

	// TranscriptUnavailable is set when the agent stores child activity only in
	// the parent transcript. It prevents generic layout fallback from attaching
	// an unrelated file to this record during condensation.
	TranscriptUnavailable bool `json:"transcript_unavailable,omitempty"`

	// Files is the set of files touched by this subagent, merged into the
	// session's FilesTouched at completion time. Populated when the record
	// is completed; empty for a still in-flight record and for a completed
	// read-only subagent.
	Files []string `json:"files,omitempty"`

	// TokenUsage is this subagent's token usage, when the completing hook
	// payload provided one or the agent computed it from the subagent's
	// transcript. nil when unavailable.
	TokenUsage *agent.TokenUsage `json:"token_usage,omitempty"`

	// TokenUsageFromTranscript records that TokenUsage was computed from the
	// subagent's transcript at completion (or would have been, had that read
	// found usage) rather than reported by the agent. Condensation recomputes
	// such usage from the transcript it stores, which by then holds API calls
	// the agent had not yet written when it stopped.
	TokenUsageFromTranscript bool `json:"token_usage_from_transcript,omitempty"`

	// CompletedAt is when this record was completed (CompleteTaskRecord).
	// Zero means the record is still in flight. See the type doc comment.
	CompletedAt time.Time `json:"completed_at,omitempty"`
}

TaskRecord is the durable pointer ledger entry for a subagent dispatched by this session: a small session-state record (correlation ID, agent type, description, declared transcript path, files touched, tokens). Condensation is what materializes a record's transcript (sanitize → externalize → redact) into the parent session's checkpoint — see docs/superpowers/plans/2026-08-19-subagent-durable-records.md.

CompletedAt zero means the record is still in flight (background launch observed, no completion signal yet); non-zero means the record was completed — via CompleteTaskRecord — by a foreground/background post-task capture, SubagentStop, or the SessionEnd sweep. Unlike the prior remove-on-claim model, a completed record is NOT deleted: it must persist so a later condensation can materialize its transcript. Consumed by the Final captures — the SubagentStop handler (handleSubagentStopFinal) and the SessionEnd sweep (completeLiveTaskRecords). A Final capture completes the record LAST, after successful extraction (strategy.CompleteTaskRecord's exactly-once mutation), so a failed capture leaves the record live for the SessionEnd sweep to retry.

type TransitionContext added in v0.3.13

type TransitionContext struct {
	HasFilesTouched    bool // len(FilesTouched) > 0
	IsRebaseInProgress bool // .git/rebase-merge/ or .git/rebase-apply/ exists
}

TransitionContext provides read-only context for transitions that need to inspect session state without mutating it.

type TransitionResult added in v0.3.13

type TransitionResult struct {
	NewPhase Phase
	Actions  []Action
}

TransitionResult holds the outcome of a state machine transition.

func Transition added in v0.3.13

func Transition(current Phase, event Event, ctx TransitionContext) TransitionResult

Transition computes the next phase and required actions given the current phase and an event. This is a pure function with no side effects.

Empty or unknown phase values are normalized to PhaseIdle for backward compatibility with session state files created before phase tracking.

Jump to

Keyboard shortcuts

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