opencode-codex-memory 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/src/capture.d.ts +19 -0
  2. package/dist/src/capture.js +120 -0
  3. package/dist/src/citation.d.ts +14 -0
  4. package/dist/src/citation.js +81 -0
  5. package/dist/src/db.d.ts +3 -0
  6. package/dist/src/db.js +78 -0
  7. package/dist/src/git-baseline.d.ts +24 -0
  8. package/dist/src/git-baseline.js +150 -0
  9. package/dist/src/index.d.ts +163 -0
  10. package/dist/src/index.js +365 -0
  11. package/dist/src/llm.d.ts +19 -0
  12. package/dist/src/llm.js +251 -0
  13. package/dist/src/path-guard.d.ts +10 -0
  14. package/dist/src/path-guard.js +44 -0
  15. package/dist/src/paths.d.ts +4 -0
  16. package/dist/src/paths.js +23 -0
  17. package/dist/src/phase1.d.ts +11 -0
  18. package/dist/src/phase1.js +104 -0
  19. package/dist/src/phase2.d.ts +11 -0
  20. package/dist/src/phase2.js +83 -0
  21. package/dist/src/ratelimit.d.ts +5 -0
  22. package/dist/src/ratelimit.js +20 -0
  23. package/dist/src/redact.d.ts +8 -0
  24. package/dist/src/redact.js +37 -0
  25. package/dist/src/source.d.ts +3 -0
  26. package/dist/src/source.js +46 -0
  27. package/dist/src/store.d.ts +96 -0
  28. package/dist/src/store.js +346 -0
  29. package/dist/src/token.d.ts +8 -0
  30. package/dist/src/token.js +19 -0
  31. package/dist/src/workspace.d.ts +8 -0
  32. package/dist/src/workspace.js +194 -0
  33. package/dist/tools/control.d.ts +29 -0
  34. package/dist/tools/control.js +153 -0
  35. package/dist/tools/memory.d.ts +52 -0
  36. package/dist/tools/memory.js +322 -0
  37. package/package.json +23 -6
  38. package/src/capture.ts +0 -137
  39. package/src/citation.ts +0 -94
  40. package/src/db.ts +0 -84
  41. package/src/git-baseline.ts +0 -162
  42. package/src/index.ts +0 -366
  43. package/src/llm.ts +0 -266
  44. package/src/path-guard.ts +0 -44
  45. package/src/paths.ts +0 -29
  46. package/src/phase1.ts +0 -116
  47. package/src/phase2.ts +0 -101
  48. package/src/ratelimit.ts +0 -26
  49. package/src/redact.ts +0 -44
  50. package/src/source.ts +0 -62
  51. package/src/store.ts +0 -434
  52. package/src/templates/consolidation.md +0 -448
  53. package/src/templates/read_path.md +0 -104
  54. package/src/templates/stage_one_input.md +0 -11
  55. package/src/templates/stage_one_system.md +0 -333
  56. package/src/token.ts +0 -21
  57. package/src/workspace.ts +0 -190
  58. package/tools/control.ts +0 -145
  59. package/tools/memory.ts +0 -318
