changes

package
v0.32.2 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Auto-skip learning for ChangeTracker shell-mutation tracking.

After the first walk of a fat directory (one whose file count exceeds autoSkipFileCountThreshold or autoSkipCumulativeThreshold), the dir is added to autoSkipDirs so subsequent walks skip it entirely. Learned sets persist across agent sessions via change_tracking_shell_persist.go.

Mutation recording and bulk rollup for ChangeTracker shell-mutation tracking.

Takes the diff from before/after snapshots and records TrackedFileChange entries, collapsing high-churn directories into bulk rollups when appropriate.

Shell-mutation tracking: captures before/after snapshots around shell_command invocations to detect file changes the structured tools miss (sed, mv, rm, etc.). Supporting code: change_tracking_snapshot.go, change_tracking_mutations.go, change_tracking_autoskip.go, change_tracking_shell_persist.go.

Persistence for the ChangeTracker's adaptive auto-skip set.

The walker learns "fat directories" on first visit (those with more immediate child files than autoSkipFileCountThreshold) and skips them on subsequent walks within the same session. Without persistence, every new agent session re-learns from scratch — paying the first-walk cost over and over for the same dirs.

This file persists the learned set to `~/.config/sprout/shell_skip_dirs.json`, keyed by absolute workspace root, so subsequent sessions in the same workspace inherit the learning. The file is best-effort: read failures fall back to an empty set (re-learn), write failures log a warning.

To prevent unbounded growth across many workspaces, the file caps at maxPersistedWorkspaces entries — least-recently-used workspaces are evicted first when the cap is exceeded.

Snapshot walking and file I/O for ChangeTracker shell-mutation tracking.

Walks the workspace tree before and after shell commands, capturing file bytes inside size/binary limits, then diffing the two snapshots.

SP-141 phase 2: the pure transcript-manifest half of the old pkg/agent/transcript_snapshot.go. The snapshot/diff structs stay in pkg/agent (their State field is *agent.ConversationState, a core type that moves last); everything here is agent-free logic: file-change manifest extraction, compacted-segment block round-tripping, message-source annotation, snapshot directory layout + working-dir path normalization, retention pruning, and label sanitizing.

Index

Constants

View Source
const CompactedFilesHeader = "Files modified during compacted segment:"

CompactedFilesHeader marks the file-change manifest block that `/compact` appends to its LLM-generated summary. Future compactions re-parse this block to keep the running file-change history visible across the summary boundary — without this, every `/compact` would lose the manifest of files touched in the summarized turns. pkg/agent aliases this constant next to its snapshot methods.

View Source
const RedactedContentMarker = history.RedactedContentMarker

RedactedContentMarker aliases history.RedactedContentMarker so existing call sites within this package keep working.

Variables

This section is empty.

Functions

func BackupFileWithExt

func BackupFileWithExt(src, ext string) error

backupFileWithExt copies the current contents of src to src+ext, overwriting any previous backup. Best-effort: errors are returned to the caller, who treats them as non-fatal. Exported for pkg/agent's session-persistence forwarder (SP-141 phase 2).

func FormatFileChangesForSummary

func FormatFileChangesForSummary(changes []TranscriptFileChange) string

FormatFileChangesForSummary renders a manifest into the canonical text block appended to a /compact summary. The format is chosen so parseCompactedFilesBlock can round-trip it back to TranscriptFileChange entries, preserving source / tool attribution across compaction boundaries. Returns the empty string when the manifest is empty so callers can skip appending altogether.

func ListTranscriptSnapshots

func ListTranscriptSnapshots(sessionID, workingDir string) ([]string, error)

ListTranscriptSnapshots returns snapshot file paths for the given session within the current workspace scope, sorted oldest-first.

func NormalizeWorkingDirectory

func NormalizeWorkingDirectory(workingDir string) (string, error)

NormalizeWorkingDirectory normalizes a working directory to a cleaned, symlink-resolved absolute path (empty resolves to the process CWD). Moved from pkg/agent/persistence_message.go in SP-141 phase 2; shared by transcript session-dir resolution and session persistence.

