history

package
v0.16.21 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Audit logging for SP-077: tracks every write-back of OriginalCode (or NewCode) to the working tree. Each write-back is a potential source of silent committed-work reversion, so these audit lines include a stack trace to definitively identify the caller.

The logs use a distinctive [SP077-AUDIT] prefix for easy grepping. They are written to the standard logger so they show up in agent debug output without requiring verbose mode.

Quantity-based tiered compaction for the persistent revision store.

The revisions / changes directories grow unbounded over a project's lifetime — every Commit() writes a new revision dir + diff payloads. On a heavy project this is hundreds of MB per month and untenable over a year. The ChangeTracker is a short-horizon stop-gap (undo a bad sed -i, recover a hasty rm), not a long-term audit log — git is for that — so the compaction policy is correspondingly simple:

  • Hot (most recent HotCount revisions): kept verbatim
  • Warm (next WarmCount): conversation.json dropped
  • Dropped (everything older): deleted (or archived if ArchiveFrozen is enabled)

Position is by revision-directory mtime (newest first). The view tools (view_history) bump mtime when they access a revision so an old revision the user comes back to floats to the top automatically — the next compaction pass sees it as hot and keeps it (or warm, depending on its new position).

Index

Constants

View Source
const (
	RedColor             = "\x1b[31m"
	GreenColor           = "\x1b[32m"
	YellowColor          = "\x1b[33m"
	BoldStyle            = "\x1b[1m"
	ResetColor           = "\x1b[0m"
	NumberOfContextLines = 3 // Number of context lines to show around changes
)

Color constants for better readability

View Source
const RedactedContentMarker = "[REDACTED - external file]"

RedactedContentMarker is the canonical marker used when file content is redacted because the file is outside the workspace root (to avoid leaking sensitive data). It is defined here, in the lower-level history package, so both pkg/history and pkg/agent reference a single source of truth instead of maintaining duplicate copies that can silently drift. pkg/agent references this via history.RedactedContentMarker.

Variables

This section is empty.

Functions

func AuditRevertSkip added in v0.16.18

func AuditRevertSkip(caller, path, reason string)

AuditRevertSkip logs when a staleness guard refuses a write-back. Useful for correlating how many reverts were blocked vs. how many went through, and confirming the guards are firing.

func AuditRevertWrite added in v0.16.18

func AuditRevertWrite(caller, path, contentType string)

AuditRevertWrite logs a write-back of tracked content (OriginalCode or NewCode) to the working tree. Called immediately before every os.WriteFile / filesystem.SaveFile in the rollback/recovery paths.

`caller` identifies the function performing the write (e.g. "handleRevisionRollback", "revertOne"). `path` is the absolute or relative filesystem path being written. `contentType` is "OriginalCode" or "NewCode" so the log distinguishes reverts from restores.

The stack trace captures the full call chain — this is the critical piece for diagnosing whether the write was triggered by an LLM tool call, a CLI command, a test, or an unexpected automatic path.

func ClearAll

func ClearAll(workspace string) (changesCleared int, revisionsCleared int, err error)

ClearAll removes all change entries and all revision directories. If workspace is non-empty, it operates on that workspace's .sprout directory. If workspace is empty, it uses the globally configured paths. Returns the number of changes cleared, revisions cleared, and any error.

func ClearOlderThan

func ClearOlderThan(workspace string, since time.Time) (changesCleared int, revisionsCleared int, err error)

ClearOlderThan removes all change entries and revision directories where the change timestamp is strictly before 'since'. If workspace is non-empty, it operates on that workspace's .sprout directory. If workspace is empty, it uses the globally configured paths. Returns the number of changes cleared, revisions cleared, and any error.

func GetChangedFilesSince

func GetChangedFilesSince(since time.Time) ([]string, error)

GetChangedFilesSince returns a unique list of filenames changed after the given time.

func GetChangesDir

func GetChangesDir() string

GetChangesDir returns the current changes directory path

func GetDiff

func GetDiff(filename, originalCode, newCode string) string

func GetFilesForRevision

func GetFilesForRevision(revisionID string) ([]string, error)

HasActiveChangesForRevision returns whether a revision ID exists and has any active changes GetFilesForRevision returns the file paths of all active changes in a revision. Returns an empty slice if the revision is not found or has no active changes.

func GetRevisionsDir

func GetRevisionsDir() string

GetRevisionsDir returns the current revisions directory path

func HasActiveChangesForRevision

func HasActiveChangesForRevision(revisionID string) (bool, error)

func InitializeHistoryPaths

func InitializeHistoryPaths(config *configuration.Config)

InitializeHistoryPaths configures the history storage paths based on configuration This should be called at application startup to ensure correct path resolution

func IsChangeOlderThan

func IsChangeOlderThan(metadataPath string, since time.Time) bool

