| 1 | // Package memory implements Reasonix's persistent memory. It mirrors Claude |
| 2 | // Code's two-layer model while honoring Reasonix's cache-first architecture: |
| 3 | // |
| 4 | // - Standing instructions resolved by internal/instruction and exposed here |
| 5 | // through compatibility aliases for existing panel and controller APIs. |
| 6 | // - Auto-memory store: per-project fact files with frontmatter plus a MEMORY.md |
| 7 | // index, which the model maintains via the `remember` tool (see store.go). |
| 8 | // |
| 9 | // All of it folds into the durable system-prompt prefix exactly once at boot |
| 10 | // (see Compose), so it rides DeepSeek's automatic prefix cache at zero per-turn |
| 11 | // cost. Mid-session changes never mutate that prefix; they take effect through |
| 12 | // the controller's transient tail-injection and fold into the prefix on the next |
| 13 | // session. |
| 14 | package memory |
| 15 | |
| 16 | import ( |
| 17 | "path/filepath" |
| 18 | |
| 19 | "reasonix/internal/instruction" |
| 20 | ) |
| 21 | |
| 22 | // Scope labels where a doc source was discovered, so the assembled block can |
| 23 | // attribute each chunk and callers (e.g. the `#` quick-add picker) can offer |
| 24 | // meaningful targets. |
| 25 | type Scope = instruction.Scope |
| 26 | |
| 27 | const ( |
| 28 | ScopeUser = instruction.ScopeUser // ~/.reasonix/REASONIX.md |
| 29 | ScopeAncestor = instruction.ScopeAncestor // an instruction file between workspace root and target |
| 30 | ScopeProject = instruction.ScopeProject // instruction file at the workspace root |
| 31 | ScopeLocal = instruction.ScopeLocal // *.local.md personal override |
| 32 | ) |
| 33 | |
| 34 | // docNames are the recognized memory filenames at each level, in load order. |
| 35 | // REASONIX.md is ours; AGENTS.md and CLAUDE.md are the cross-tool conventions. |
| 36 | // When several distinct files exist in one directory, all load (each labeled with |
| 37 | // its source path), so a repo already carrying an AGENTS.md / CLAUDE.md is picked |
| 38 | // up without renaming. New docs are created as AGENTS.md (the universal |
| 39 | // convention) — see defaultDocName / Set.DocPath. |
| 40 | var docNames = instruction.DocumentNames |
| 41 | |
| 42 | // localNames are the personal, git-ignored overrides, highest precedence. |
| 43 | var localNames = instruction.LocalDocumentNames |
| 44 | |
| 45 | // defaultDocName / defaultLocalName are the filenames a fresh doc is created as |
| 46 | // when a directory has none yet: AGENTS.md is the widely-shared convention, so a |
| 47 | // new project's memory is portable to other agent tools out of the box. |
| 48 | const ( |
| 49 | defaultDocName = "AGENTS.md" |
| 50 | defaultLocalName = "AGENTS.local.md" |
| 51 | ) |
| 52 | |
| 53 | // Source is one loaded memory file with provenance and @import-expanded body. |
| 54 | type Source = instruction.Document |
| 55 | |
| 56 | // absOf returns the absolute form of p, falling back to a cleaned p on error so |
| 57 | // the value is still usable as a stable map key. |
| 58 | func absOf(p string) string { |
| 59 | if abs, err := filepath.Abs(p); err == nil { |
| 60 | return abs |
| 61 | } |
| 62 | return filepath.Clean(p) |
| 63 | } |
| 64 | |
| 65 | // sameDir reports whether two paths denote the same directory. |
| 66 | func sameDir(a, b string) bool { return absOf(a) == absOf(b) } |
| 67 |