func PruneTranscriptDir

func PruneTranscriptDir(dir string)

PruneTranscriptDir enforces the default per-bucket retention caps on a session's transcript directory. Exported for pkg/agent's CaptureTranscriptSnapshot (SP-141 phase 2).

func SanitizeSnapshotLabel

func SanitizeSnapshotLabel(label string) string

SanitizeSnapshotLabel reduces a snapshot label to the canonical filename-safe form. Exported for pkg/agent's CaptureTranscriptSnapshot (SP-141 phase 2).

func SetTrackedChangeCaps added in v0.22.6

func SetTrackedChangeCaps(maxChanges int, maxBytes int64)

SetTrackedChangeCaps overrides the buffer caps (tests / tuning).

func TranscriptSessionDir

func TranscriptSessionDir(sessionID, workingDir string) (string, error)

TranscriptSessionDir returns the per-session transcript directory for the given session within the current workspace scope: <state>/transcripts/<scope-hash>/<session-id>. Exported for pkg/agent's CaptureTranscriptSnapshot (SP-141 phase 2).

func WorkingDirectoryScopeHash

func WorkingDirectoryScopeHash(workingDir string) string

WorkingDirectoryScopeHash returns the short hash that scopes per-working-directory state directories (transcripts, scoped sessions). Moved from pkg/agent/persistence_message.go in SP-141 phase 2.

func WriteFileAtomic

func WriteFileAtomic(path string, data []byte, perm os.FileMode) error

WriteFileAtomic durably replaces path's contents: write to a temp file in the same directory, fsync, then rename over the destination. A crash at any point leaves either the old or the new file — never a torn write. Exported because pkg/agent's session persistence shares it (SP-141 phase 2); the package-local writeFileAtomic name continues below.

Types

type AgentView

type AgentView interface {
	// GetSessionID returns the session identifier ("" when unset).
	GetSessionID() string
	// GetModel returns the model identifier ("unknown" fallback is the
	// tracker's, not the agent's).
	GetModel() string
	// GetWorkspaceRoot returns the logical workspace root ("" when unset).
	GetWorkspaceRoot() string
	// GenerateResponse runs an LLM completion (AI summary path).
	GenerateResponse(messages []api.Message) (string, error)
	// DebugLogger returns the agent logger or nil; the tracker logs
	// debug-level shell-snapshot messages through it when non-nil.
	DebugLogger() DebugLogger
	// PublishFileChange emits a file_changed event for tracker-detected
	// mutations. Implementations drop the event when no bus is wired.
	PublishFileChange(filePath, action, content string)
	// PublishRawFileChanged publishes a file_changed event WITHOUT the
	// agent's event-metadata decoration (bulk rollups and destructive
	// rollups publish command labels / directory paths, not real files,
	// and historically bypassed decorateEventPayload via a raw
	// eventBus.Publish). Implementations must preserve that behavior.
	PublishRawFileChanged(eventType string, payload interface{})
}

AgentView is the seam between the change tracker and its host agent (SP-141 phase 2). The tracker lives in pkg/agent/changes; the agent lives in pkg/agent, and the import arrow is one-way (pkg/agent → changes), so the tracker sees only this narrow interface. pkg/agent adapts *Agent to it via the unexported changesAgentView adapter — the same shape phase 1 used for workflow.LoopAgent. Every method is nil-tolerant on the implementation side where the old direct-field access was nil-checked.

type ChangeTracker

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

ChangeTracker manages change tracking for the agent workflow

func NewChangeTracker

func NewChangeTracker(view AgentView, instructions string) *ChangeTracker

NewChangeTracker creates a new change tracker for an agent session. view may be nil (bare trackers in tests); every view access is nil-guarded exactly where the old ct.agent accesses were.

func NewTrackerForTesting

func NewTrackerForTesting(revisionID string) *ChangeTracker

NewTrackerForTesting returns a minimal enabled tracker for tests that exercise tracker methods directly: no agent view, no history-path initialization. revisionID may be empty.

func (*ChangeTracker) ApplyShellWalkConfig