IsChangeOlderThan reads a change's metadata.json and returns true if the change's timestamp is strictly before 'since'. Returns false if the file cannot be read or parsed.

func IsRevertSafe added in v0.16.18

func IsRevertSafe(filename, newCode string) bool

IsRevertSafe reports whether it is SAFE to proceed with a revert that writes OriginalCode back to disk. It is the canonical git-aware staleness guard for ALL rollback/revert paths (the history package's handleRevisionRollback, the agent_tools RollbackChanges single-file path, and the agent package's recover_file / revert_my_changes).

It returns true (safe to proceed) when the revert will NOT clobber intentional work, and false (must skip) when it would. The decision layers two checks:

  1. Content-identity: if the file on disk no longer matches the content the agent wrote (newCode), it was modified intentionally after the snapshot — return false (stale). Empty or redacted newCode, or a missing file, means there's no baseline to compare against, so the content check is skipped (return true).

  2. Git-awareness (NEW): even when disk == newCode, the agent's edit may have since been committed to git. Writing OriginalCode back would silently undo committed, version-controlled work. If the working-tree copy matches HEAD (committed, clean), return false (protected). A git error (e.g. not a repo, or untracked file) means no git protection applies, so the content check alone decides — return true.

The function never blocks legitimate reverts: outside a git repo, on untracked files, or when the file has uncommitted modifications, the content check is the sole authority.

func IsRevertSafeWithOriginal added in v0.16.18

func IsRevertSafeWithOriginal(filename, newCode, originalCode string) bool

IsRevertSafeWithOriginal is the full-aware staleness guard used by recovery paths that have the OriginalCode (the content to be written back). The original-aware path allows recovery when the file on disk matches HEAD (a destructive git command aligned it to HEAD) but the OriginalCode is NOT the HEAD content — meaning the original was uncommitted work that the destructive command destroyed. Restoring it does NOT undo committed work; it restores destroyed work.

func MarkChangeSuperseded added in v0.16.18

func MarkChangeSuperseded(fileRevisionHash string) error

MarkChangeSuperseded marks a change record as "superseded" — the change has been committed to version control and is no longer a recoverable agent edit. This is used by the SP-077 sweep in ChangeTracker.Commit() to prevent old snapshots from being reverted after their content has been committed to git HEAD.

func PrintDiff

func PrintDiff(filename, originalCode, newCode string)

func PrintRevisionHistory

func PrintRevisionHistory() error

func PrintRevisionHistoryBuffer

func PrintRevisionHistoryBuffer() (string, error)

PrintRevisionHistoryBuffer displays the revision history to a buffer for seamless console experience

func PrintRevisionHistoryWithReader

func PrintRevisionHistoryWithReader(inputReader *bufio.Reader) error

PrintRevisionHistoryWithReader allows custom input reader for interactive navigation

func RecordBaseRevision

func RecordBaseRevision(requestHash, instructions, response string, conversation []APIMessage) (string, error)

RecordBaseRevision saves the initial request and response, returning a revision ID. conversation is the full conversation history (all user/assistant/tool messages)

func RecordChange

func RecordChange(baseRevisionID string, filename, originalCode, newCode, description, note string) error

RecordChange saves a specific file change against a base revision.

func RecordChangeWithDetails

func RecordChangeWithDetails(baseRevisionID string, filename, originalCode, newCode, description, note string, originalPrompt string, llmMessage string, editingModel string) error

RecordChangeWithDetails saves a specific file change against a base revision with additional details.

func RevertChangeByRevisionID

func RevertChangeByRevisionID(revisionID string) error

RevertChangeByRevisionID reverts all changes associated with a given revision ID.

func TouchRevision

func TouchRevision(revisionID string) error

TouchRevision bumps the revision directory's mtime to now. Called when view_history accesses a revision so the next compaction pass considers it "recently used" and keeps it (or re-promotes it from warm back toward hot) regardless of its position in raw creation order. No-op if the revision dir doesn't exist (already dropped).

Types

type APIMessage

type APIMessage struct {
	Role             string        `json:"role"`
	Content          string        `json:"content"`
	ReasoningContent string        `json:"reasoning_content,omitempty"`
	ToolCallID       string        `json:"tool_call_id,omitempty"`
	ToolCalls        []APIToolCall `json:"tool_calls,omitempty"`
}

APIMessage represents a message in the conversation (imported from agent_api to avoid circular dependency)

type APIToolCall

type APIToolCall struct {
	ID       string `json:"id"`
	Type     string `json:"type"`
	Function struct {
		Name      string `json:"name"`
		Arguments string `json:"arguments"`
	} `json:"function"`
}

APIToolCall represents a tool call in a message

type ChangeLog

