Documentation
¶
Overview ¶
Package memfile is ogcode's per-turn markdown memory: after a turn completes, a structured summary of that turn is written as one dated markdown file under the project's .ogcode/memory/, and an incremental index of that folder lets a read-only recall agent find and read the right file cheaply.
It replaces the removed graph/embedding agentic-memory system. The whole subsystem is gated by TurnMemoryEnabled (env OGCODE_TURN_MEMORY): off by default, so a build with these types linked in behaves exactly as before until the flag is set.
Index ¶
- Constants
- func DeleteSessionMemory(projectDir, sessionID string) error
- func Filename(createdAt time.Time, sessionID, title string) string
- func MemoryDir(projectDir string) string
- func SessionKey(sessionID string) string
- func SessionMemoryPath(projectDir, sessionID string) string
- func SessionMemoryRelPath(projectDir, sessionID string) string
- func SessionTag(sessionID string) string
- func TurnMemoryEnabled() bool
- func Write(projectDir string, meta Meta, body string) (string, error)
- type Entry
- type Hit
- type HitTurn
- type Manager
- type Meta
- type ReindexStats
- type SearchStore
- func (s *SearchStore) Delete(ref string) error
- func (s *SearchStore) IndexSession(sessionID, title, summary, projectID string, updatedAt int64) error
- func (s *SearchStore) IndexSessionMemory(projectDir, sessionID string) error
- func (s *SearchStore) IndexTurn(path string, meta Meta) error
- func (s *SearchStore) RefreshStale(projectDir string) (int, error)
- func (s *SearchStore) ReindexProject(projectDir, projectID string) (ReindexStats, error)
- func (s *SearchStore) Search(ctx context.Context, projectID, projectDir, query string, limit int) ([]Hit, error)
- type Store
Constants ¶
const SummarySystemPrompt = `You are a memory scribe. You are given a digest of one completed turn between a developer and a coding agent: the developer's request, the agent's tool calls (their inputs and intent only — results are omitted), and the agent's final response.
Write a single, self-contained markdown summary of what happened in this turn, so a future agent reading only this file understands the request, what was done, and the outcome — without access to the original conversation. This file is the only record of the turn a later agent will ever see: a detail you leave out cannot be recovered, so when a fact might be asked about later, keep it.
STRUCTURE (this is mandatory — the summary is indexed and read by its headings):
- Begin with exactly one H1 (` + "`# `" + `) title: a short, specific noun phrase naming the turn's subject.
- Immediately under the H1, add ONE line: ` + "`Topics: <topic>, <topic>, <topic>`" + ` — 3 to 6 short noun-phrase topics (1–4 words each) naming what the turn was actually about: features, subsystems, files, bugs, tools. These are indexed and shown on a conversation's table-of-contents line, so be specific ("memory_map drilldown", "SQLite migration") rather than generic ("coding", "changes"). No other line may start with "Topics:".
- Group the body under H2 (` + "`## `" + `) sections, and use H3 (` + "`### `" + `) for sub-points within a section. Never skip a level.
- Suggested sections, included only when they apply: "## Request" (what the developer asked), "## Developer feedback & preferences" (corrections the developer gave — "don't do X", "use Y instead" — and preferences they stated, in their own words; these guide later turns too), "## What was done" (the actions taken and why, grounded in the tool calls), "## Key files & symbols" (concrete paths / names touched or examined), "## Decisions & why" (the rationale behind a choice, and any approach ruled out — the agent's final response may carry a section like this; preserve it rather than paraphrasing it away), "## Outcome" (the result, how far each piece got, and anything left open).
- A later agent picks what to read from the heading outline alone. Name every H3 — and any H2 beyond the suggested ones — for its specific subject: the feature, file, bug, decision or question it covers ("### Why carry-over is excluded", "### Refund path in ledger.go"), never a generic label ("### Details", "### Changes", "### Notes").
- When the turn produced or settled on an enumerated set — a proposal, a plan, options, definitions, steps, requirements, names, fields, a schema — give it its own section (` + "`## `" + ` or ` + "`### `" + `) named for what it is (for example "### Proposed definitions"), so a later lookup lands on it directly.
- Prefer short paragraphs and tight bullet lists under the deepest relevant heading. Put concrete facts — file paths, symbol names, commands, values, decisions — where they belong in the hierarchy, not in a flat wall of text.
PRESERVE SPECIFICS (later questions turn on these — keep them exactly):
- Reproduce EVERY item of an enumerated set with its content — its name and its definition, value or description — in the original order, keeping each item's original wording as far as you can. Never collapse a set into its count ("proposed 7 definitions") or into a sample ("e.g. X and Y").
- Copy identifiers, numbers, thresholds, versions, file paths, commands, URLs and error messages verbatim. Never round, paraphrase or rename them. Secrets are the one exception — see RULES.
- Keep the developer's stated requirements, constraints, corrections and preferences in their own words.
- Record what is still open: questions the agent asked, choices left to the developer, and the next steps it proposed.
STATUS (say exactly how far each thing got — never further):
- Tool results are not in the digest. A tool call shows only that something was attempted: a test or build command does not mean it passed, and an edit does not mean the change works. Take outcomes only from the agent's final response.
- Mark each change, fix or proposal with how far it got, in the final response's own terms where it gives them: proposed (suggested, not made), changed (files edited, not yet checked), verified (the response says it was tested or run and worked), committed, pushed or released, failed, or abandoned. If the response does not say, write that it does not say.
- Keep the response's caveats — "not verified", "untested", "needs a restart", "not committed" — with the item they qualify.
SELF-CONTAINED (the reader was not there):
- Name what you refer to. Replace "the bug", "this file", "the new approach", "as discussed" and "the above" with the actual file path, symbol, feature, error or decision. If the digest does not make a reference clear, say it is unclear rather than guess.
RULES:
- Never write a secret into the summary — this overrides every rule above. Replace API keys, tokens, passwords, private keys, session cookies, signed URLs and the credentials inside connection strings or headers with <redacted>, keeping the rest of the line (curl -H "Authorization: Bearer <redacted>" …). The name of a variable that holds a secret may stay (OGX_APP_SECRET=<redacted>); its value may not.
- Be faithful to the digest. Do not invent files, symbols, or outcomes that are not evidenced by it.
- Be concise in narration — drop filler, restated boilerplate and step-by-step play-by-play — but never at the cost of a specific above. If the agent's final response is long, keep every item and tighten each one's wording rather than dropping any.
- Output ONLY the markdown summary, starting with the ` + "`#`" + ` title. No preamble, no code fence around the whole thing, no trailing commentary.`
SummarySystemPrompt instructs the synthesis LLM to turn a turn's digest into a tightly-structured markdown summary. The hierarchy demand is not cosmetic: the recall agent navigates these files through the file-map outline, which is built from the heading levels — a clean H1/H2/H3 tree whose headings name their subject is what makes a lookup land on the right section and read only a handful of lines.
The specifics demand is just as load-bearing. This file is all a later turn ever learns about this one, and "be concise" alone let the scribe fold a seven-item proposal into "proposed 7 definitions": recall could then say the proposal existed but not what it said, and the agent went digging through raw session storage for it. So concision applies to narration only — every item of a list, and every exact name and value, is kept.
Two rules keep that faithfulness from carrying the wrong things over. The digest holds tool inputs but not their results, so a scribe told to copy commands verbatim would copy a credential out of one, and one reading a test command as a passing test would record work as done that was only attempted. Secrets are redacted whatever else the prompt says, and a status comes only from the agent's own account of the outcome.
Variables ¶
This section is empty.
Functions ¶
func DeleteSessionMemory ¶ added in v0.53.0
DeleteSessionMemory removes the per-session memory file for sessionID. A missing file is not an error; any other failure is returned.
func Filename ¶
Filename builds a turn summary's filename. The UTC timestamp leads so a lexical sort of the folder is chronological; a session tag groups a conversation's turns and lets session-scoped recall filter by name; the slug makes the file recognisable at a glance. Example:
2026-09-09T143005Z--ses01M269J8TM53--wire-the-recall-agent.md
func MemoryDir ¶
MemoryDir returns the absolute path to a project's turn-summary folder. It does not create the directory — Write does that on demand.
func SessionKey ¶ added in v0.41.0
SessionKey is a session id reduced to its ASCII letters and digits: "ses_01M269J8TM53…" becomes "ses01M269J8TM53…". Every tag for a session — the one in a summary's file name, the shorter one memory_map shows — is a prefix of its key, which is what lets a tag be resolved back to the one conversation it names. "session" for an id with no such characters.
func SessionMemoryPath ¶ added in v0.53.0
SessionMemoryPath returns the absolute path of the per-session memory file for sessionID under projectDir. It does not create anything — the file exists only once the agent writes it.
func SessionMemoryRelPath ¶ added in v0.53.0
SessionMemoryRelPath returns the session memory file's path relative to projectDir, slash-joined, so the prompt shows one path on every OS.
func SessionTag ¶ added in v0.37.0
SessionTag is the exported form of the grouping token Filename embeds in a summary's name: the tag one conversation's turns share. memory_map groups by the full SessionKey instead, and shows the shortest prefix of it that is unique among the conversations it lists.
func TurnMemoryEnabled ¶
func TurnMemoryEnabled() bool
TurnMemoryEnabled reports whether the per-turn markdown memory subsystem is switched on. It is the project's memory by default — a single process-wide env gate turns it OFF (OGCODE_TURN_MEMORY=0/false/off) for the rare case that wants the legacy graph route or no memory at all. Any other value, or an unset variable, leaves it on. It is deliberately env-only rather than a settings-DB field so memory does not depend on the UI toggle.
func Write ¶
Write persists a turn summary as a dated markdown file under the project's .ogcode/memory/ folder and returns its absolute path. body is the synthesized markdown (starting with its own H1); Write prepends YAML frontmatter carrying the Meta so recall can scope/attribute the file, and picks a collision-free filename.
Types ¶
type Entry ¶
type Entry struct {
Path string
SessionID string
ProjectID string
Labels []string
CreatedAt int64 // unix millis; the turn's time, used for temporal ordering
IndexedAt int64 // unix millis; when this row was written
}
Entry is one indexed turn summary. path is the primary key and holds the absolute file path. Labels are the topics the summary writer called out on its `Topics:` line — what memory_map ranks and shows on a conversation's collapsed line.
type Hit ¶ added in v0.56.0
type Hit struct {
SessionID string `json:"sessionId"`
Title string `json:"title"`
Model string `json:"model"`
UpdatedAt int64 `json:"updatedAt"` // unix millis
Score float64 `json:"score"`
MatchedTurns int `json:"matchedTurns"`
Source string `json:"source"` // "title" | "turn" | "memory"
Labels []string `json:"labels"`
Snippet string `json:"snippet"`
Turns []HitTurn `json:"turns"`
}
Hit is one ranked answer to a content search: a conversation, not a turn, because "which session was this about?" is the question, and a conversation has many turns that can each match.
type HitTurn ¶ added in v0.56.0
type HitTurn struct {
At int64 `json:"at"`
Labels []string `json:"labels"`
Snippet string `json:"snippet"`
}
HitTurn is one of the best-matching turns behind a Hit, carrying just enough to render a row: when it happened, what it was about, and the matched words in context.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the barrier between background turn-summary work and recall. A turn summary is synthesized, written, and indexed on a detached goroutine, so a recall that fires immediately afterwards could otherwise read a stale or missing index. Recall calls Wait(project) first; the background job brackets itself with Begin/Done, so Wait blocks until the project's in-flight summaries have all landed in the index.
The counter is keyed by project so a summary in one workspace never blocks recall in another. A sync.Cond (not a per-project WaitGroup) is used because WaitGroup forbids Add concurrent with Wait — exactly the pattern here, where a new turn can begin while a recall is waiting.
func (*Manager) Begin ¶
Begin records that a background summary for project has started. Call it synchronously, before launching the goroutine, so a recall that follows the turn immediately observes the in-flight work.
type Meta ¶
type Meta struct {
SessionID string
ProjectID string
SessionType string
Title string
CreatedAt time.Time
}
Meta is the identifying context for one turn summary, carried through both the file's frontmatter and the index row so recall can scope and attribute a summary without opening its body.
type ReindexStats ¶ added in v0.56.0
type ReindexStats struct{ Scanned, Indexed, Purged int }
ReindexStats reports what a project backfill sweep did.
type SearchStore ¶ added in v0.56.0
type SearchStore struct {
// contains filtered or unexported fields
}
SearchStore is the full-text index over a project's markdown memory corpus: one row per turn summary, per session memory file, and per conversation's title/summary, searched as sessions.
It is separate from Store (the memory_turn_index the memory_map tool reads) because the two have different freshness contracts — see the migration.
func NewSearchStore ¶ added in v0.56.0
func NewSearchStore(database *db.DB) *SearchStore
NewSearchStore creates a SearchStore backed by the given (project-local) database.
func (*SearchStore) Delete ¶ added in v0.56.0
func (s *SearchStore) Delete(ref string) error
Delete drops one document and its FTS mirror. A ref that is not indexed is not an error: the caller is usually reconciling against the filesystem, where the file is already gone.
func (*SearchStore) IndexSession ¶ added in v0.56.0
func (s *SearchStore) IndexSession(sessionID, title, summary, projectID string, updatedAt int64) error
IndexSession indexes a conversation itself: its title and compaction summary. A conversation with neither is dropped from the index rather than stored as an empty row that would rank on nothing.
func (*SearchStore) IndexSessionMemory ¶ added in v0.56.0
func (s *SearchStore) IndexSessionMemory(projectDir, sessionID string) error
IndexSessionMemory indexes a conversation's session memory file — the notes the agent curates and rewrites every turn. A missing file is not an error: it just means this conversation has no memory document, so any stale row goes.
func (*SearchStore) IndexTurn ¶ added in v0.56.0
func (s *SearchStore) IndexTurn(path string, meta Meta) error
IndexTurn indexes one turn summary file as a 'turn' document. meta is the context the caller already knows; the file's own frontmatter wins over it, since it is what the summary was written with. body is capped and its frontmatter stripped so the indexed text is what a reader would see.
func (*SearchStore) RefreshStale ¶ added in v0.56.0
func (s *SearchStore) RefreshStale(projectDir string) (int, error)
RefreshStale re-indexes this project's file-backed documents whose file has changed since it was read, and drops the rows whose file is gone. It is the cheap half of ReindexProject — a stat per indexed file — run before every search so an agent that edited its own session memory mid-turn is searchable immediately, the invariant memory_turn_index deliberately does not have.
func (*SearchStore) ReindexProject ¶ added in v0.56.0
func (s *SearchStore) ReindexProject(projectDir, projectID string) (ReindexStats, error)
ReindexProject is the backfill sweep: it indexes every memory document of a project that is missing from the index or has changed on disk, and drops the rows whose document (or conversation) is gone. Running it twice over an untouched corpus writes nothing the second time — indexed_at is the bookmark each source's modification time is compared against.
func (*SearchStore) Search ¶ added in v0.56.0
func (s *SearchStore) Search(ctx context.Context, projectID, projectDir, query string, limit int) ([]Hit, error)
Search answers a query with the conversations it most likely means, best first. An empty (never nil) slice means "no match", and the project index is built on demand the first time so a fresh process answers the first keystroke instead of showing an empty palette.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the incremental index over a project's turn-summary folder. It mirrors internal/docindex: existence-keyed, one row per file. Turn summaries are immutable once written, so an existence check is a complete freshness check — a file is indexed exactly once and never re-parsed.
func (*Store) IndexFile ¶
IndexFile upserts one summary file's index row. It is the incremental unit: called once for a freshly written file, it never touches any other row. meta supplies the scoping/attribution the index stores.
func (*Store) ListByProject ¶
ListByProject returns every indexed summary for a project, newest first.
func (*Store) ListBySession ¶
ListBySession returns every indexed summary for one conversation, newest first.
func (*Store) PurgeMissing ¶
PurgeMissing drops rows for a project whose files no longer exist on disk, so a deleted summary stops showing up in recall. Cheap to run: it stats one file per indexed row.