func (ct *ChangeTracker) ApplyShellWalkConfig(cfg ShellWalkConfig)

ApplyShellWalkConfig stamps resolved ChangeTrackingConfig values onto the tracker. Exported for pkg/agent's applyChangeTrackingConfig facade (SP-141 phase 2); zero-value MaxFiles/MaxTotalBytes/ MaxDuration/AutoSkipFileCountThreshold mean "no override — use the package default", exactly as before.

func (*ChangeTracker) Clear

func (ct *ChangeTracker) Clear()

Clear clears all tracked changes (but keeps the tracker enabled). Also resets the shell-snapshot cache.

func (*ChangeTracker) CollectFileChangesForCheckpoint

func (ct *ChangeTracker) CollectFileChangesForCheckpoint() ([]CheckpointFileChange, string)

CollectFileChangesForCheckpoint returns the (path, op) manifest of changes appended since the most recent checkpoint capture.

func (*ChangeTracker) Commit

func (ct *ChangeTracker) Commit(llmResponse string, conversation []api.Message) error

Commit commits all tracked changes to the change tracker

func (*ChangeTracker) Disable

func (ct *ChangeTracker) Disable()

Disable disables change tracking.

func (*ChangeTracker) Enable

func (ct *ChangeTracker) Enable()

Enable enables change tracking.

func (*ChangeTracker) GenerateAISummary

func (ct *ChangeTracker) GenerateAISummary() (string, error)

GenerateAISummary creates an AI-generated summary of the changes.

func (*ChangeTracker) GetChangeCount

func (ct *ChangeTracker) GetChangeCount() int

GetChangeCount returns the number of tracked changes

func (*ChangeTracker) GetChanges

func (ct *ChangeTracker) GetChanges() []TrackedFileChange

GetChanges returns a copy of the tracked changes

func (*ChangeTracker) GetRevisionID

func (ct *ChangeTracker) GetRevisionID() string

GetRevisionID returns the current revision ID

func (*ChangeTracker) GetSummary

func (ct *ChangeTracker) GetSummary() string

GetSummary returns a deterministic summary of tracked changes (no LLM call).

func (*ChangeTracker) GetTrackedFiles

func (ct *ChangeTracker) GetTrackedFiles() []string

GetTrackedFiles returns a list of files that have been modified

func (*ChangeTracker) IsEnabled

func (ct *ChangeTracker) IsEnabled() bool

IsEnabled returns whether change tracking is enabled. Production code must call this instead of reading ct.enabled directly.

func (*ChangeTracker) IsOutsideWorkspace

func (ct *ChangeTracker) IsOutsideWorkspace(filePath string) bool

IsOutsideWorkspace returns true if filePath is outside the agent's workspace root. Exported for pkg/agent's IsPathOutsideWorkspace facade (SP-141 phase 2).

func (*ChangeTracker) MarkTurnStart added in v0.23.0

func (ct *ChangeTracker) MarkTurnStart()

MarkTurnStart records the change count at the moment the current turn window opens. The agent calls it from EnableChangeTracking, which runs once per turn, so the window spans exactly that turn's own changes.

func (*ChangeTracker) MergeChild

func (ct *ChangeTracker) MergeChild(changes []TrackedFileChange, source string)

MergeChild appends a subagent's tracked changes into this (parent) tracker so list_changes / recover_file / revert_my_changes see subagent edits too. Each merged entry is tagged with Source.

func (*ChangeTracker) PrimeShellTracking

func (ct *ChangeTracker) PrimeShellTracking(workDir string)

PrimeShellTracking captures the workspace's current state as the baseline against which future shell_command invocations are diffed. Idempotent: a second call against the already-primed tracker is a no-op. Safe to call multiple times — only the first does work.

Lazy callers can skip this and rely on TrackShellTurn to auto-prime on first invocation; in that mode the first shell_command's own pre-state is captured but no changes are recorded for it (the initial walk IS the baseline). When the first shell command's mutations need to be tracked, PrimeShellTracking should be called from EnableChangeTracking so the baseline pre-exists.

func (*ChangeTracker) RebaseShellTracking added in v0.21.5

