Documentation
¶
Overview ¶
Package memory implements the agent-facing operations: BuildContextPack (M2 T2.7), and — added later — the structured update pipeline (M3) and staging engine (M5).
fetch.go assembles the Markdown context pack that memory.fetch_context returns. The algorithm follows design doc v0.4.1 §20.5:
- Empty query → bootstrap pack (current.<branch>.md + current.shared.md + conventions.md + a compact index.md summary).
- Non-empty query → Index.Search + ranking + section-level assembly, with the bootstrap files always prepended.
Budget enforcement is greedy: sections are appended in ranked order until the running character total would exceed Budget, then the remaining sections move to the Omitted list.
sync.go materialises referenced "landscape" stores (federation, PR3) into the rebuildable cache under .agent-memory/meta/cache/stores/<name>/ and pins each to a resolved commit in meta/stores.lock. The pipeline per store is:
clone (or local-path copy) → validate it is an agent-memory store → sandbox-validate (no symlinks, contained) → secret/PII scan → swap into the cache → record in the lock
The committed lock is authoritative: a store already pinned at a commit is re-materialised at THAT commit on every sync (reproducible landscape memory), not silently re-pinned to a moving branch. Pass Update to deliberately move a pin forward to the latest of its requested revision. External content is treated as untrusted (its own allowlist markers do NOT exempt it from scanning). The index store dimension lands in PR4, so sync does not rebuild the index here.
Index ¶
- Constants
- Variables
- func AppendRejection(memDir string, entry RejectionEntry) error
- func BuildIndexContent(memDir string, sch *schema.Schema) ([]byte, error)
- func CheckAllowlistLimits(regions []AllowlistRegion, limits AllowlistLimits) string
- func ClassifyFindings(findings []Finding) string
- func IsAllowlisted(start, end int, regions []AllowlistRegion) bool
- func IsValidConfidence(c string) bool
- func IsValidIntent(i Intent) bool
- func RegenerateIndex(memDir string, sch *schema.Schema) (bool, error)
- func ResolveStagingID(memDir, ref string) (string, error)
- func StagingExists(memDir, stagingID string) bool
- func ValidateProvenance(policy schema.Provenance, ctx ProvenanceContext) []string
- type AffectedSection
- type AllowlistLimits
- type AllowlistRegion
- type AppendSection
- type AppendToSection
- type ApplyResult
- type ArchiveSection
- func (op *ArchiveSection) ExtraFiles(src []byte) ([]ExtraFile, error)
- func (op *ArchiveSection) Kind() string
- func (op *ArchiveSection) Path() string
- func (op *ArchiveSection) Plan(src []byte) (agentmd.SpliceOp, error)
- func (op *ArchiveSection) Targets() []OperationTarget
- func (op *ArchiveSection) Validate(sch *schema.Schema) error
- type AutoStageResult
- type ContextMetadata
- type CreateFile
- type DriftPolicy
- type DriftReport
- type ErrAmbiguousPrefix
- type ExpiredProposal
- type ExtraFile
- type ExtraFileProducer
- type FetchDeps
- type FetchRequest
- type FetchResponse
- type Finding
- type IncludedFile
- type Intent
- type MemoryStatus
- type MemoryStatusGit
- type MemoryStatusLock
- type MemoryStatusSecurity
- type OmittedFile
- type Operation
- type OperationInput
- type OperationTarget
- type OwnerInfo
- type ProposeRequest
- type ProposeResponse
- type ProvenanceContext
- type RebaseResult
- type RejectionEntry
- type RemoveSection
- func (op *RemoveSection) ExtraFiles(src []byte) ([]ExtraFile, error)
- func (op *RemoveSection) Kind() string
- func (op *RemoveSection) Path() string
- func (op *RemoveSection) Plan(src []byte) (agentmd.SpliceOp, error)
- func (op *RemoveSection) Targets() []OperationTarget
- func (op *RemoveSection) Validate(sch *schema.Schema) error
- type RenameHeading
- type ReplaceSection
- type ReplaceSectionContent
- type Routing
- type ScanOpts
- type Source
- type StagedProposal
- type StagedStatusEntry
- type StatusDeps
- type StoreRef
- type StoreSyncResult
- type SweepResult
- type SyncDeps
- type UpdateDeps
Constants ¶
const ( StatusRebased = "rebased" StatusSkippedClean = "skipped_clean" )
Status values for RebaseResult. StatusRejected is reused from the orchestrator's vocabulary (rebase rejection uses the same exit-code semantics as a propose_update rejection).
const ( ReasonForceRequired = "force_required" ReasonUnresolvableDrift = "unresolvable_drift" ReasonRebasePlanFailed = "rebase_plan_failed" ReasonRebaseSecret = "rebase_secret_detected" ReasonRebasePIIDetected = "rebase_pii_detected" ReasonRebaseInvalidMD = "rebase_invalid_markdown" )
Reason codes for RebaseResult. Stable wire identifiers — CLI / tests match against them.
const ( RejectionReasonUser = "user_rejected" RejectionReasonTTLExpired = "ttl_expired" )
RejectionReason values are the wire-stable strings written to the audit log's `reason` field. Stable so log scrapers can filter on them.
const ( StatusApplied = "applied" StatusStaged = "staged" StatusRejected = "rejected" )
Status values returned in ProposeResponse.Status.
const ( ReasonInvalidIntent = "invalid_intent" ReasonInvalidOperation = "invalid_operation" ReasonNoOperations = "no_operations" ReasonValidationFailed = "validation_failed" ReasonInvalidPath = "invalid_path" ReasonUnknownCategory = "unknown_category" ReasonServerManagedCategory = "server_managed_category" ReasonReadError = "read_error" ReasonPlanFailed = "plan_failed" ReasonSpliceFailed = "splice_failed" ReasonInvalidMarkdown = "invalid_markdown" ReasonAllowlistParseError = "allowlist_parse_error" ReasonSecretDetected = "secret_detected" ReasonProvenanceViolation = "provenance_violation" ReasonServerOnlyCategory = "server_only_category" ReasonLockHeld = "lock_held" ReasonTargetDrift = "target_drift" ReasonStagingNotFound = "staging_not_found" ReasonAllowlistLimitExceeded = "allowlist_limit_exceeded" ReasonPIIDetected = "pii_detected" ReasonWriteOnceViolation = "write_once_violation" ReasonArchiveExists = "archive_exists" )
Reject reason codes. These are part of the propose_update wire contract: callers (CLI, MCP tool consumers, evals) match against them. Add a new code rather than reusing an existing one when introducing a new failure mode.
const LatestRef = "--latest"
LatestRef is the sentinel ResolveStagingID accepts to mean "the most recently staged proposal". Staging IDs start with a UTC timestamp, so the lexically-largest id is the newest.
const PIIPrefix = "pii_"
PIIPrefix is the prefix every PII Finding.Type carries. Classifiers use it to split mixed scan results into "credential present" vs "only PII".
Variables ¶
var AllIntents = []Intent{ IntentUpdateCurrent, IntentUpdateShared, IntentSessionLog, IntentAddPitfall, IntentRecordDecision, IntentRefreshModule, IntentUpdateConventions, IntentArchiveStale, }
AllIntents is the canonical list used by Validate / docs.
var ConfidenceLevels = []string{
"confirmed",
"inferred",
"user-provided",
"stale",
"unknown",
}
ConfidenceLevels are the allowed values for the Confidence field on a propose_update input (design doc §23.5). Empty Confidence is also accepted — the orchestrator treats it as "unknown".
var ErrNoStaged = errors.New("no matching staged proposal")
ErrNoStaged is returned by ResolveStagingID when no staged proposal matches the reference (empty queue, or no prefix match).
var SourceTypes = []string{
"file", "test", "user", "session", "inference", "external",
}
SourceTypes are the canonical source-type strings. Used as a hint; the per-category policy in schema.Provenance can narrow this further.
Functions ¶
func AppendRejection ¶
func AppendRejection(memDir string, entry RejectionEntry) error
AppendRejection serialises entry as one JSON object on its own line and appends it to meta/rejection-log.jsonl, creating the file (and the parent meta/ directory) on demand. Append is O_APPEND so concurrent writes within a single process get atomic per-line semantics from the OS; the in-process mutex above keeps the field order stable.
Best-effort: a write failure does NOT cause the calling reject / sweep to fail. The bytes-on-disk reality is the staging dir being gone; the log is a downstream audit channel.
func BuildIndexContent ¶
BuildIndexContent walks memDir and produces the index.md routing file per design §10.1. The output is DETERMINISTIC for a given tree — no wall-clock timestamps in the body — so regeneration only changes the file when the underlying memory actually changed, avoiding needless git churn and keeping tests stable.
Sections produced:
# Agent Memory Index + @generated comment ## Always include — local current state + conventions ## Topic map — decisions (by status), pitfalls (count), modules ## Archive — archived-context count ## Freshness — placeholder until per-section freshness lands
func CheckAllowlistLimits ¶
func CheckAllowlistLimits(regions []AllowlistRegion, limits AllowlistLimits) string
CheckAllowlistLimits verifies regions against limits. Returns an empty string when all checks pass; a human-readable description of the first hit limit otherwise. The orchestrator passes this string through as the rejection Message.
Limits are checked in this order (lowest-impact wins for the error message; if both region-count and total-bytes are exceeded, the region-count message is what the user sees first):
- MaxRegionsPerFile — count of regions
- MaxBytesPerRegion — single largest region
- MaxBytesPerFile — sum across all regions
func ClassifyFindings ¶
ClassifyFindings inspects a mixed Findings slice and returns the appropriate reject reason: ReasonSecretDetected if ANY finding is a credential (Type doesn't start with PIIPrefix), ReasonPIIDetected otherwise. Used by the orchestrator + rebase paths so callers don't have to walk the slice manually.
An empty slice returns "" — caller checks len() first.
func IsAllowlisted ¶
func IsAllowlisted(start, end int, regions []AllowlistRegion) bool
IsAllowlisted reports whether the byte range [start, end) is fully contained in any of regions.
func IsValidConfidence ¶
IsValidConfidence reports whether c is one of ConfidenceLevels or empty.
func IsValidIntent ¶
IsValidIntent reports whether i is a recognised Intent.
func RegenerateIndex ¶
RegenerateIndex rebuilds index.md and writes it atomically IF the new content differs from what's on disk. Returns whether the file changed.
Best-effort by contract: callers in the apply path ignore the error (the durable bytes already landed; a stale index can be rebuilt via `agent-memory rebuild-index`). The "changed" return lets the apply path decide whether to include index.md in a git auto-stage batch.
func ResolveStagingID ¶
ResolveStagingID turns a user-supplied reference into a full staging ID. Accepts:
- LatestRef ("--latest") or "latest" → the newest staged proposal.
- an exact staging ID → returned as-is (even if it's also a prefix of others).
- a unique prefix (Git-style) → the single matching ID.
Errors: ErrNoStaged (no match / empty queue), *ErrAmbiguousPrefix (more than one prefix match).
func StagingExists ¶
StagingExists reports whether staging/<stagingID>/ is a directory under memDir. Cheap probe used by review/apply/reject before doing heavier work.
func ValidateProvenance ¶
func ValidateProvenance(policy schema.Provenance, ctx ProvenanceContext) []string
ValidateProvenance checks a ProvenanceContext against a category's Provenance policy from the schema. Returns a slice of human-readable violations; an empty slice means the proposal satisfies the policy.
Checks (in order):
- Confidence is one of ConfidenceLevels (or empty).
- Every Source.Type is in schema's AllowedSourceTypes (when set).
- No Source.Type is in schema's ForbiddenSourceTypes.
- If Required → at least one Source must be present.
- If RequiredForNewSections AND ctx.IsNewSection → at least one Source.
Types ¶
type AffectedSection ¶
type AffectedSection struct {
File string `json:"file"`
SectionID string `json:"section_id,omitempty"`
}
AffectedSection identifies one (file, section) touched by an applied proposal. Part of the design §15.2 Applied output.
type AllowlistLimits ¶
type AllowlistLimits struct {
MaxBytesPerFile int // sum of bytes inside all regions
MaxRegionsPerFile int // count of regions
MaxBytesPerRegion int // single largest region
}
AllowlistLimits caps how much of a file's content can sit inside secret-scanner allowlist regions. The allowlist is a per-region escape hatch for documenting token FORMATS (a few dozen bytes per example). Without a cap, a careless or malicious agent could wrap a 5 KB region around a real credential and bypass the scanner.
A field set to 0 means "unlimited for that dimension" (the orchestrator skips the corresponding check). Defaults shipped via manifest.security.allowlist_limits: see config.DefaultManifest.
type AllowlistRegion ¶
type AllowlistRegion struct {
ByteStart int `json:"byte_start"`
ByteEnd int `json:"byte_end"`
Reason string `json:"reason"`
}
AllowlistRegion is a byte range [ByteStart, ByteEnd) in scanned content that the secret scanner must skip. Produced by ExtractAllowlistRegions from paired HTML-comment markers in Markdown source.
func ExtractAllowlistRegions ¶
func ExtractAllowlistRegions(content []byte) ([]AllowlistRegion, error)
ExtractAllowlistRegions scans content for paired allowlist markers and returns the byte ranges between them.
The marker format (verbatim):
<!-- @secret-scan: allow reason="documentation example" --> ...allowed content... <!-- @secret-scan: end -->
Region boundaries: ByteStart is the offset right after the opening marker's '>' character; ByteEnd is the offset of the closing marker's '<' character. The markers themselves are NOT inside the allowed range (so a secret that happened to live in the comment text would still be flagged — but markers don't look like secrets in practice).
Errors:
- open marker without a closing one in scope
- close marker with no preceding open
- nested open (a second open before the first has closed)
- empty reason on an open marker
An empty input or input without any markers returns nil regions and nil error.
type AppendSection ¶
type AppendSection struct {
FilePath string
ParentSectionID string
Heading string
Level int
Content []byte
}
AppendSection adds a new section to the file. If ParentSectionID is set, the new section is inserted at the "first child slot" of that parent — the position of the parent's first existing child (a section inside the parent's byte range with strictly greater HeadingLevel). When the parent has no children, the insert falls back to parent.ByteEnd (just before the parent's next sibling/ancestor heading or EOF).
Without ParentSectionID the new section is appended at EOF.
Why the "first child slot" semantic: a level-1 parent subsumes ALL subsequent sections until the next level-1 heading (or EOF). Inserting at parent.ByteEnd would put the new child AFTER every other section in the file, which is rarely what the agent intends. Inserting before the first existing child keeps the new section visually "at the top of the parent's contents" — matching how a human would add a sub-section to a chapter.
Content must start with the heading line. The orchestrator's AssignMissingIDs pass will inject an @id anchor on the next index pass if Content doesn't carry one.
func (*AppendSection) Kind ¶
func (op *AppendSection) Kind() string
func (*AppendSection) Path ¶
func (op *AppendSection) Path() string
func (*AppendSection) Targets ¶
func (op *AppendSection) Targets() []OperationTarget
type AppendToSection ¶
type AppendToSection struct {
FilePath string
SectionID string
Heading string
Level int
Occurrence int
Content []byte
}
AppendToSection appends Content to the END of an existing section (just before the next heading at same/higher level). Heading stays untouched. Used for bullet-level entries (pitfalls, session logs).
func (*AppendToSection) Kind ¶
func (op *AppendToSection) Kind() string
func (*AppendToSection) Path ¶
func (op *AppendToSection) Path() string
func (*AppendToSection) Plan ¶
func (op *AppendToSection) Plan(src []byte) (agentmd.SpliceOp, error)
func (*AppendToSection) Targets ¶
func (op *AppendToSection) Targets() []OperationTarget
type ApplyResult ¶
type ApplyResult struct {
StagingID string `json:"staging_id"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
Files []string `json:"files,omitempty"`
Drift []DriftReport `json:"drift,omitempty"`
// AutoStage carries git auto-stage / commit outcomes when the apply
// produced writes AND manifest.git.auto_stage_changes is true. nil
// on rejection; nil on success when the feature is disabled.
AutoStage *AutoStageResult `json:"auto_stage,omitempty"`
}
ApplyResult is what ApplyStaged returns. Status is one of StatusApplied / StatusRejected. On rejection, Reason is a stable code:
- ReasonStagingNotFound — no staging/<id>/ directory.
- ReasonTargetDrift — the disk state changed since stage.
- ReasonLockHeld — another writer holds the advisory lock.
func ApplyStaged ¶
func ApplyStaged(ctx context.Context, stagingID string, deps UpdateDeps) (res *ApplyResult, err error)
ApplyStaged applies a staged proposal:
- Acquires the cross-process advisory lock (same lock ProposeUpdate uses).
- Loads proposal.json + target-checksums.json.
- CheckDrift on every target; any drift → ApplyResult{Status: rejected, Reason: target_drift, Drift: [...]} (no I/O).
- Reads each staged file under staging/<id>/files/ and WriteAtomics it to its destination under memDir.
- Re-indexes touched files (best-effort; failures don't roll back).
- Removes staging/<id>/ from disk.
Errors only for infrastructure failures (lock open, JSON parse fatal, destination write). Application-level rejections (drift, missing staging dir, lock held) come back in ApplyResult, NOT as Go errors. This mirrors ProposeUpdate's contract so the MCP wrapper can stay simple.
func RejectStaged ¶
func RejectStaged(memDir, stagingID string) (*ApplyResult, error)
RejectStaged removes staging/<stagingID>/ from disk and appends an audit entry to meta/rejection-log.jsonl with reason="user_rejected". Cheap; no drift checks, no lock acquisition (the proposal isn't touching any other memory file).
Returns ReasonStagingNotFound through the ApplyResult (NOT as a Go error) when the directory doesn't exist — symmetric with ApplyStaged's contract. Real filesystem errors during removal propagate as Go errors.
The rejection log write is best-effort: a logging failure doesn't undo the removal. See sweep.go for the shared rejectStagedWithReason helper that also serves the TTL sweeper.
type ArchiveSection ¶
type ArchiveSection struct {
FilePath string
SectionID string
Heading string
Level int
Occurrence int
ArchivePath string
Replacement []byte // new content for the source section (heading + anchor + stub)
}
ArchiveSection copies a section's current content to a new archive file and replaces the source section (heading included) with a pointer/stub Replacement. The archive file is write-once: it must not already exist. Per design §15.8, archiving never destroys content and always stages.
This is a MULTI-FILE operation: Plan() handles the source-file splice, and ExtraFiles() produces the new archive file. The orchestrator wires both together.
func (*ArchiveSection) ExtraFiles ¶
func (op *ArchiveSection) ExtraFiles(src []byte) ([]ExtraFile, error)
ExtraFiles copies the section's current bytes (heading included) into the archive file. src is the source file's bytes BEFORE this op's splice, so the section is still present.
func (*ArchiveSection) Kind ¶
func (op *ArchiveSection) Kind() string
func (*ArchiveSection) Path ¶
func (op *ArchiveSection) Path() string
func (*ArchiveSection) Targets ¶
func (op *ArchiveSection) Targets() []OperationTarget
type AutoStageResult ¶
type AutoStageResult struct {
// Staged is the list of repo-root-relative paths git add was called
// with. Forward slashes.
Staged []string `json:"staged,omitempty"`
// CommitSHA is non-empty when auto_commit was on AND a commit was
// actually created (nothing staged ≠ commit).
CommitSHA string `json:"commit_sha,omitempty"`
// Errors, if any. Auto-stage NEVER fails an apply — errors are
// collected for surfacing only. Bytes are durable; git is a
// best-effort side channel.
Errors []string `json:"errors,omitempty"`
// Skipped is true when auto-stage logic ran but produced no work
// (manifest disabled, not-a-repo, nothing tracked). Distinguishes
// "ran and did nothing" from "didn't run".
Skipped bool `json:"skipped,omitempty"`
}
AutoStageResult reports what auto-stage did. Returned through the apply paths (Direct apply and ApplyStaged) so the CLI can surface it in its human + JSON output. Empty / zero-valued when auto-stage is disabled or the project isn't a git repo.
type ContextMetadata ¶
type ContextMetadata struct {
ActiveBranch string `json:"active_branch,omitempty"`
BudgetUsed int `json:"budget_used"`
BudgetRemaining int `json:"budget_remaining"`
StaleWarnings []string `json:"stale_warnings,omitempty"`
}
ContextMetadata is sidecar info the agent can use to decide follow-up behaviour without re-fetching.
type CreateFile ¶
CreateFile creates a new file with Content at FilePath. IfExists controls what to do when the file already exists at apply time:
"reject" (default) — fail; the orchestrator emits "target_drift". "append" — append Content to the existing file. "replace" — overwrite the file with Content.
func (*CreateFile) Kind ¶
func (op *CreateFile) Kind() string
func (*CreateFile) Path ¶
func (op *CreateFile) Path() string
func (*CreateFile) Targets ¶
func (op *CreateFile) Targets() []OperationTarget
type DriftPolicy ¶
type DriftPolicy int
DriftPolicy controls how the staging engine (M5) re-validates an operation's target at apply time. See design doc v0.4.1 §16.4.
const ( // RequireSectionContentMatch — used by replace/archive/remove/rename // where the operation is only meaningful against a specific snapshot // of the target section. Drift = section's content hash changed. RequireSectionContentMatch DriftPolicy = iota // RequireSectionResolvable — used by append_to_section. The section // may have grown since staging; we only require it to still resolve // by its ID. RequireSectionResolvable // RequireFileAbsent — used by create_file with if_exists=reject. // Drift = file appeared between stage and apply. RequireFileAbsent // RequireFilePresent — used by create_file with if_exists=append/replace, // and by append_section against a parent_section_id. RequireFilePresent )
func (DriftPolicy) MarshalJSON ¶
func (p DriftPolicy) MarshalJSON() ([]byte, error)
MarshalJSON renders DriftPolicy as its String() form, not as the integer iota value. Staged targets must be human-readable on disk.
func (DriftPolicy) String ¶
func (p DriftPolicy) String() string
String renders the DriftPolicy as a stable snake_case identifier used in staged target-checksums.json and CLI output. Keep these values stable — the M5 apply path matches against them.
func (*DriftPolicy) UnmarshalJSON ¶
func (p *DriftPolicy) UnmarshalJSON(b []byte) error
UnmarshalJSON reverses MarshalJSON for the M5 apply path, which round- trips target-checksums.json off disk. An unknown identifier is an error — silently mapping it to the zero value would mask staging-file corruption.
type DriftReport ¶
type DriftReport struct {
Path string `json:"path"`
SectionID string `json:"section_id,omitempty"`
Policy string `json:"policy"`
Expected string `json:"expected"`
Found string `json:"found"`
}
DriftReport describes a single target whose current disk state no longer matches the snapshot recorded at stage time. Surfaced verbatim to humans (review/apply CLI output) and to agents (response JSON) so they can decide between re-staging and abandoning.
func CheckDrift ¶
func CheckDrift(memDir string, t OperationTarget) (*DriftReport, error)
CheckDrift re-validates one OperationTarget against the current disk state of memDir. Returns:
- (nil, nil) → no drift; safe to apply.
- (*DriftReport, nil) → drift detected; report describes the diff.
- (nil, err) → I/O failure unrelated to drift; caller decides.
Drift semantics by policy:
RequireSectionContentMatch: section must resolve by ID and its current ContentHash must match the stored Hash. A missing file / missing section / changed hash all count as drift. RequireSectionResolvable: section must resolve by ID. Hash is ignored. A missing file / missing section count as drift. RequireFileAbsent: the file must not exist. A present file is drift. RequireFilePresent: the file must exist. An absent file is drift.
type ErrAmbiguousPrefix ¶
ErrAmbiguousPrefix is returned when a prefix matches more than one staged proposal. Its message lists the candidates so the caller can disambiguate.
func (*ErrAmbiguousPrefix) Error ¶
func (e *ErrAmbiguousPrefix) Error() string
type ExpiredProposal ¶
type ExpiredProposal struct {
StagingID string `json:"staging_id"`
Intent string `json:"intent,omitempty"`
Rationale string `json:"rationale,omitempty"`
StagedAt string `json:"staged_at,omitempty"`
AgeSeconds int `json:"age_seconds"`
}
ExpiredProposal is the per-entry shape inside SweepResult.Expired. Keeps just enough metadata for humans to recognise what got removed without re-reading staging/<id>/proposal.json (which is already gone).
type ExtraFile ¶
type ExtraFile struct {
// Path is the forward-slash, memory-relative destination.
Path string
// Content is the full bytes to write. The orchestrator treats the
// file as new (RequireFileAbsent) and write-once.
Content []byte
}
ExtraFile is an additional file an operation produces beyond its primary Path() target. archive_section / remove_section use this to copy the archived section content into a brand-new archive/ file.
type ExtraFileProducer ¶
type ExtraFileProducer interface {
// ExtraFiles computes the additional files this op creates, derived
// from src — the primary file's bytes at the moment the op runs,
// BEFORE its own splice is applied. (archive_section reads the
// section it's about to replace; remove_section reads the section it's
// about to delete.)
ExtraFiles(src []byte) ([]ExtraFile, error)
}
ExtraFileProducer is the optional interface an Operation implements when it writes to files beyond its primary Path(). The orchestrator type-asserts for it during the per-file planning loop and, if present, collects the extra files for validation + staging/apply.
Only archive_section and remove_section implement this today; the five original operations don't, so they're untouched.
type FetchDeps ¶
type FetchDeps struct {
Idx *index.Index
Schema *schema.Schema
Manifest *config.Manifest
MemoryDir string // absolute path to .agent-memory/ (the local store's dir)
Branch git.BranchInfo
// Stores lists the cached external stores to federate into the SEARCH path
// (PR5). Empty → the single-store path, byte-for-byte (the opt-in
// invariant). The bootstrap path is always local-only. Build with
// LoadFetchStores.
Stores []StoreRef
// ChangedFiles are repo-relative paths with uncommitted changes,
// resolved by the caller (CLI / MCP) via git.ChangedFiles. Feeds the
// "decisions/pitfalls referencing changed files" ranking signal. Empty
// is fine (no boost) — outside a git repo, or on a clean tree.
ChangedFiles []string
// Logger is optional; nil → discard. See UpdateDeps.log().
Logger *slog.Logger
}
FetchDeps bundles the dependencies BuildContextPack needs. Callers (CLI fetch, MCP server handler) construct this and reuse it for the lifetime of one request.
type FetchRequest ¶
type FetchRequest struct {
Query string
Scope []string
Budget int // characters; 0 = use manifest default
Include []string // categories (advisory in M2; M3 enforces)
ExcludeArchive bool // hard exclude vs penalize
}
FetchRequest mirrors the memory.fetch_context MCP tool input.
type FetchResponse ¶
type FetchResponse struct {
Context string `json:"context"`
IncludedFiles []IncludedFile `json:"included_files"`
Omitted []OmittedFile `json:"omitted,omitempty"`
SuggestedNextQueries []string `json:"suggested_next_queries,omitempty"`
ContextMetadata ContextMetadata `json:"context_metadata"`
}
FetchResponse is the structured shape returned to the caller. The MCP tool serialises this verbatim; the CLI emits the Context field by default and the whole structure under --json.
func BuildContextPack ¶
func BuildContextPack(ctx context.Context, req FetchRequest, deps FetchDeps) (resp *FetchResponse, err error)
BuildContextPack is the entry point. It dispatches between the bootstrap path (empty query) and the search path (non-empty query). All output goes into FetchResponse.
type Finding ¶
type Finding struct {
Type string `json:"type"`
Line int `json:"line"`
ApproximateLocation string `json:"approximate_location"`
}
Finding describes one secret-scan hit. The actual matched bytes are intentionally NOT included — by policy (design doc §13.2 / §23.3) the scanner must not echo the full secret value back to the caller or to logs. Type + Line + ApproximateLocation is enough for the agent to locate and rewrite the offending content.
func Scan ¶
Scan returns every secret-shaped hit in content, ordered by byte position. Returns nil (not an error) when content is empty.
Behaviour:
- Each pattern in the rule set is checked against content. Matches fully inside an opts.Allowlist region are dropped.
- If opts.EntropyThreshold and opts.EntropyMinLength are both > 0, a secondary pass flags any alphanumeric-ish token whose Shannon entropy meets the threshold.
- Findings carry the type, line number (1-based), and a string location — NEVER the matched bytes.
type IncludedFile ¶
type IncludedFile struct {
Path string `json:"path"`
Reason string `json:"reason"`
Freshness string `json:"freshness,omitempty"`
Confidence string `json:"confidence,omitempty"`
SectionCount int `json:"section_count,omitempty"`
// Store + Origin are populated for federated (non-local) results: Store is
// the manifest store name, Origin is the provenance label "name@<commit>".
// Local results omit both (single-store output is unchanged).
Store string `json:"store,omitempty"`
Origin string `json:"origin,omitempty"`
}
IncludedFile describes one file (possibly contributing multiple sections) that ended up in the pack.
type Intent ¶
type Intent string
Intent classifies what KIND of change the agent wants to make. Each intent maps to a specific approval slot in manifest.updates.approval; routing.go holds that mapping in one place so the orchestrator never has to think about category-vs-intent semantics.
The set is closed: an unknown intent is a hard reject ("invalid_intent") rather than a silent stage. This matches the design doc v0.4.1 §22.2 intent enumeration.
const ( IntentUpdateCurrent Intent = "update_current" IntentSessionLog Intent = "session_log" IntentAddPitfall Intent = "add_pitfall" IntentRecordDecision Intent = "record_decision" IntentRefreshModule Intent = "refresh_module" IntentUpdateConventions Intent = "update_conventions" IntentArchiveStale Intent = "archive_stale" )
type MemoryStatus ¶
type MemoryStatus struct {
MemoryVersion string `json:"memory_version"`
Repo string `json:"repo"`
ActiveBranch string `json:"active_branch,omitempty"`
DurableFiles int `json:"durable_files"`
ArchiveFiles int `json:"archive_files"`
LocalSessions int `json:"local_sessions"`
LocalCurrentFiles int `json:"local_current_files"`
OrphanLocalFiles []string `json:"orphan_local_files,omitempty"`
IndexSizeBytes int64 `json:"index_size_bytes"`
CurrentSizeBytes int64 `json:"current_size_bytes"`
StagedUpdates []StagedStatusEntry `json:"staged_updates,omitempty"`
// StaleNotes lists files flagged "stale" by future per-section
// freshness tracking. Currently always empty (mechanism unimplemented
// — see design doc §20.3).
StaleNotes []string `json:"stale_notes,omitempty"`
Security MemoryStatusSecurity `json:"security"`
Git MemoryStatusGit `json:"git"`
Lock MemoryStatusLock `json:"lock"`
}
MemoryStatus matches the design-doc §15.11 output shape for the `memory.status` MCP tool. Same type is used by the CLI `status` subcommand so both transports return identical structured data.
Some fields are populated with conservative approximations until the underlying mechanism lands — they're documented inline.
func BuildStatus ¶
func BuildStatus(ctx context.Context, deps StatusDeps) (*MemoryStatus, error)
BuildStatus walks the .agent-memory/ tree and assembles a MemoryStatus matching design §15.11. Read-only; never modifies any file. Acquires the advisory lock briefly to determine `lock.held` (best-effort).
type MemoryStatusGit ¶
type MemoryStatusGit struct {
TrackLocal bool `json:"track_local"`
TrackSessions bool `json:"track_sessions"`
IgnoredLocalState bool `json:"ignored_local_state"`
MergeDriverInstalled bool `json:"merge_driver_installed"`
}
MemoryStatusGit is the §15.11 `git` sub-block.
type MemoryStatusLock ¶
type MemoryStatusLock struct {
Held bool `json:"held"`
// StaleRecoveriesLast24h would count crash-recovered locks in a 24h
// window. The kernel handles lock release on process death so this
// is informational and currently always 0 until persisted.
StaleRecoveriesLast24h int `json:"stale_recoveries_last_24h"`
}
MemoryStatusLock is the §15.11 `lock` sub-block.
type MemoryStatusSecurity ¶
type MemoryStatusSecurity struct {
// LastSecretScan is one of "passed" | "n/a" | "failed". Currently
// always "n/a" — there's no per-write scan history persisted. Future
// work (M8b2) will populate this from a scan log.
LastSecretScan string `json:"last_secret_scan"`
// AllowlistedRegions is the total count of secret-scan allowlist
// regions discovered across all durable .md files at status time.
AllowlistedRegions int `json:"allowlisted_regions"`
// UntrustedSources counts proposals with sources of type
// "external" or "inference" recorded against durable files. Not yet
// persisted; always 0 until proposal history tracking lands.
UntrustedSources int `json:"untrusted_sources"`
}
MemoryStatusSecurity is the §15.11 `security` sub-block.
type OmittedFile ¶
type OmittedFile struct {
Path string `json:"path"`
Reason string `json:"reason"`
// Store + Origin mirror IncludedFile's provenance for non-local candidates,
// so a dropped landscape section is as traceable as an included one (useful
// for budget/dedup/stale-cache debugging and the PR6 eval). Local omits
// leave both empty.
Store string `json:"store,omitempty"`
Origin string `json:"origin,omitempty"`
}
OmittedFile describes a candidate that was dropped (budget exceeded or relevance below threshold).
type Operation ¶
type Operation interface {
// Kind returns the operation type ("create_file", "replace_section", ...).
Kind() string
// Path returns the target memory-relative file path (forward-slash).
Path() string
// Validate runs op-specific structural checks: content parses as
// Markdown, required fields are present, paths are well-formed.
// The schema is passed so an op can consult per-category policy.
// Category-level checks (server_managed, agent_writable) are the
// orchestrator's responsibility.
Validate(sch *schema.Schema) error
// Targets returns the drift-check targets the staging engine should
// verify at apply time. May be empty for ops with no drift concern.
Targets() []OperationTarget
// Plan returns the byte-range splice that, applied via
// markdown.Splice(src, []SpliceOp{plan}), produces the desired
// post-state. For create_file with if_exists=reject, src is expected
// to be nil (file doesn't exist) and the splice covers [0, 0).
Plan(src []byte) (agentmd.SpliceOp, error)
}
Operation is one structured Markdown edit. Concrete types live in this file (T3.2). Construct via ParseOperation from an OperationInput, or directly with a struct literal in tests.
func ParseOperation ¶
func ParseOperation(in OperationInput) (Operation, error)
ParseOperation dispatches on in.Op to construct a concrete Operation. Returns a clear error for unknown op kinds.
type OperationInput ¶
type OperationInput struct {
Op string `json:"operation"`
Path string `json:"path"`
SectionID string `json:"section_id,omitempty"`
Heading string `json:"heading,omitempty"`
HeadingLevel int `json:"heading_level,omitempty"`
Occurrence int `json:"occurrence,omitempty"`
ParentSectionID string `json:"parent_section_id,omitempty"`
Content string `json:"content,omitempty"`
IfExists string `json:"if_exists,omitempty"`
IfMissing string `json:"if_missing,omitempty"`
// M4 archival/rename fields.
ArchivePath string `json:"archive_path,omitempty"` // archive_section, remove_section
Replacement string `json:"replacement,omitempty"` // archive_section: new source-section body
Reason string `json:"reason,omitempty"` // remove_section: why it's gone
NewHeading string `json:"new_heading,omitempty"` // rename_heading
NewHeadingLevel int `json:"new_heading_level,omitempty"` // rename_heading; 0 = keep current
}
OperationInput is the JSON shape every operation deserialises from. Fields not relevant to a given op are omitted (omitempty); ParseOperation validates required fields per op type.
type OperationTarget ¶
type OperationTarget struct {
Path string `json:"path"`
SectionID string `json:"section_id,omitempty"`
Policy DriftPolicy `json:"policy"`
Hash string `json:"hash,omitempty"`
}
OperationTarget describes a single (file, optional section) the operation depends on for its drift check. The orchestrator (T3.7) materialises Hash from disk at staging time.
func LoadStagedTargets ¶
func LoadStagedTargets(memDir, stagingID string) ([]OperationTarget, error)
LoadStagedTargets reads staging/<stagingID>/target-checksums.json. Used by apply to re-verify drift policies against the current disk state.
type OwnerInfo ¶
type OwnerInfo struct {
ID string `json:"id,omitempty"`
Kind string `json:"kind,omitempty"` // "agent" | "cli" | ...
OpID string `json:"op_id,omitempty"`
}
OwnerInfo identifies who is proposing the update. Used to populate the lock metadata (which is informational only — see internal/lock).
type ProposeRequest ¶
type ProposeRequest struct {
Intent Intent `json:"intent"`
Rationale string `json:"rationale,omitempty"`
Operations []OperationInput `json:"operations"`
Sources []Source `json:"sources,omitempty"`
Confidence string `json:"confidence,omitempty"`
Owner OwnerInfo `json:"owner,omitempty"`
}
ProposeRequest is the orchestrator's input. Mirrors the propose_update MCP tool input verbatim — see design doc v0.4.1 §22.
type ProposeResponse ¶
type ProposeResponse struct {
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
Routing Routing `json:"routing,omitempty"`
StagingID string `json:"staging_id,omitempty"`
Files []string `json:"files,omitempty"`
Findings []Finding `json:"findings,omitempty"`
Violations []schema.SectionViolation `json:"violations,omitempty"`
ProvenanceViolations []string `json:"provenance_violations,omitempty"`
// --- Applied output (design §15.2) ---
// AppliedAt is the RFC3339 UTC timestamp of the write. Set on apply.
AppliedAt string `json:"applied_at,omitempty"`
// AffectedSections lists the (file, section_id) pairs the proposal's
// operations touched. Set on apply.
AffectedSections []AffectedSection `json:"affected_sections,omitempty"`
// IndexUpdated reports whether the FTS shadow was refreshed. Set on
// apply (true when deps.Idx was present).
IndexUpdated bool `json:"index_updated,omitempty"`
// Warnings carries non-fatal advisories. Always present on apply
// (possibly empty) so consumers can rely on the field.
Warnings []string `json:"warnings,omitempty"`
// --- Staged output (design §15.2) ---
// StagingTTLSeconds is the manifest's staging.ttl_seconds. Set on stage.
StagingTTLSeconds int `json:"staging_ttl_seconds,omitempty"`
// HumanApprovalRequired is always true on the staged path (staging
// means a human must review). Set on stage.
HumanApprovalRequired bool `json:"human_approval_required,omitempty"`
// ReviewCommand is the exact CLI invocation to inspect the staged
// proposal. Set on stage.
ReviewCommand string `json:"review_command,omitempty"`
// AutoStage reports git auto-stage / auto-commit outcomes when the
// applied path produced a write AND manifest.git.auto_stage_changes
// is true. nil on stage or reject; nil on apply when the feature is
// off. See internal/memory/autostage.go.
AutoStage *AutoStageResult `json:"auto_stage,omitempty"`
}
ProposeResponse is what the orchestrator returns to the caller. The MCP tool serialises this verbatim. Field set tracks design §15.2's three output shapes (Applied / Staged / Rejected).
func ProposeUpdate ¶
func ProposeUpdate(ctx context.Context, req ProposeRequest, deps UpdateDeps) (resp *ProposeResponse, err error)
ProposeUpdate runs the full propose_update pipeline:
- Validate intent + non-empty operations.
- session_log intent: rewrite each op's path to sessions/<UTC-today>.md unless it already lives under sessions/.
- Parse + per-op Validate() against the schema.
- Validate paths (ValidateMemoryPath) and resolve each op's Category; reject unknown / server_managed categories.
- Acquire the .agent-memory/meta/lock advisory lock with WaitTimeout from manifest.Concurrency.
- For each unique file in the proposal: a. Read current bytes (empty if file is absent). b. Apply this file's ops SEQUENTIALLY in memory (each op's Plan sees the post-previous-op bytes). c. ValidateMarkdown on the final bytes. d. Per-section schema validation on the final bytes. e. ExtractAllowlistRegions on the final bytes. f. Scan(final, ScanOpts{Allowlist, ...}) — reject on any finding.
- Provenance validation against the dominant category's policy.
- Routing: combine per-op routings. server_only → reject; stage → stage; apply → apply.
- Apply (write atomic + re-index) OR Stage (write proposal artefacts).
Any single step's failure short-circuits to ProposeResponse{Status: rejected} with a stable Reason code.
type ProvenanceContext ¶
type ProvenanceContext struct {
// Sources from the proposal.
Sources []Source
// Confidence from the proposal. May be empty.
Confidence string
// IsNewSection is true when the operation creates a new section (e.g.,
// append_section, replace_section with if_missing=append) rather than
// modifying an existing one. Used by Provenance.RequiredForNewSections.
IsNewSection bool
}
ProvenanceContext bundles the runtime facts the validator needs about a specific propose_update operation.
type RebaseResult ¶
type RebaseResult struct {
StagingID string `json:"staging_id"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
Forced bool `json:"forced,omitempty"`
Drift []DriftReport `json:"drift,omitempty"`
Files []string `json:"files,omitempty"` // staged files updated by rebase
Findings []Finding `json:"findings,omitempty"` // populated on rebase_secret_detected
}
RebaseResult is what RebaseStaged returns.
func RebaseStaged ¶
func RebaseStaged(ctx context.Context, stagingID string, deps UpdateDeps, force bool) (res *RebaseResult, err error)
RebaseStaged attempts to make a staged proposal applyable again after the target disk state changed since stage time.
Phases:
- Acquire the cross-process advisory lock.
- Load proposal.json + target-checksums.json.
- CheckDrift every target. Classify: - file_present drift (file gone) → hard block - file_absent drift (file appeared) → hard block - section_resolvable drift (section gone) → hard block - section_content_match where section is gone → hard block - section_content_match where only hash differs → SOFT (rebaseable with --force; we accept the new base bytes as the planning input)
- No drift at all → return Status=skipped_clean.
- Any hard block → return rejected with reason=unresolvable_drift.
- All soft but --force not set → return rejected with reason=force_required.
- Re-plan: walk proposal.Request.Operations grouped by path; for each file, read current disk bytes, ParseOperation + Validate(schema) + Plan + Splice sequentially. The post-state per file becomes the new staging/<id>/files/<path>.
- Re-validate the new staged bytes (ValidateMarkdown + allowlist+Scan). A re-splice that introduces a secret rejects the rebase without touching disk.
- WriteAtomic the new staged files. Update target-checksums.json with refreshed hashes for content_match targets.
Provenance is NOT re-checked — sources don't drift. Routing is NOT re-checked — the stored proposal already passed routing at stage time.
Returns a Go error only for infrastructure failures (lock open, I/O). Application-level rejections come back in RebaseResult, NOT as errors, symmetric with ProposeUpdate / ApplyStaged.
type RejectionEntry ¶
type RejectionEntry struct {
RejectedAt string `json:"rejected_at"` // RFC3339 UTC
Reason string `json:"reason"` // one of RejectionReason* constants
StagingID string `json:"staging_id"`
Intent string `json:"intent,omitempty"` // copied from the staged Request
Rationale string `json:"rationale,omitempty"`
Files []string `json:"files,omitempty"`
StagedAt string `json:"staged_at,omitempty"` // when the proposal was originally staged
AgeSeconds int `json:"age_seconds,omitempty"` // RejectedAt - StagedAt
}
RejectionEntry is one row in meta/rejection-log.jsonl, recording a staged proposal that was discarded (either by the user or by the TTL sweeper). JSON tags match the on-disk format byte-for-byte.
func ListRejections ¶
func ListRejections(memDir string) ([]RejectionEntry, error)
ListRejections reads meta/rejection-log.jsonl in full and returns the entries in file order (chronological because the log is append-only). A missing log file returns (nil, nil) — that's the normal state for a fresh project.
Malformed JSON lines are silently skipped. This is intentional: an audit log is a forensic tool; a corrupted line shouldn't prevent the rest from being readable.
type RemoveSection ¶
type RemoveSection struct {
FilePath string
SectionID string
Heading string
Level int
Occurrence int
ArchivePath string
Reason string // why it's being removed; recorded as a comment in the archive
}
RemoveSection archives a section to a new write-once archive file, then splices the section out of the source entirely (heading included). Per design §15.9, removal is archive-first — content is preserved in archive/ before the source loses it — and always stages.
func (*RemoveSection) ExtraFiles ¶
func (op *RemoveSection) ExtraFiles(src []byte) ([]ExtraFile, error)
ExtraFiles archives the section content. When Reason is set, it's prepended as an HTML comment so the archive file records WHY the section was removed without affecting rendered output.
func (*RemoveSection) Kind ¶
func (op *RemoveSection) Kind() string
func (*RemoveSection) Path ¶
func (op *RemoveSection) Path() string
func (*RemoveSection) Targets ¶
func (op *RemoveSection) Targets() []OperationTarget
type RenameHeading ¶
type RenameHeading struct {
FilePath string
SectionID string
Heading string
Level int
Occurrence int
NewHeading string
NewHeadingLevel int // 0 = keep current level
}
RenameHeading changes a section's heading text (and optionally its level, constrained to ±1) while preserving the @id anchor and all bytes outside the heading line. Per design §15.10.
func (*RenameHeading) Kind ¶
func (op *RenameHeading) Kind() string
func (*RenameHeading) Path ¶
func (op *RenameHeading) Path() string
func (*RenameHeading) Targets ¶
func (op *RenameHeading) Targets() []OperationTarget
type ReplaceSection ¶
type ReplaceSection struct {
FilePath string
SectionID string
Heading string
Level int
Occurrence int
Content []byte
IfMissing string // "reject" (default) | "append" | "create_file"
}
ReplaceSection replaces the entire section identified by SectionID (or Heading+Level+Occurrence) with Content. Content must start with the same heading line and include the @id anchor (if the original had one).
func (*ReplaceSection) Kind ¶
func (op *ReplaceSection) Kind() string
func (*ReplaceSection) Path ¶
func (op *ReplaceSection) Path() string
func (*ReplaceSection) Targets ¶
func (op *ReplaceSection) Targets() []OperationTarget
type ReplaceSectionContent ¶
type ReplaceSectionContent struct {
FilePath string
SectionID string
Heading string
Level int
Occurrence int
Content []byte // body only; must NOT start with a heading
}
ReplaceSectionContent replaces a section's BODY only — the heading line and the immediately-following @id anchor (if any) and at most one blank line after the anchor are preserved. Useful when the heading/ID must stay stable but the body changes.
func (*ReplaceSectionContent) Kind ¶
func (op *ReplaceSectionContent) Kind() string
func (*ReplaceSectionContent) Path ¶
func (op *ReplaceSectionContent) Path() string
func (*ReplaceSectionContent) Plan ¶
func (op *ReplaceSectionContent) Plan(src []byte) (agentmd.SpliceOp, error)
func (*ReplaceSectionContent) Targets ¶
func (op *ReplaceSectionContent) Targets() []OperationTarget
type Routing ¶
type Routing struct {
Mode schema.ApprovalMode
Reason string
}
Routing is the resolved approval decision for one operation under one intent. Mode comes from the manifest's per-slot ApprovalPolicy; Reason is a human-readable trace of how we got there (which slot was consulted, any overrides, etc.) — surfaced in propose_update's response so the agent can learn why a proposal was staged vs applied.
func CombineRoutings ¶
CombineRoutings merges per-op Routings into a single proposal-level decision. Rules (most restrictive wins):
- any server_only → reject (the orchestrator translates this to a "server_only_category" rejection; routing itself just reports the mode).
- any stage → stage.
- else → apply.
Reason is the concatenation of contributing routings' reasons.
func DecideRouting ¶
DecideRouting picks the approval slot for (intent, op) and returns the resolved Mode from the manifest plus a Reason trace.
The intent → slot mapping (manifest.updates.approval.<slot>):
update_current → current update_shared → current_shared session_log → sessions add_pitfall + append_to_section → pitfalls_append add_pitfall + anything else → pitfalls_replace record_decision → decisions refresh_module → modules update_conventions → conventions archive_stale → archive
add_pitfall is the only intent whose slot depends on the operation kind: append-style updates apply without review (low-risk, additive) while section-level rewrites require staging (high-risk, can drop knowledge). See manifest defaults in config.DefaultManifest.
Returns an error only when the intent is not in the recognised set; callers should map that to a "reject: invalid_intent" response.
type ScanOpts ¶
type ScanOpts struct {
// Allowlist excludes the listed byte ranges from scanning. Build it
// with ExtractAllowlistRegions over the same content.
Allowlist []AllowlistRegion
// EntropyThreshold and EntropyMinLength enable Shannon-entropy
// detection over alphanumeric tokens. Tokens shorter than
// EntropyMinLength are skipped. Tokens whose entropy meets or exceeds
// EntropyThreshold are flagged as type "high_entropy".
//
// Set both to zero to disable entropy scanning. Recommended starting
// values per design doc §23.2: threshold=4.5, min_length=32. Tighter
// thresholds reduce false positives at the cost of catching fewer
// real-but-unrecognised tokens.
EntropyThreshold float64
EntropyMinLength int
// PIIScanSSNAndCC enables high-confidence PII detection (SSN shape +
// credit card with Luhn validation). Both patterns are extremely rare
// in legitimate technical content; default-on in DefaultManifest.
PIIScanSSNAndCC bool
// PIIScanEmail enables email-address detection. Opt-in because emails
// appear legitimately in documentation (maintainer addresses, support
// contacts, example syntax). Use allowlist regions for legitimate
// occurrences when this is on.
PIIScanEmail bool
}
ScanOpts configures Scan.
func DefaultScanOpts ¶
func DefaultScanOpts() ScanOpts
DefaultScanOpts returns the recommended scanner configuration per design doc §23.2.
type Source ¶
type Source struct {
Type string `json:"type"` // file | test | user | session | inference | external
Ref string `json:"ref,omitempty"` // file path, test name, etc.
}
Source describes one provenance entry attached to a propose_update proposal — where the agent claims this knowledge comes from. The fields mirror design doc v0.4.1 §23.5.
type StagedProposal ¶
type StagedProposal struct {
StagingID string `json:"staging_id"`
StagedAt string `json:"staged_at"`
Request ProposeRequest `json:"request"`
Routing Routing `json:"routing"`
Files []string `json:"files"`
}
StagedProposal is the envelope written to staging/<id>/proposal.json by stageProposal and read back by review/apply. Public so CLI renderers and test fixtures can inspect fields without re-parsing JSON themselves.
func ListStaged ¶
func ListStaged(memDir string) ([]StagedProposal, error)
ListStaged returns every staged proposal under memDir/staging/, sorted ascending by staging-id (which gives chronological order because the IDs start with a UTC timestamp).
A missing staging/ directory returns (nil, nil) — that's the normal state right after `agent-memory init`.
Malformed staging entries (missing proposal.json, unreadable JSON) are silently skipped. The reasoning: review should still surface the rest of the queue; a separate `doctor` command will eventually inspect quarantined staging entries.
func LoadStaged ¶
func LoadStaged(memDir, stagingID string) (*StagedProposal, error)
LoadStaged reads staging/<stagingID>/proposal.json and returns the parsed envelope. Returns a wrapped error when the directory or file is missing.
type StagedStatusEntry ¶
type StagedStatusEntry struct {
ID string `json:"id"`
Intent string `json:"intent"`
AgeSeconds int `json:"age_seconds"`
TTLRemainingSeconds int `json:"ttl_remaining_seconds"`
TargetFiles []string `json:"target_files"`
DriftDetected bool `json:"drift_detected"`
}
StagedStatusEntry is the per-proposal summary inside MemoryStatus.StagedUpdates.
type StatusDeps ¶
type StatusDeps struct {
MemoryDir string
Manifest *config.Manifest
Schema *schema.Schema
Branch agentgit.BranchInfo
MemoryVersion string // "0.X.Y" or "dev"
}
StatusDeps bundles the inputs BuildStatus needs. MemoryDir is the absolute path to .agent-memory/; Manifest + Schema are loaded; Branch is optional (zero value treated as "not in a git repo").
type StoreRef ¶ added in v0.5.0
type StoreRef struct {
Name string // manifest store name (= index store tag); never "local"
Dir string // abs path to the cached store dir (meta/cache/stores/<name>/)
Origin string // provenance label, e.g. "platform@a1b2c3"
PriorityMultiplier float64 // applied to the negative BM25 score; <1 penalises (default 0.8)
}
StoreRef is one cached external "landscape" store the search path federates over (federation, PR5). The local store is NOT represented here — it is MemoryDir, with an implicit priority of 1.0.
func LoadFetchStores ¶ added in v0.5.0
LoadFetchStores builds the cached-store registry for the fetch search path (federation, PR5) from the manifest's declared stores and the committed meta/stores.lock. A store is federated only when it is BOTH materialised (cache dir present) AND recorded in the lock — the federation contract is that nothing opaque or unpinned lands, and every chunk's provenance shows the store + pin state. So:
- declared but not synced (no cache dir) → skipped (run `sync`);
- cache dir but no lock entry (orphan) → skipped (sync reconciles it);
- locked with a resolved commit → origin "name@<short>";
- unlocked local-path source (recorded) → origin "name@unlocked".
Returns (nil, nil) when no stores are declared — the caller then runs the single-store path unchanged (the opt-in invariant). A malformed or too-new lock returns an error (fail-closed, consistent with sync); callers should degrade to a local-only fetch rather than fail the whole request.
type StoreSyncResult ¶ added in v0.5.0
type StoreSyncResult struct {
Name string
ResolvedCommit string
Unlocked bool
Reused bool // reproduced the previously-locked commit (no re-pin)
Err error
}
StoreSyncResult reports the outcome of one store. Err is non-nil when that store failed; Sync still attempts the others.
func Sync ¶ added in v0.5.0
func Sync(ctx context.Context, deps SyncDeps) ([]StoreSyncResult, error)
Sync materialises every referenced store and rewrites meta/stores.lock. Stores no longer declared in the manifest are reconciled away (cache dir + lock entry removed). The returned error is for whole-operation failures (a malformed lock, an unwritable lock); per-store failures live in the results.
type SweepResult ¶
type SweepResult struct {
DryRun bool `json:"dry_run,omitempty"`
Expired []ExpiredProposal `json:"expired,omitempty"`
Removed []string `json:"removed,omitempty"`
}
SweepResult is what SweepStale returns. Expired is every staging entry whose age exceeded TTL at sweep time; Removed is the subset that SweepStale actually deleted (matches Expired unless DryRun was true).
func SweepStale ¶
SweepStale walks .agent-memory/staging/ and removes every proposal whose (now - StagedAt) exceeds ttl. Each removal is also recorded in meta/rejection-log.jsonl with reason="ttl_expired".
- dryRun=true → no filesystem changes; only Expired is populated.
- ttl <= 0 → returns SweepResult{} immediately (sweep disabled).
- Missing / malformed StagedAt on a staged proposal → conservative: the entry is skipped (kept on disk) so corrupted-but-recent proposals don't get nuked.
- Per-entry failures during removal are collected silently; the remaining proposals still get processed. The user can re-run sweep after fixing the underlying issue.
SweepStale does NOT acquire the cross-process advisory lock — the caller (CLI subcommand) should. A propose_update writing into the same staging directory mid-sweep would be the only race, and the lock is the right primitive for that.
type SyncDeps ¶ added in v0.5.0
type SyncDeps struct {
MemoryDir string // absolute path to the consuming repo's .agent-memory/
Manifest *config.Manifest
Logger *slog.Logger
// Update, when true, moves each git store's pin forward to the latest of
// its requested revision instead of reproducing the locked commit.
Update bool
// Now is injectable for deterministic tests; nil → time.Now.
Now func() time.Time
}
SyncDeps bundles what Sync needs.
type UpdateDeps ¶
type UpdateDeps struct {
Manifest *config.Manifest
Schema *schema.Schema
MemoryDir string // absolute path to .agent-memory/
Idx *index.Index // optional
// Logger receives structured events from the orchestrator + staging
// engine. Optional: nil → a discard logger (see log()). NEVER logs
// matched secret bytes; only Finding.Type / .Line.
Logger *slog.Logger
}
UpdateDeps bundles the orchestrator's dependencies. Index is optional — nil means "skip re-index after apply" (used in tests that don't care about the FTS shadow).