Documentation
¶
Overview ¶
Package localproject owns the local Project: the stable identity that groups the sessions of one local unit of work across CLI, TUI, and Desktop, and that will own the first cross-session memory scope.
A Project is not a Workspace. Workspace is the directory a session actually executes in and decides containment, hooks, skills, and AGENTS.md; Project is the identity those sessions share, which for a Git repository spans the primary checkout and every linked worktree. See docs/design/local-project-memory.md §6.
Persistence lives in internal/infra/localprojectstore. This package owns what the operations mean; that one owns making them durable.
Index ¶
- Constants
- Variables
- func BodyDigest(body string) string
- func FormatIndex(memories []Memory) []byte
- func FormatMemory(m Memory) []byte
- func NameForWorkspace(workspace string) string
- func ParseVerifiedAt(s string) (*time.Time, error)
- func ScanMemoryForSecrets(description, body string) error
- func SortMemories(memories []Memory)
- func ValidMemoryName(s string) bool
- func ValidMemoryType(t MemoryType) bool
- type Key
- type Kind
- type Memory
- type MemorySet
- type MemoryType
- type MemoryWrite
- type Project
- type SkippedMemory
- type Store
- type Summary
- type Update
Constants ¶
const ( // MaxMemories bounds the store. It is a starting bound chosen to be raised // on evidence rather than lowered after users have filled it. MaxMemories = 20 // MaxDescriptionChars bounds one index line's payload. MaxDescriptionChars = 100 // MaxBodyChars bounds one body. Generous because it is not resident. MaxBodyChars = 2000 // MaxMemoryNameChars bounds a slug, which is also a file name. MaxMemoryNameChars = 64 )
Budgets. Each is enforced on write; none truncates silently at render time.
const IndexFileName = "MEMORY.md"
IndexFileName is the generated index over the memory files.
It is a projection, rebuilt from the files after every write exactly as the Project catalog is rebuilt from Project metadata, and for the same reason: an index that can disagree with its sources is a defect surface with no compensating capability. Users edit memories, not this.
const MemoryFileExt = ".md"
MemoryFileExt is the extension of a memory file. The file's base name is the slug, which is the memory's identity.
const MetaVersion = 1
MetaVersion is the meta.json format this build writes and is the only one it reads.
Variables ¶
var ( // ErrMemoryNotFound reports that no memory has that name. ErrMemoryNotFound = errors.New("localproject: no such memory") // ErrMemoryUnread reports an attempt to replace a memory this run has not // read. It is not a conflict: there is nothing to merge against, because // the writer has not seen what it would be overwriting. ErrMemoryUnread = errors.New("localproject: memory was not read by this run") // ErrMemoryConflict reports that the body changed since this run read it -- // another session's write, or a direct user edit. ErrMemoryConflict = errors.New("localproject: memory changed since this run read it") // ErrMemoryFull reports that the store already holds MaxMemories. ErrMemoryFull = errors.New("localproject: project memory is full") // ErrMemoryInvalid reports a memory this build will not persist. ErrMemoryInvalid = errors.New("localproject: invalid memory") // ErrMemorySecret reports a body holding something that looks like a // credential. It is a refusal to persist, not proof of harm, and its // absence is not proof of safety. ErrMemorySecret = errors.New("localproject: memory looks like it holds a credential") )
var ( // ErrNotFound reports that no Project matches the id or key asked for. ErrNotFound = errors.New("localproject: not found") // ErrDuplicateLocator reports two Projects claiming one locator. It is a // refusal, not a choice: picking either would silently join or split a // memory domain. See docs/design/local-project-memory.md §7.4. ErrDuplicateLocator = errors.New("localproject: duplicate locator") // ErrCatalogBusy reports that another process holds the catalog writer // lock and did not release it in time. ErrCatalogBusy = errors.New("localproject: catalog is busy") )
Errors a Store reports. They are declared here rather than in the store package because a caller decides what to do about them — resolve, repair, or refuse — and that decision is domain, not persistence.
Functions ¶
func BodyDigest ¶
BodyDigest identifies a body for the read-then-replace rule.
func FormatIndex ¶
FormatIndex generates MEMORY.md from the memory files.
It is written for a person browsing the directory, not parsed by anything: the runtime renders its own block from the same memories, and the store rebuilds this file rather than reading it.
func FormatMemory ¶
FormatMemory renders one memory to its file form.
func NameForWorkspace ¶
NameForWorkspace derives a Project's initial name from the directory it was first opened in. It is presentation only, and a user may rename it at any time.
func ParseVerifiedAt ¶
ParseVerifiedAt reads a YYYY-MM-DD verification date. An empty string is no date rather than an error: most memories assert nothing they do not hold.
func ScanMemoryForSecrets ¶
ScanMemoryForSecrets refuses a memory holding a recognizable credential.
The description is scanned as well as the body, because it is the part that goes to the model on every call: a token pasted into a one-line summary would be the most exposed place in the store, not the least.
Best effort, and the Agent's own contract -- do not persist credentials or surprising sensitive information -- remains the real guard. The error names the shapes, never the values, so a refusal does not put the credential into a log or back into the model's context.
func SortMemories ¶
func SortMemories(memories []Memory)
SortMemories orders a set by name, which is what makes the generated index stable across writes.
func ValidMemoryName ¶
ValidMemoryName reports whether s is a usable slug: lowercase letters, digits, and single hyphens between them.
The slug is a file name, so the character set is a containment guard as much as a convention -- a name that could traverse a directory or collide under a case-insensitive filesystem is one the store must never be asked to write.
func ValidMemoryType ¶
func ValidMemoryType(t MemoryType) bool
ValidMemoryType reports whether t is one this build understands.
Types ¶
type Key ¶
Key is what a resolved workspace is looked up by: the kind of identity and the locator that anchors it.
type Kind ¶
type Kind string
Kind says what a Project's identity is anchored to, and therefore what locator resolves it.
type Memory ¶
type Memory struct {
Name string
Description string
Type MemoryType
// SessionID and UpdatedAt are this memory's own provenance, which is why
// there is no sidecar metadata file: a lone Markdown document needed one
// because it had nowhere to record who wrote it, and per-file frontmatter
// removes that need rather than moving it.
SessionID string
UpdatedAt time.Time
// VerifiedAt is the date a memory that caches something expensive was last
// checked against its source of truth. Nil for the ordinary memory that
// asserts nothing it does not itself hold. Only the date is meaningful.
VerifiedAt *time.Time
Body string
}
Memory is one remembered thing: its own file, its own provenance.
Name is the slug, the file name, and the identity all at once -- there is no second identifier that can disagree with it.
func ParseMemory ¶
ParseMemory reads one memory file. name is the slug taken from the file name, which is authoritative: frontmatter that disagrees with it is an error rather than a second opinion about what this memory is called.
type MemorySet ¶
type MemorySet struct {
// Memories are the usable ones, ordered by name so the index is stable.
Memories []Memory
Skipped []SkippedMemory
}
MemorySet is what the store found in one Project's memory directory.
type MemoryType ¶
type MemoryType string
MemoryType says what kind of knowledge a memory holds.
There is deliberately no user type. Who the user is would be global user memory, which §5.2 keeps out of a Project-scoped store.
const ( // MemoryTypeFeedback is guidance the user gave about how to work in this // Project. It records what the user wants, never what the user is. MemoryTypeFeedback MemoryType = "feedback" // MemoryTypeProject is ongoing work, goals, decisions, and constraints not // derivable from the code or its history. MemoryTypeProject MemoryType = "project" // MemoryTypeReference points at external resources: dashboards, tickets, // specifications. MemoryTypeReference MemoryType = "reference" )
func MemoryTypes ¶
func MemoryTypes() []MemoryType
MemoryTypes returns every valid type, in the order a surface should list them. It returns a fresh slice so no caller can reorder or extend the set the validator trusts.
type MemoryWrite ¶
type MemoryWrite struct {
Name string
Description string
Type MemoryType
Body string
SessionID string
PriorDigest string
// VerifiedAt is the date this memory was last checked against the source of
// truth its body names. Nil leaves an existing date alone -- rewording a
// body is not re-verifying it -- and clearing one is a hand edit, not
// something a run does on the way past.
VerifiedAt *time.Time
}
MemoryWrite creates or replaces exactly one memory.
PriorDigest is supplied by the runtime from the bodies it has handed the model this run, never by the model. Routing a correctness token out to the least reliable component in the loop and expecting it back verbatim would add an omitted-parameter case whose only plausible fallback is an unconditional overwrite. Empty means this run has not read the memory, which is a refusal on replacement and irrelevant on creation.
type Project ¶
type Project struct {
Version int `json:"version"`
ID string `json:"id"`
Name string `json:"name"`
Kind Kind `json:"kind"`
// DefaultWorkspace is where a new Desktop run opens by default. For a Git
// Project it is one of possibly several worktrees and may change without
// identity changing; for a directory Project it is also the locator.
DefaultWorkspace string `json:"default_workspace"`
// GitCommonDir is set only when Kind is KindGit.
GitCommonDir string `json:"git_common_dir,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastUsedAt time.Time `json:"last_used_at"`
}
Project is one local unit of work: a stable opaque ID plus the metadata that lets a surface find it again and name it to a user.
ID is the relationship key that sessions and memory hang off. Name is presentation and never identity — renaming a Project moves nothing. The locator (GitCommonDir, or DefaultWorkspace for a directory Project) is how a workspace is matched back to this record, and it is a locator rather than an identity precisely because moving a repository invalidates it: that is a relink, not a new Project.
func Apply ¶
Apply returns p with update applied and UpdatedAt advanced to now. It does not mutate p and does not validate: the caller persists through a Store, which validates before writing.
func New ¶
New returns a Project for key, with a freshly minted opaque ID.
The caller supplies the workspace and name it derived from the filesystem; this constructor owns identity, kind consistency, and timestamps, so a caller cannot produce a record whose locator disagrees with its kind.
type SkippedMemory ¶
SkippedMemory is a file the store could not use. It is reported rather than guessed at: skipping one memory is a smaller failure than rendering an index line promising a body the read tool cannot return.
type Store ¶
type Store interface {
// Resolve returns the Project registered for key, registering proposed
// when there is none.
//
// Creation is serialized by the catalog writer lock and repeats the lookup
// after acquiring it. That second lookup is the point: CLI and Desktop
// starting in the same new repository at the same moment must agree on one
// Project ID rather than mint two and split its memory. proposed is used
// only on that path; when a Project already exists it is discarded and the
// stored one's last_used_at is advanced.
//
// A locator claimed by two Projects is ErrDuplicateLocator: choosing one
// would silently join or split a memory domain, so repair is the user's.
Resolve(ctx context.Context, key Key, proposed Project) (Project, error)
// Find returns the Project registered for key, or ErrNotFound. Unlike
// Resolve it registers nothing, which is what a diagnostic needs: a command
// that reports which Project a directory belongs to must not be the thing
// that decides it belongs to one.
Find(ctx context.Context, key Key) (Project, error)
// Get returns one Project by ID, or ErrNotFound. It is the authoritative
// read: it opens the bundle rather than trusting the catalog projection.
Get(ctx context.Context, id string) (Project, error)
// List returns the catalog projection, rebuilding it from the bundles when
// it is missing or unusable.
List(ctx context.Context) ([]Summary, error)
// Update changes presentation or relinks a moved Project. It takes the
// catalog writer lock, because a locator change has to be visible in the
// projection at the same moment it is visible in the bundle.
Update(ctx context.Context, id string, update Update) error
// Delete removes a Project bundle and its row.
//
// It deletes nothing outside that bundle. Sessions are owned by the session
// store and are not cascaded by a Project going away — a caller that means
// to remove them says so there, having shown the user which ones. See
// docs/design/local-project-memory.md §15.
Delete(ctx context.Context, id string) error
// Memories returns every memory of the Project, plus the files it could
// not use. A Project that has never written one reads as empty, not as an
// error: no memories and no memory directory are the same state to every
// caller.
Memories(ctx context.Context, projectID string) (MemorySet, error)
// WriteMemory creates or replaces exactly one memory under the memory
// writer lock, and regenerates the index from the files.
//
// Creating a name that does not exist is always accepted. Replacing an
// existing memory requires MemoryWrite.PriorDigest to match the body on
// disk: an empty one is ErrMemoryUnread, since the writer has not seen what
// it would overwrite, and a stale one is ErrMemoryConflict. Both refusals
// return the stored memory so the caller can say which version won.
WriteMemory(ctx context.Context, projectID string, write MemoryWrite) (Memory, error)
// DeleteMemory removes one memory and its index line.
DeleteMemory(ctx context.Context, projectID, name string) error
// ClearMemories removes every memory file and leaves an empty index,
// returning how many were removed. It touches nothing else in the bundle.
ClearMemories(ctx context.Context, projectID string) (int, error)
}
Store is the persistence seam between the runtime and physical storage. It expresses Project semantics, not paths, mirroring session.Store: core owns what these operations mean, infra owns making them durable. See docs/design/local-project-memory.md §14.
type Summary ¶
type Summary struct {
ID string `json:"id"`
Name string `json:"name"`
Kind Kind `json:"kind"`
Locator string `json:"locator"`
DefaultWorkspace string `json:"default_workspace"`
LastUsedAt time.Time `json:"last_used_at"`
}
Summary is one Project's row in the catalog projection: enough to resolve a workspace and list Projects without opening every bundle. It is rebuildable from the bundles and is never the only copy of anything.
type Update ¶
type Update struct {
Name *string
// DefaultWorkspace moves where a Project opens by default. For a directory
// Project it also relocates the locator, which is what an explicit relink
// after a move does.
DefaultWorkspace *string
// GitCommonDir relinks a Git Project whose repository moved. It is only
// ever set by an explicit user decision: no heuristic may join two memory
// domains, so nothing infers this from a remote URL or a directory name.
GitCommonDir *string
// TouchLastUsed advances last_used_at, which orders the picker.
TouchLastUsed bool
}
Update describes a change to a Project's presentation or locator. A nil field leaves that value unchanged.
There is no field for ID or Kind: both are immutable, and a workspace that stopped being a Git checkout is a different Project rather than the same one under a new kind.