func (ct *ChangeTracker) RebaseShellTracking(workDir string)

RebaseShellTracking re-captures the workspace as the shell-diff baseline (priming it if needed). Called at the start of each primary-agent turn so the user's own edits between turns — files they create, change, or delete in their editor — become part of the baseline instead of being reported as the agent's mutations at its next shell command (where revert would then undo or delete the user's work).

func (*ChangeTracker) RecordRevert added in v0.21.5

func (ct *ChangeTracker) RecordRevert(filePath, before, after string, existsAfter bool, toolCall string)

RecordRevert records that a revert or recovery restored filePath, so the change history reflects it: list_changes nets the file out (it is back to its original state) instead of still listing the reverted change, and a later revert sees the restored content as current. existsAfter=false records the removal of a file the session created.

func (*ChangeTracker) RecordShellMutations

func (ct *ChangeTracker) RecordShellMutations(before, after map[string]*shellSnapshotEntry, toolCall string)

RecordShellMutations diffs a pair of snapshots (before/after a shell_command invocation) and appends TrackedFileChange entries for every file that materially changed. Deduplicates against direct-tool hooks. Above shellBulkThreshold, collapses into bulk rollup.

Production diffing now happens inside TrackShellTurn (walkWorkspace computes pending changes against the live cache), so this exists for callers that hold explicit before/after snapshot pairs — the tests, and any future caller that captures snapshots out-of-band.

func (*ChangeTracker) Reset

func (ct *ChangeTracker) Reset(instructions string)

Reset resets the change tracker with a new revision ID and instructions

func (*ChangeTracker) SetRevisionIDForTest

func (ct *ChangeTracker) SetRevisionIDForTest(id string)

SetRevisionIDForTest stamps a fixed revision ID (tests need deterministic revision ids that the generated one can't provide).

func (*ChangeTracker) SetSessionID

func (ct *ChangeTracker) SetSessionID(sessionID string)

func (*ChangeTracker) SetTrackedChangesForTesting

func (ct *ChangeTracker) SetTrackedChangesForTesting(changes []TrackedFileChange)

SetTrackedChangesForTesting replaces the tracker's change buffer. Test seam: production code appends through the Track* methods.

func (*ChangeTracker) SetView

func (ct *ChangeTracker) SetView(view AgentView)

SetView (re)binds the tracker's host view. Construction wires the view; tests that build a bare tracker (nil view) then attach an Agent rebind here, mirroring the old `tracker.agent = a` in-package assignment.

func (*ChangeTracker) ShellCachePrimed

func (ct *ChangeTracker) ShellCachePrimed() bool

ShellCachePrimed reports whether the shell-snapshot baseline cache has been primed. Exported as part of the phase-2 seam: pkg/agent's TestAgent_EnableChangeTracking_SubagentSkipsShellPrime reads the old unexported shellCache field to distinguish eager (primary) from lazy (subagent) priming.

func (*ChangeTracker) SyncShellCacheForPath

func (ct *ChangeTracker) SyncShellCacheForPath(path string)

SyncShellCacheForPath refreshes the shell cache entry for one path against its current on-disk state. Called by direct file-write hooks so the cache reflects writes the agent just performed.

func (*ChangeTracker) TrackFileEdit

func (ct *ChangeTracker) TrackFileEdit(filePath string, originalContent string, newContent string) error

TrackFileEdit tracks an edit operation (EditFile tool)

func (*ChangeTracker) TrackFileWrite

func (ct *ChangeTracker) TrackFileWrite(filePath string, originalContent string, newContent string) error

TrackFileWrite tracks a write operation (WriteFile tool). originalContent is the file's pre-write state, captured by the handler before the write. Empty means the file did not exist (a create). Never re-read filePath here: the write has already happened, so a re-read returns new content and recovery becomes a no-op.

func (*ChangeTracker) TrackFileWriteState added in v0.21.5

func (ct *ChangeTracker) TrackFileWriteState(filePath string, originalContent string, newContent string, existed bool) error

TrackFileWriteState is TrackFileWrite for callers that know whether the file existed before the write. An empty original alone can't distinguish a new file from an existing empty one (.gitkeep, __init__.py); recording the latter as a create made revert delete it.

func (*ChangeTracker) TrackShellCommandTurn added in v0.21.5

func (ct *ChangeTracker) TrackShellCommandTurn(workDir, command string, destructive bool)

TrackShellCommandTurn is TrackShellTurn for a shell_command run: entries keep the stable "shell_command" tool label, while the command text drives git-stash detection and names destructive rollup rows (so each rollup is distinguishable and individually recoverable).

func (*ChangeTracker) TrackShellTurn

func (ct *ChangeTracker) TrackShellTurn(workDir, toolCall string, destructive bool)

TrackShellTurn diffs the workspace against the primed baseline, records mutations, and rebases the baseline to the new state. Auto-primes if the cache hasn't been primed yet (no changes recorded first time). `destructive` enables the safer mode that bypasses autoSkipDirs.

func (*ChangeTracker) TrackerSessionID

func (ct *ChangeTracker) TrackerSessionID() string

TrackerSessionID exposes the tracker's current session identifier (the rotation contract: pkg/agent re-points it on session rotate and its test reads it back; SP-141 phase 2).

func (*ChangeTracker) TurnChangedPaths added in v0.23.0

func (ct *ChangeTracker) TurnChangedPaths() []string

TurnChangedPaths returns the distinct workspace paths of the changes recorded since the current turn window opened (MarkTurnStart), in first-seen order. It is non-destructive: the window is not consumed, so repeated calls and the separate checkpoint capture (CollectFileChangesForCheckpoint) are unaffected. Nil when the tracker is nil or disabled, or when the turn recorded no changes.

type CheckpointFileChange

type CheckpointFileChange struct {
	Path string `json:"path"`
	Op   string `json:"op"`
}

CheckpointFileChange is a single file-change entry in a turn checkpoint's manifest. Op is one of "A" (added), "M" (modified), "D" (deleted), "R" (renamed) to mirror git's status codes; anything else is "?" (other). pkg/agent aliases this type so the TurnCheckpoint JSON shape is unchanged (SP-141 phase 2).

type DebugLogger

type DebugLogger interface {
	Debug(format string, args ...interface{})
}

DebugLogger is the logger facet the tracker needs from the agent logger (mirrors *agent.AgentLogger.Debug).

type MessageAnnotation

type MessageAnnotation struct {
	Index         int           `json:"index"`
	Role          string        `json:"role"`
	Source        MessageSource `json:"source"`
	ContentChars  int           `json:"content_chars"`
	ToolCallCount int           `json:"tool_call_count,omitempty"`
	FirstLine     string        `json:"first_line,omitempty"`
}

MessageAnnotation is the per-message diagnostic view. Index aligns 1:1 with TranscriptSnapshot.State.Messages.

func AnnotateMessages

func AnnotateMessages(messages []api.Message) []MessageAnnotation

AnnotateMessages builds the per-message diagnostic annotations (exported for pkg/agent's BuildTranscriptSnapshot).

type MessageSource

type MessageSource string

MessageSource tags how a message arrived in the live conversation. It is the single most useful diagnostic field in a snapshot: it tells a reader whether what the model sees at index i is the user's original turn, a turn collapsed to a rule-based heuristic bullet list, or a structural summary produced by seed's LLM summarizer.

const (
	MessageSourceOriginal      MessageSource = "original"
	MessageSourceLLMCheckpoint MessageSource = "llm_checkpoint"
)

type ShellWalkConfig

type ShellWalkConfig struct {
	ShellWalkEnabled           bool
	MaxFiles                   int
	MaxTotalBytes              int64
	MaxDuration                time.Duration
	AutoSkipFileCountThreshold int
}

ShellWalkConfig carries the per-tracker shell-walk budgets and thresholds stamped from the change_tracking config section. ApplyShellWalkConfig is the single write path (formerly pkg/agent's applyChangeTrackingConfig reaching directly into unexported fields).

type TrackedBulkItem

type TrackedBulkItem struct {
	FilePath     string `json:"file_path"`
	OriginalCode string `json:"original_code"`
	NewCode      string `json:"new_code"`
	Operation    string `json:"operation"` // "create" | "edit" | "delete"
}

TrackedBulkItem is the per-file payload packed inside a bulk TrackedFileChange.

type TrackedFileChange

type TrackedFileChange struct {
	FilePath     string    `json:"file_path"`
	OriginalCode string    `json:"original_code"`
	NewCode      string    `json:"new_code"`
	Operation    string    `json:"operation"` // "write", "edit", "create", "delete", "bulk"
	Timestamp    time.Time `json:"timestamp"`
	ToolCall     string    `json:"tool_call"`

	// Source attributes a change to its origin. Empty for direct
	// primary-agent edits; "subagent:<persona>" for subagent changes.
	Source string `json:"source,omitempty"`

	// BulkCount is set on a rollup entry when a single shell command
	// churns more than the bulk threshold. FilePath names the directory
	// or command label and Operation is "bulk".
	BulkCount int `json:"bulk_count,omitempty"`

	// BulkItems carries the per-file recovery payload for bulk entries.
	BulkItems []TrackedBulkItem `json:"bulk_items,omitempty"`
}

TrackedFileChange represents a file change made during agent execution

type TranscriptFileChange

type TranscriptFileChange struct {
	Path      string    `json:"path"`
	Operation string    `json:"operation"`
	Source    string    `json:"source"`
	ToolCall  string    `json:"tool_call,omitempty"`
	Timestamp time.Time `json:"timestamp,omitempty"`
	BulkCount int       `json:"bulk_count,omitempty"`
}

TranscriptFileChange is the slim per-file projection embedded in a snapshot's top-level FileChanges field. It deliberately omits the full original/new file bodies that the ChangeTracker keeps for recovery — those can be multi-megabyte per file and would blow up snapshot size. The path, operation, and tool-call identifier give a reader enough to answer "what files were touched between snapshot A and snapshot B" without loading the bytes themselves.

Source distinguishes changes the primary agent made directly ("primary") from rollups parsed out of subagent tool results ("subagent"). The subagent's [subagent files modified] block is the authoritative per-call manifest, so the parser is a deterministic text scan rather than heuristic prose extraction.

func ExtractFileChangesFromMessages

func ExtractFileChangesFromMessages(messages []api.Message) []TranscriptFileChange

ExtractFileChangesFromMessages walks the supplied message slice and returns a deduped manifest of files touched, drawn from three authoritative sources: (1) tool_calls on assistant messages whose function name is a known file-write tool, (2) `[subagent files modified]` blocks embedded by tool_handlers_subagent in subagent tool results, and (3) `Files modified during compacted segment:` blocks that this package writes when /compact substitutes a summary for prior turns. The third source is what carries the manifest forward across successive compactions.

func FileChangesAdded

func FileChangesAdded(older, newer []TranscriptFileChange) []TranscriptFileChange

fileChangesAdded returns the entries present in newer but not in older, keyed by source+op+path. Used by /transcript diff to highlight what touched the filesystem between two snapshots. FileChangesAdded returns manifest entries present in newer but not older (exported for pkg/agent's DiffTranscriptSnapshots).

func MergeFileChanges

func MergeFileChanges(primary, secondary []TranscriptFileChange) []TranscriptFileChange

MergeFileChanges dedupes and concatenates primary+secondary manifest entries (exported for pkg/agent's BuildTranscriptSnapshot).

func TrackedChangesAsTranscript

func TrackedChangesAsTranscript(changes []TrackedFileChange) []TranscriptFileChange

trackedChangesAsTranscript projects the ChangeTracker's full TrackedFileChange records to the slim TranscriptFileChange shape and tags them as primary-source. The original/new bodies are intentionally dropped — they belong to the recovery flow, not to the diagnostic snapshot. TrackedChangesAsTranscript projects tracker changes to the slim manifest shape (exported for pkg/agent's BuildTranscriptSnapshot).

Jump to

Keyboard shortcuts

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