type ChangeLog struct {
	RequestHash      string
	Instructions     string
	Response         string
	FileRevisionHash string
	Filename         string
	OriginalCode     string
	NewCode          string
	Description      string
	Note             sql.NullString
	Status           string
	Timestamp        time.Time
	OriginalPrompt   string // Added: Original user prompt
	LLMMessage       string // Added: Full message sent to LLM
	AgentModel       string // Added: Editing model used
	HasConversation  bool   // Added: Whether conversation.json exists for this revision
	// Tier reflects the revision's compaction state: "hot" (full data
	// including conversation.json) or "warm" (conversation.json
	// dropped). Empty string is treated as hot for backward compat.
	Tier string
}

ChangeLog represents a logged change, including context from the base revision.

func GetAllChanges

func GetAllChanges() ([]ChangeLog, error)

GetAllChanges returns all recorded changes (most recent first).

func GetAllChangesMetadata added in v0.16.12

func GetAllChangesMetadata() ([]ChangeLog, error)

GetAllChangesMetadata returns change metadata WITHOUT reading or base64-decoding the .original/.updated content files. This is the lightweight alternative to GetAllChanges for callers that only need the manifest fields (filename, revision, timestamp, status, tier) — primarily list_changes when include_diff/show_content aren't set.

The OriginalCode and NewCode fields of the returned ChangeLog entries are left EMPTY. Callers that infer op/recoverability from content presence should instead use HasOriginal/HasNew, which report whether the content files exist on disk (a cheap os.Stat, not a read+decode). This avoids the O(total-history) base64 decode that fetchAllChanges performs on every list_changes invocation.

func GetChangesSince

func GetChangesSince(since time.Time) ([]ChangeLog, error)

GetChangesSince returns changes whose timestamp is strictly after the provided time.

type ChangeMetadata

type ChangeMetadata struct {
	Version          int       `json:"version"`
	Filename         string    `json:"filename"`
	FileRevisionHash string    `json:"file_revision_hash"`
	RequestHash      string    `json:"request_hash"` // This is the revision ID
	Timestamp        time.Time `json:"timestamp"`
	Status           string    `json:"status"`
	Note             string    `json:"note"`
	Description      string    `json:"description"`
	OriginalPrompt   string    `json:"original_prompt,omitempty"` // Added: Original user prompt
	LLMMessage       string    `json:"llm_message,omitempty"`     // Added: Full message sent to LLM
	AgentModel       string    `json:"agent_model,omitempty"`     // Added: Editing model used
}

ChangeMetadata stores metadata about a specific file change.

type CompactionStats

type CompactionStats struct {
	TotalRevisions         int
	HotKept                int
	WarmDemoted            int // revisions moved hot→warm or already warm
	Dropped                int // revisions moved out of warm → deleted/archived
	ChangesPayloadsDeleted int
	BytesReclaimed         int64
	HardCapTrimmed         int
	OrphanChangesDropped   int
	OverCapChangesDropped  int
	AgedChangesDropped     int
}

CompactionStats reports what a single CompactRevisions pass did. Useful for logs / metrics; not consumed by anything load-bearing.

func CompactRevisions

func CompactRevisions(policy RetentionPolicy) (CompactionStats, error)

CompactRevisions runs one compaction pass over the configured revisions directory according to the given policy. Safe to call concurrently from multiple agents (mutex-serialized). Idempotent: repeated calls are no-ops once revisions are in their target tier.

Returns stats for logging; errors are non-fatal at the call site (caller should log and continue — a failed compaction just means disk usage stays where it was, nothing breaks).

type RetentionPolicy

type RetentionPolicy struct {
	HotCount      int
	WarmCount     int
	MaxDirBytes   int64
	ArchiveFrozen bool

	// MaxChangesPerRevision caps the per-revision change-record count
	// in the changes/ directory. A single runaway session can produce
	// tens of thousands of records (e.g. when the agent `cd`s into
	// $HOME and a shell walk misclassifies pre-existing files as
	// creates). Without this cap, count-based bloat persists even
	// when total bytes are under MaxDirBytes. Zero disables.
	MaxChangesPerRevision int

	// MaxChangesAge drops change records older than this regardless of
	// their parent revision's tier. Belt-and-suspenders against
	// changes/ growing unbounded inside the hot window. Zero disables.
	MaxChangesAge time.Duration
}

RetentionPolicy is the subset of RevisionRetentionConfig the compactor needs. Kept separate to avoid a cycle with pkg/configuration.

type RevisionGroup

type RevisionGroup struct {
	RevisionID   string
	Instructions string
	Response     string
	Changes      []ChangeLog
	Timestamp    time.Time
	AgentModel   string       // Editing model used for this revision
	Conversation []APIMessage // Full conversation history for multi-turn conversations
}

RevisionGroup represents a group of changes that belong to the same revision

func GetRevisionGroups

func GetRevisionGroups() ([]RevisionGroup, error)

GetRevisionGroups returns all revision groups sorted by timestamp (most recent first)

Jump to

Keyboard shortcuts

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