opencode-codex-memory 0.2.1 → 0.3.1
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.
- package/README.md +8 -4
- package/dist/opencode.json +2 -1
- package/dist/src/llm.d.ts +10 -1
- package/dist/src/llm.js +55 -14
- package/dist/src/paths.js +19 -1
- package/opencode.json +2 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -104,6 +104,10 @@ echo 'I prefer TypeScript strict mode and 2-space indentation.' \
|
|
|
104
104
|
└── extensions/ad_hoc/notes/ # things you explicitly asked it to remember
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
The location follows opencode's own data directory — `$XDG_DATA_HOME/opencode`
|
|
108
|
+
when that's set, otherwise `~/.local/share/opencode` (same resolution on macOS,
|
|
109
|
+
Linux, and Windows).
|
|
110
|
+
|
|
107
111
|
It's all plain files and a local SQLite database. Read them, edit them, delete
|
|
108
112
|
them — it's yours. (The `memories/` folder also holds a few working files and
|
|
109
113
|
an internal `.git/` the plugin uses for change tracking; `memory_reset` wipes
|
|
@@ -117,10 +121,10 @@ those too.)
|
|
|
117
121
|
session transcripts and extracted memories before anything is written or sent
|
|
118
122
|
to a model. Notes you explicitly dictate ("remember that ...") are stored as
|
|
119
123
|
you said them.
|
|
120
|
-
- **The learning agents are sandboxed** — the extraction agent
|
|
121
|
-
|
|
122
|
-
only file tools plus access to the
|
|
123
|
-
MCP tools are denied for both.
|
|
124
|
+
- **The learning agents are sandboxed** — the extraction agent can't touch your
|
|
125
|
+
filesystem (the transcript is handed to it inline; it only emits its structured
|
|
126
|
+
result), and the consolidation agent gets only file tools plus access to the
|
|
127
|
+
memory folder. Shell, network, IDE, and MCP tools are denied for both.
|
|
124
128
|
- **Reset is safe.** `memory_reset` refuses to run if the memory folder is a
|
|
125
129
|
symlink, so it can't be tricked into deleting something else.
|
|
126
130
|
- **Web/MCP sessions:** by default, sessions that used web search, fetch, or MCP
|
package/dist/opencode.json
CHANGED
package/dist/src/llm.d.ts
CHANGED
|
@@ -17,5 +17,14 @@ export declare function consolidateViaSubagent(memoryRoot: string, diffFileName:
|
|
|
17
17
|
export declare function cleanupOldSubSessions(maxAgeMinutes?: number): Promise<void>;
|
|
18
18
|
export declare function fillTemplate(tmpl: string, vars: Record<string, string>): string;
|
|
19
19
|
export declare function buildConsolidationPrompt(memoryRoot: string, diffFileName: string): string;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Validates a parsed stage-1 object into an ExtractionResult, or null for the
|
|
22
|
+
* all-empty no-op. Shared by the structured-output path (AssistantMessage.
|
|
23
|
+
* structured) and the text parser below.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateExtraction(obj: Partial<ExtractionResult>): ExtractionResult | null;
|
|
26
|
+
/**
|
|
27
|
+
* Parses stage-1 JSON from assistant text. Fallback for when structured output
|
|
28
|
+
* is unavailable; the primary path reads AssistantMessage.structured directly.
|
|
29
|
+
*/
|
|
21
30
|
export declare function parseExtraction(raw: string): ExtractionResult | null;
|
package/dist/src/llm.js
CHANGED
|
@@ -63,7 +63,8 @@ function parseModelRef(ref) {
|
|
|
63
63
|
return null;
|
|
64
64
|
return { providerID: ref.slice(0, slash), modelID: ref.slice(slash + 1) };
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
/** Runs a sub-agent prompt and returns the raw response data (`{ info, parts }`). */
|
|
67
|
+
async function runPrompt(sessionId, prompt, agent, opts = {}) {
|
|
67
68
|
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
68
69
|
const input = getPluginInput();
|
|
69
70
|
if (!input)
|
|
@@ -71,10 +72,13 @@ async function promptSession(sessionId, prompt, agent, opts = {}) {
|
|
|
71
72
|
const model = opts.model ? parseModelRef(opts.model) : null;
|
|
72
73
|
const promptPromise = input.client.session.prompt({
|
|
73
74
|
path: { id: sessionId },
|
|
75
|
+
// `format` lives in the server's PromptInput but not the generated SDK body
|
|
76
|
+
// type yet (same OpenAPI lag as session.list scope/roots), hence the cast.
|
|
74
77
|
body: {
|
|
75
78
|
agent,
|
|
76
79
|
...(opts.system ? { system: opts.system } : {}),
|
|
77
80
|
...(model ? { model } : {}),
|
|
81
|
+
...(opts.format ? { format: opts.format } : {}),
|
|
78
82
|
parts: [{ type: "text", text: prompt }],
|
|
79
83
|
},
|
|
80
84
|
});
|
|
@@ -88,12 +92,15 @@ async function promptSession(sessionId, prompt, agent, opts = {}) {
|
|
|
88
92
|
]);
|
|
89
93
|
if (!res.data)
|
|
90
94
|
throw new Error(`prompt failed: ${JSON.stringify(res.error ?? {})}`);
|
|
91
|
-
return
|
|
95
|
+
return res.data;
|
|
92
96
|
}
|
|
93
97
|
finally {
|
|
94
98
|
clearTimeout(timer);
|
|
95
99
|
}
|
|
96
100
|
}
|
|
101
|
+
async function promptSession(sessionId, prompt, agent, opts = {}) {
|
|
102
|
+
return extractAssistantText(await runPrompt(sessionId, prompt, agent, opts));
|
|
103
|
+
}
|
|
97
104
|
function extractAssistantText(body) {
|
|
98
105
|
if (!body)
|
|
99
106
|
return "";
|
|
@@ -116,6 +123,19 @@ function extractAssistantText(body) {
|
|
|
116
123
|
return body.output;
|
|
117
124
|
return JSON.stringify(body);
|
|
118
125
|
}
|
|
126
|
+
// JSON Schema for structured stage-1 output. Mirrors the deliverables in
|
|
127
|
+
// stage_one_system.md (three required string fields; all-empty = no-op) and is
|
|
128
|
+
// opencode's equivalent of codex's output_schema + output_schema_strict.
|
|
129
|
+
const EXTRACTION_SCHEMA = {
|
|
130
|
+
type: "object",
|
|
131
|
+
additionalProperties: false,
|
|
132
|
+
properties: {
|
|
133
|
+
raw_memory: { type: "string" },
|
|
134
|
+
rollout_summary: { type: "string" },
|
|
135
|
+
rollout_slug: { type: "string" },
|
|
136
|
+
},
|
|
137
|
+
required: ["raw_memory", "rollout_summary", "rollout_slug"],
|
|
138
|
+
};
|
|
119
139
|
/** Returns null when the extractor reported a no-op (nothing worth remembering). */
|
|
120
140
|
export async function extractViaSubagent(sessionId, transcript, opts = {}) {
|
|
121
141
|
const agent = "memorize-extract";
|
|
@@ -124,15 +144,27 @@ export async function extractViaSubagent(sessionId, transcript, opts = {}) {
|
|
|
124
144
|
const prompt = buildExtractionInput(sessionId, opts.cwd ?? "unknown", transcript);
|
|
125
145
|
// extract_model option > opencode small_model > session default.
|
|
126
146
|
const model = opts.model ?? (await getConfigModels()).smallModel;
|
|
127
|
-
const
|
|
147
|
+
const data = await runPrompt(subId, prompt, agent, {
|
|
128
148
|
// Mirrors the stage-1 job lease (1h): codex has no per-request timeout,
|
|
129
149
|
// and a near-600k-char transcript on a slow model can easily exceed a
|
|
130
150
|
// short one — repeated timeouts would exhaust the job's retries.
|
|
131
151
|
timeoutMs: 3600_000,
|
|
132
152
|
system: readTemplate("stage_one_system.md"),
|
|
133
153
|
model,
|
|
154
|
+
// opencode enforces json_schema output via a forced StructuredOutput tool
|
|
155
|
+
// call (toolChoice: required) — which is why memorize-extract must allow
|
|
156
|
+
// that one otherwise-denied tool.
|
|
157
|
+
format: { type: "json_schema", schema: EXTRACTION_SCHEMA },
|
|
134
158
|
});
|
|
135
|
-
|
|
159
|
+
// The captured JSON lands on AssistantMessage.structured (schema
|
|
160
|
+
// v1/session.ts; absent from the generated SDK type, so read it untyped).
|
|
161
|
+
// Fall back to text parsing when structured output is unavailable (a host
|
|
162
|
+
// without the feature, or a model that emitted JSON as plain text).
|
|
163
|
+
const structured = data?.info?.structured;
|
|
164
|
+
if (structured && typeof structured === "object") {
|
|
165
|
+
return validateExtraction(structured);
|
|
166
|
+
}
|
|
167
|
+
return parseExtraction(extractAssistantText(data));
|
|
136
168
|
}
|
|
137
169
|
finally {
|
|
138
170
|
void deleteSession(subId).catch(() => { });
|
|
@@ -257,16 +289,12 @@ export function buildConsolidationPrompt(memoryRoot, diffFileName) {
|
|
|
257
289
|
function readTemplate(name) {
|
|
258
290
|
return fs.readFileSync(path.join(import.meta.dirname, "templates", name), "utf8");
|
|
259
291
|
}
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
throw new Error("extraction response contained no JSON object");
|
|
267
|
-
}
|
|
268
|
-
const json = cleaned.slice(start, end + 1);
|
|
269
|
-
const obj = JSON.parse(json);
|
|
292
|
+
/**
|
|
293
|
+
* Validates a parsed stage-1 object into an ExtractionResult, or null for the
|
|
294
|
+
* all-empty no-op. Shared by the structured-output path (AssistantMessage.
|
|
295
|
+
* structured) and the text parser below.
|
|
296
|
+
*/
|
|
297
|
+
export function validateExtraction(obj) {
|
|
270
298
|
if (typeof obj.raw_memory !== "string" || typeof obj.rollout_summary !== "string") {
|
|
271
299
|
throw new Error("extraction response missing required fields");
|
|
272
300
|
}
|
|
@@ -289,3 +317,16 @@ export function parseExtraction(raw) {
|
|
|
289
317
|
rollout_slug: typeof obj.rollout_slug === "string" && obj.rollout_slug.trim() ? obj.rollout_slug : null,
|
|
290
318
|
};
|
|
291
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Parses stage-1 JSON from assistant text. Fallback for when structured output
|
|
322
|
+
* is unavailable; the primary path reads AssistantMessage.structured directly.
|
|
323
|
+
*/
|
|
324
|
+
export function parseExtraction(raw) {
|
|
325
|
+
const cleaned = raw.replace(/^```(?:json)?/gim, "").replace(/```$/gim, "").trim();
|
|
326
|
+
const start = cleaned.indexOf("{");
|
|
327
|
+
const end = cleaned.lastIndexOf("}");
|
|
328
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
329
|
+
throw new Error("extraction response contained no JSON object");
|
|
330
|
+
}
|
|
331
|
+
return validateExtraction(JSON.parse(cleaned.slice(start, end + 1)));
|
|
332
|
+
}
|
package/dist/src/paths.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import os from "os";
|
|
3
|
+
import { xdgData } from "xdg-basedir";
|
|
3
4
|
const MEMORY_DIR_NAME = "memories";
|
|
4
5
|
const MEMORY_DB_NAME = "memory.db";
|
|
5
6
|
const OVERRIDE_ENV = "OPENCODE_CODEX_MEMORY_TEST_ROOT";
|
|
7
|
+
const OPENCODE_APP_DIR = "opencode";
|
|
8
|
+
/**
|
|
9
|
+
* Memory lives under opencode's data dir, mirroring codex's
|
|
10
|
+
* `<codex_home>/memories` (codex-rs `find_codex_home` + `from_codex_home`) but
|
|
11
|
+
* with the opencode-specific home. opencode resolves its data dir as
|
|
12
|
+
* `path.join(xdgData, "opencode")` via the `xdg-basedir` lib
|
|
13
|
+
* (packages/core/src/global.ts), so we reuse the SAME lib to stay byte-identical
|
|
14
|
+
* across platforms and `XDG_DATA_HOME` overrides. opencode does not surface this
|
|
15
|
+
* directory through the plugin API (`/path` gives home/config/state/worktree/
|
|
16
|
+
* directory, not data), so it must be recomputed here.
|
|
17
|
+
*
|
|
18
|
+
* `OPENCODE_CODEX_MEMORY_TEST_ROOT` is our `CODEX_HOME` analog: an explicit
|
|
19
|
+
* override that wins outright (tests + the write-pipeline sandbox).
|
|
20
|
+
*/
|
|
6
21
|
function dataRoot() {
|
|
7
22
|
const override = process.env[OVERRIDE_ENV];
|
|
8
23
|
if (override)
|
|
9
24
|
return override;
|
|
10
|
-
|
|
25
|
+
// xdgData = XDG_DATA_HOME || ~/.local/share (identical on every platform).
|
|
26
|
+
// The `??` mirrors xdg-basedir's own guard for a missing home directory.
|
|
27
|
+
const base = xdgData ?? path.join(os.homedir(), ".local", "share");
|
|
28
|
+
return path.join(base, OPENCODE_APP_DIR);
|
|
11
29
|
}
|
|
12
30
|
export function memoryRoot() {
|
|
13
31
|
return path.join(dataRoot(), MEMORY_DIR_NAME);
|
package/opencode.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-codex-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Persistent memory plugin for opencode — ports codex's two-phase memory system (extraction → consolidation → injection → citation feedback)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@opencode-ai/plugin": "^1.18.0",
|
|
49
49
|
"diff": "^9.0.0",
|
|
50
|
-
"isomorphic-git": "^1.38.6"
|
|
50
|
+
"isomorphic-git": "^1.38.6",
|
|
51
|
+
"xdg-basedir": "^5.1.0"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"typescript": "^5.5.0",
|