package/src/capture.ts DELETED
@@ -1,137 +0,0 @@
1
- import { Database } from "bun:sqlite"
2
- import { opencodeDbPath } from "./paths.js"
3
- import { MemoryStore, SCAN_LIMIT } from "./store.js"
4
-
5
- export interface SessionRow {
6
- id: string
7
- updated_at: number
8
- directory: string | null
9
- }
10
-
11
- let opencodeDb: Database | null = null
12
-
13
- function openOpencodeDb(): Database | null {
14
- if (opencodeDb) return opencodeDb
15
- const p = opencodeDbPath()
16
- try {
17
- opencodeDb = new Database(p, { readonly: true })
18
- } catch {
19
- opencodeDb = null
20
- }
21
- return opencodeDb
22
- }
23
-
24
- export function listRecentSessions(limit: number = SCAN_LIMIT): SessionRow[] {
25
- const db = openOpencodeDb()
26
- if (!db) return []
27
- try {
28
- // Top-level sessions only: task-tool children are summarized into their
29
- // parent, and the plugin's own sub-sessions must never be memorized.
30
- return db
31
- .prepare(
32
- `SELECT id, time_updated AS updated_at, directory FROM session
33
- WHERE parent_id IS NULL AND title NOT LIKE 'codex-memory-%'
34
- ORDER BY time_updated DESC LIMIT ?`,
35
- )
36
- .all(limit) as SessionRow[]
37
- } catch {
38
- return []
39
- }
40
- }
41
-
42
- export interface TranscriptMessage {
43
- type: string
44
- role?: string
45
- text?: string
46
- seq: number
47
- }
48
-
49
- export function loadTranscript(sessionId: string): TranscriptMessage[] {
50
- const db = openOpencodeDb()
51
- if (!db) return []
52
- try {
53
- const rows = db
54
- .prepare(
55
- `SELECT p.data, m.data AS msg_data
56
- FROM part p
57
- JOIN message m ON p.message_id = m.id
58
- WHERE p.session_id = ?
59
- ORDER BY p.time_created ASC`,
60
- )
61
- .all(sessionId) as { data: string; msg_data: string }[]
62
- return rows.map((r, i) => {
63
- let parsed: any = {}
64
- try {
65
- parsed = JSON.parse(r.data)
66
- } catch {
67
- }
68
- let role: string | undefined
69
- try {
70
- const msg = JSON.parse(r.msg_data)
71
- role = msg.role
72
- } catch {
73
- }
74
- return {
75
- seq: i,
76
- type: parsed.type ?? "unknown",
77
- role,
78
- text: extractText(parsed),
79
- }
80
- })
81
- } catch {
82
- return []
83
- }
84
- }
85
-
86
- function extractText(msg: any): string | undefined {
87
- if (!msg) return undefined
88
- if (typeof msg.text === "string") return msg.text
89
- if (msg.type === "tool") {
90
- // Full tool payloads: codex serializes complete FunctionCall/Output items
91
- // and relies solely on the global transcript truncation. Tool outputs are
92
- // the extractor's strongest evidence — do not slice them per call.
93
- const tool = msg.tool ?? "unknown"
94
- const input = msg.state?.input ? JSON.stringify(msg.state.input) : ""
95
- const output = typeof msg.state?.output === "string" ? msg.state.output : ""
96
- return `[tool: ${tool}] ${input}${output ? "\n" + output : ""}`
97
- }
98
- if (msg.type === "step-start" || msg.type === "step-finish") return undefined
99
- if (Array.isArray(msg.parts)) {
100
- return msg.parts
101
- .filter((p: any) => p?.type === "text" && typeof p.text === "string")
102
- .map((p: any) => p.text)
103
- .join("\n")
104
- }
105
- if (Array.isArray(msg.content)) {
106
- return msg.content
107
- .filter((c: any) => typeof c === "string" || typeof c?.text === "string")
108
- .map((c: any) => (typeof c === "string" ? c : c.text))
109
- .join("\n")
110
- }
111
- return undefined
112
- }
113
-
114
- export interface EligibilityOptions {
115
- maxAgeDays: number
116
- minIdleHours: number
117
- excludeSession?: string
118
- }
119
-
120
- export function selectEligibleSessions(
121
- store: MemoryStore,
122
- opts: EligibilityOptions,
123
- ): SessionRow[] {
124
- const now = Date.now()
125
- const minUpdated = now - opts.maxAgeDays * 24 * 60 * 60 * 1000
126
- const maxUpdated = now - opts.minIdleHours * 60 * 60 * 1000
127
- const sessions = listRecentSessions()
128
- return sessions.filter((s) => {
129
- if (opts.excludeSession && s.id === opts.excludeSession) return false
130
- if (s.updated_at < minUpdated) return false
131
- if (s.updated_at > maxUpdated) return false
132
- const mode = store.getMemoryMode(s.id)
133
- if (mode === "disabled") return false
134
- if (store.isPolluted(s.id)) return false
135
- return true
136
- })
137
- }
package/src/citation.ts DELETED
@@ -1,94 +0,0 @@
1
- export interface MemoryCitationEntry {
2
- path: string
3
- lineStart: number
4
- lineEnd: number
5
- note: string
6
- }
7
-
8
- export interface ParsedCitation {
9
- sessionIds: string[]
10
- entries: MemoryCitationEntry[]
11
- raw: string
12
- }
13
-
14
- const CITATION_BLOCK_RE = /<memory-citation>[\s\S]*?<\/memory-citation>/gi
15
-
16
- function extractSection(block: string, name: string): string | null {
17
- const m = block.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "i"))
18
- return m ? m[1] : null
19
- }
20
-
21
- function parseEntry(line: string): MemoryCitationEntry | null {
22
- const trimmed = line.trim()
23
- if (!trimmed) return null
24
- const noteSplit = trimmed.lastIndexOf("|note=[")
25
- if (noteSplit === -1 || !trimmed.endsWith("]")) return null
26
- const location = trimmed.slice(0, noteSplit)
27
- const note = trimmed.slice(noteSplit + "|note=[".length, -1).trim()
28
- const colon = location.lastIndexOf(":")
29
- if (colon === -1) return null
30
- const path = location.slice(0, colon).trim()
31
- const range = location.slice(colon + 1)
32
- const dash = range.indexOf("-")
33
- if (dash === -1) return null
34
- const lineStart = Number.parseInt(range.slice(0, dash).trim(), 10)
35
- const lineEnd = Number.parseInt(range.slice(dash + 1).trim(), 10)
36
- if (!path || Number.isNaN(lineStart) || Number.isNaN(lineEnd)) return null
37
- return { path, lineStart, lineEnd, note }
38
- }
39
-
40
- export function parseCitations(text: string): ParsedCitation[] {
41
- const results: ParsedCitation[] = []
42
- const re = new RegExp(CITATION_BLOCK_RE)
43
- let m: RegExpExecArray | null
44
- while ((m = re.exec(text)) !== null) {
45
- const raw = m[0]
46
- const entries: MemoryCitationEntry[] = []
47
- const sessionIds: string[] = []
48
- const seen = new Set<string>()
49
-
50
- const entriesBlock = extractSection(raw, "citation_entries")
51
- if (entriesBlock) {
52
- for (const line of entriesBlock.split(/\r?\n/)) {
53
- const entry = parseEntry(line)
54
- if (entry) entries.push(entry)
55
- }
56
- }
57
-
58
- const idsBlock = extractSection(raw, "session_ids")
59
- if (idsBlock) {
60
- for (const line of idsBlock.split(/\r?\n/)) {
61
- const id = line.trim()
62
- if (id && !seen.has(id)) {
63
- seen.add(id)
64
- sessionIds.push(id)
65
- }
66
- }
67
- } else if (entriesBlock && entries.length === 0) {
68
- // Legacy format: <citation_entries> held a comma-separated session-id list.
69
- for (const id of entriesBlock.split(",").map((s) => s.trim()).filter(Boolean)) {
70
- if (!seen.has(id)) {
71
- seen.add(id)
72
- sessionIds.push(id)
73
- }
74
- }
75
- }
76
-
77
- if (entries.length > 0 || sessionIds.length > 0) {
78
- results.push({ sessionIds, entries, raw })
79
- }
80
- }
81
- return results
82
- }
83
-
84
- export function extractCitedSessionIds(text: string): string[] {
85
- const seen = new Set<string>()
86
- for (const c of parseCitations(text)) {
87
- for (const id of c.sessionIds) seen.add(id)
88
- }
89
- return Array.from(seen)
90
- }
91
-
92
- export function stripCitations(text: string): string {
93
- return text.replace(CITATION_BLOCK_RE, "").replace(/[ \t]*\n{3,}/g, "\n\n").trimEnd()
94
- }
package/src/db.ts DELETED
@@ -1,84 +0,0 @@
1
- import { Database } from "bun:sqlite"
2
- import { memoryDbPath } from "./paths.js"
3
-
4
- export const SCHEMA_V1 = [
5
- `CREATE TABLE IF NOT EXISTS memory_stage1_outputs (
6
- session_id TEXT PRIMARY KEY,
7
- source_updated_at INTEGER NOT NULL,
8
- raw_memory TEXT NOT NULL,
9
- rollout_summary TEXT NOT NULL,
10
- rollout_slug TEXT,
11
- cwd TEXT,
12
- generated_at INTEGER NOT NULL,
13
- usage_count INTEGER DEFAULT 0,
14
- last_usage INTEGER,
15
- selected_for_phase2 INTEGER NOT NULL DEFAULT 0,
16
- selected_for_phase2_source_updated_at INTEGER
17
- )`,
18
- `CREATE INDEX IF NOT EXISTS idx_memory_stage1_source_updated_at
19
- ON memory_stage1_outputs(source_updated_at DESC, session_id DESC)`,
20
- `CREATE TABLE IF NOT EXISTS memory_jobs (
21
- kind TEXT NOT NULL,
22
- job_key TEXT NOT NULL,
23
- status TEXT NOT NULL,
24
- worker_id TEXT,
25
- ownership_token TEXT,
26
- started_at INTEGER,
27
- finished_at INTEGER,
28
- lease_until INTEGER,
29
- retry_at INTEGER,
30
- retry_remaining INTEGER NOT NULL,
31
- last_error TEXT,
32
- input_watermark INTEGER,
33
- last_success_watermark INTEGER,
34
- PRIMARY KEY (kind, job_key)
35
- )`,
36
- `CREATE INDEX IF NOT EXISTS idx_memory_jobs_kind_status_retry_lease
37
- ON memory_jobs(kind, status, retry_at, lease_until)`,
38
- `CREATE TABLE IF NOT EXISTS memory_session_meta (
39
- session_id TEXT PRIMARY KEY,
40
- memory_mode TEXT NOT NULL DEFAULT 'enabled',
41
- polluted INTEGER NOT NULL DEFAULT 0,
42
- updated_at INTEGER NOT NULL
43
- )`,
44
- ]
45
-
46
- let dbInstance: Database | null = null
47
-
48
- export function openDb(): Database {
49
- if (dbInstance) return dbInstance
50
- const dbPath = memoryDbPath()
51
- const db = new Database(dbPath, { create: true, readwrite: true, strict: false })
52
- // Match codex's memories-DB open options (runtime.rs): WAL, NORMAL sync,
53
- // 5s busy timeout for cross-process access, incremental auto-vacuum.
54
- db.exec("PRAGMA journal_mode=WAL")
55
- db.exec("PRAGMA synchronous=NORMAL")
56
- db.exec("PRAGMA busy_timeout=5000")
57
- db.exec("PRAGMA auto_vacuum=INCREMENTAL")
58
- runMigrations(db)
59
- dbInstance = db
60
- return db
61
- }
62
-
63
- function runMigrations(db: Database): void {
64
- db.exec(`CREATE TABLE IF NOT EXISTS schema_version (
65
- version INTEGER NOT NULL,
66
- applied_at INTEGER NOT NULL
67
- )`)
68
- const current = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get() as { version: number } | null
69
- const currentVersion = current?.version ?? 0
70
- if (currentVersion >= 1) return
71
- for (const stmt of SCHEMA_V1) db.exec(stmt)
72
- db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(1, Date.now())
73
- }
74
-
75
- export function closeDb(): void {
76
- if (dbInstance) {
77
- dbInstance.close()
78
- dbInstance = null
79
- }
80
- }
81
-
82
- export function resetDbForTest(): void {
83
- closeDb()
84
- }
@@ -1,162 +0,0 @@
1
- import fs from "fs"
2
- import path from "path"
3
- import { memoryRoot } from "./paths.js"
4
- import * as isogit from "isomorphic-git"
5
- import { createPatch } from "diff"
6
-
7
- const AUTHOR = { name: "opencode-codex-memory", email: "memory@opencode.local" }
8
-
9
- // Generated prompt artifact; removed before diffing and before baseline
10
- // commits (mirrors codex's remove_workspace_diff) so it never enters the
11
- // baseline history or shows up as memory content.
12
- export const DIFF_ARTIFACT = "phase2_workspace_diff.md"
13
-
14
- export interface WorkspaceChange {
15
- status: "A" | "M" | "D"
16
- path: string
17
- }
18
-
19
- export interface WorkspaceDiff {
20
- changes: WorkspaceChange[]
21
- unifiedDiff: string
22
- }
23
-
24
- function removeDiffArtifact(dir: string): void {
25
- try {
26
- fs.unlinkSync(path.join(dir, DIFF_ARTIFACT))
27
- } catch {
28
- }
29
- }
30
-
31
- async function ensureInit(dir: string): Promise<void> {
32
- const gitDir = path.join(dir, ".git")
33
- if (!fs.existsSync(gitDir)) {
34
- await isogit.init({ fs, dir })
35
- }
36
- }
37
-
38
- // statusMatrix rows are [filepath, head, workdir, stage]; head !== workdir
39
- // means the working tree differs from HEAD (added, modified, or deleted).
40
- async function stageAll(dir: string): Promise<number> {
41
- const matrix = await isogit.statusMatrix({ fs, dir })
42
- let changes = 0
43
- for (const [filepath, head, workdir, stage] of matrix) {
44
- if (head === 1 && workdir === 1 && stage === 1) continue
45
- if (workdir === 0) {
46
- // isogit.add throws on deleted files; they must be staged via remove
47
- await isogit.remove({ fs, dir, filepath })
48
- } else {
49
- await isogit.add({ fs, dir, filepath })
50
- }
51
- if (head !== workdir) changes++
52
- }
53
- return changes
54
- }
55
-
56
- async function hasHeadCommit(dir: string): Promise<boolean> {
57
- try {
58
- await isogit.resolveRef({ fs, dir, ref: "HEAD" })
59
- return true
60
- } catch {
61
- return false
62
- }
63
- }
64
-
65
- async function commitBaseline(dir: string): Promise<string> {
66
- await stageAll(dir)
67
- return isogit.commit({ fs, dir, message: "memory baseline", author: AUTHOR })
68
- }
69
-
70
- /**
71
- * Mirrors codex prepare_memory_workspace: an existing baseline is preserved
72
- * untouched so the phase-2 diff spans last-successful-run -> now — including
73
- * manual user edits and newly added ad-hoc notes. Committing here would
74
- * swallow those changes and consolidation would never see them. Only a root
75
- * without any commit gets a fresh baseline.
76
- */
77
- export async function ensureBaseline(): Promise<boolean> {
78
- try {
79
- const dir = memoryRoot()
80
- removeDiffArtifact(dir)
81
- await ensureInit(dir)
82
- if (!(await hasHeadCommit(dir))) {
83
- await commitBaseline(dir)
84
- }
85
- return true
86
- } catch (err) {
87
- console.error("[opencode-codex-memory] ensureBaseline error:", err)
88
- return false
89
- }
90
- }
91
-
92
- async function readBaselineText(dir: string, headOid: string, filepath: string): Promise<string> {
93
- try {
94
- const { blob } = await isogit.readBlob({ fs, dir, oid: headOid, filepath })
95
- return new TextDecoder().decode(blob)
96
- } catch {
97
- return ""
98
- }
99
- }
100
-
101
- function readWorkdirText(dir: string, filepath: string): string {
102
- try {
103
- return fs.readFileSync(path.join(dir, filepath), "utf8")
104
- } catch {
105
- return ""
106
- }
107
- }
108
-
109
- export async function captureWorkspaceDiff(): Promise<WorkspaceDiff> {
110
- try {
111
- const dir = memoryRoot()
112
- await ensureInit(dir)
113
- removeDiffArtifact(dir)
114
- const matrix = await isogit.statusMatrix({ fs, dir })
115
- const changedRows = matrix.filter(
116
- ([filepath, head, workdir]) => head !== workdir && filepath !== DIFF_ARTIFACT,
117
- )
118
- const changes: WorkspaceChange[] = changedRows.map(([filepath, head, workdir]) => {
119
- if (head === 0) return { status: "A", path: filepath }
120
- if (workdir === 0) return { status: "D", path: filepath }
121
- return { status: "M", path: filepath }
122
- })
123
-
124
- let headOid: string | null = null
125
- try {
126
- headOid = await isogit.resolveRef({ fs, dir, ref: "HEAD" })
127
- } catch {
128
- // no commits yet — every file diffs against empty
129
- }
130
- const patches: string[] = []
131
- for (const [filepath, head, workdir] of changedRows) {
132
- const oldText = head === 1 && headOid ? await readBaselineText(dir, headOid, filepath) : ""
133
- const newText = workdir === 0 ? "" : readWorkdirText(dir, filepath)
134
- // No per-file cap: codex renders every file's patch in full and relies
135
- // on the global 4 MiB truncation in writeWorkspaceDiff.
136
- patches.push(createPatch(filepath, oldText, newText))
137
- }
138
- return { changes, unifiedDiff: patches.join("\n") }
139
- } catch (err) {
140
- console.error("[opencode-codex-memory] captureWorkspaceDiff error:", err)
141
- return { changes: [], unifiedDiff: "" }
142
- }
143
- }
144
-
145
- /**
146
- * Mirrors codex reset_git_repository: delete .git and re-create a fresh
147
- * single-commit baseline so deleted/redacted memory content is not retained
148
- * in unreachable git objects (history is intentionally dropped).
149
- */
150
- export async function resetBaseline(): Promise<boolean> {
151
- try {
152
- const dir = memoryRoot()
153
- removeDiffArtifact(dir)
154
- fs.rmSync(path.join(dir, ".git"), { recursive: true, force: true })
155
- await isogit.init({ fs, dir })
156
- await commitBaseline(dir)
157
- return true
158
- } catch (err) {
159
- console.error("[opencode-codex-memory] resetBaseline error:", err)
160
- return false
161
- }
162
- }