opencode-codex-memory 0.4.4 → 0.4.6
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 +5 -3
- package/dist/src/llm.js +17 -0
- package/dist/src/store.d.ts +16 -1
- package/dist/src/store.js +33 -10
- package/dist/tools/control.js +43 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ project is a faithful port of the memory system in OpenAI's Codex. It works out
|
|
|
18
18
|
of the box with zero extra configuration and uses whatever models you already
|
|
19
19
|
have set up in OpenCode.
|
|
20
20
|
|
|
21
|
+
See the [changelog](./CHANGELOG.md) for release history.
|
|
22
|
+
|
|
21
23
|
**Local-first by design.** Memory is plain markdown files plus a small SQLite
|
|
22
24
|
database on your own machine — no memory service to sign up for, no MCP server,
|
|
23
25
|
no separate process, no sync. Installing it is one line in your `opencode.json`;
|
|
@@ -50,7 +52,7 @@ If you want the mental model before the details, jump to
|
|
|
50
52
|
|
|
51
53
|
```json
|
|
52
54
|
{
|
|
53
|
-
"plugin": ["opencode-codex-memory@0.4.
|
|
55
|
+
"plugin": ["opencode-codex-memory@0.4.6"]
|
|
54
56
|
}
|
|
55
57
|
```
|
|
56
58
|
|
|
@@ -237,7 +239,7 @@ To set options, turn the plugin entry into a `[name, options]` pair:
|
|
|
237
239
|
```json
|
|
238
240
|
{
|
|
239
241
|
"plugin": [
|
|
240
|
-
["opencode-codex-memory@0.4.
|
|
242
|
+
["opencode-codex-memory@0.4.6", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
|
|
241
243
|
]
|
|
242
244
|
}
|
|
243
245
|
```
|
|
@@ -296,7 +298,7 @@ directions:
|
|
|
296
298
|
```json
|
|
297
299
|
{
|
|
298
300
|
"plugin": [
|
|
299
|
-
["opencode-codex-memory@0.4.
|
|
301
|
+
["opencode-codex-memory@0.4.6", { "codex_interop": { "import": true, "export": true } }]
|
|
300
302
|
]
|
|
301
303
|
}
|
|
302
304
|
```
|
package/dist/src/llm.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { memoryRoot } from "./paths.js";
|
|
3
4
|
let inputRef = null;
|
|
4
5
|
export function setPluginInput(input) {
|
|
5
6
|
inputRef = input;
|
|
@@ -20,11 +21,27 @@ const SUBSESSION_DELETE_TIMEOUT_MS = 10_000;
|
|
|
20
21
|
export function isMemorySubSession(sessionId) {
|
|
21
22
|
return activeSubSessions.has(sessionId);
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Host directory for memory sub-sessions. Must exist: OpenCode resolves it in
|
|
26
|
+
* SystemPrompt.environment and fails the turn with UnknownError/ENOENT when
|
|
27
|
+
* missing. Prefer the memory workspace itself — global, always ours, already
|
|
28
|
+
* granted to `memorize` via external_directory, and independent of whatever
|
|
29
|
+
* (possibly deleted) project PluginInput.directory points at.
|
|
30
|
+
*/
|
|
31
|
+
function resolveSubSessionDirectory() {
|
|
32
|
+
const root = memoryRoot();
|
|
33
|
+
fs.mkdirSync(root, { recursive: true });
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
23
36
|
async function createSession(agent, title) {
|
|
24
37
|
const input = getPluginInput();
|
|
25
38
|
if (!input)
|
|
26
39
|
throw new Error("plugin input not initialized");
|
|
40
|
+
const directory = resolveSubSessionDirectory();
|
|
27
41
|
const res = await input.client.session.create({
|
|
42
|
+
// directory is a query param (not body); without it the client inherits
|
|
43
|
+
// PluginInput.directory, which may be a deleted project path.
|
|
44
|
+
query: { directory },
|
|
28
45
|
body: {
|
|
29
46
|
title: title ?? `codex-memory-${agent}`,
|
|
30
47
|
metadata: { [SUBSESSION_METADATA_KEY]: true },
|
package/dist/src/store.d.ts
CHANGED
|
@@ -70,7 +70,22 @@ export declare class MemoryStore {
|
|
|
70
70
|
* still back the consolidated artifacts.
|
|
71
71
|
*/
|
|
72
72
|
markPhase2Succeeded(ownershipToken: string, selected?: Pick<Stage1Output, "session_id" | "source_updated_at">[]): void;
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Phase-2 job snapshot for memory_inspect. Always returns the global job row
|
|
75
|
+
* when it exists (including failed/running), so diagnostics are not limited
|
|
76
|
+
* to clean successes. `success_finished_at` is set only for a clean success
|
|
77
|
+
* (never a failure timestamp); `last_success_watermark` follows codex
|
|
78
|
+
* (preserved across later attempts; zero only counts while clean).
|
|
79
|
+
*/
|
|
80
|
+
phase2JobSnapshot(): {
|
|
81
|
+
status: string;
|
|
82
|
+
last_error: string | null;
|
|
83
|
+
finished_at: number | null;
|
|
84
|
+
retry_at: number | null;
|
|
85
|
+
success_finished_at: number | null;
|
|
86
|
+
last_success_watermark: number | null;
|
|
87
|
+
} | null;
|
|
88
|
+
/** Last recorded phase-2 success info. Null when phase 2 never succeeded. */
|
|
74
89
|
phase2LastSuccess(): {
|
|
75
90
|
finished_at: number | null;
|
|
76
91
|
last_success_watermark: number | null;
|
package/dist/src/store.js
CHANGED
|
@@ -311,27 +311,50 @@ export class MemoryStore {
|
|
|
311
311
|
mark.run(s.source_updated_at, s.session_id, s.source_updated_at);
|
|
312
312
|
}).immediate();
|
|
313
313
|
}
|
|
314
|
-
/**
|
|
315
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Phase-2 job snapshot for memory_inspect. Always returns the global job row
|
|
316
|
+
* when it exists (including failed/running), so diagnostics are not limited
|
|
317
|
+
* to clean successes. `success_finished_at` is set only for a clean success
|
|
318
|
+
* (never a failure timestamp); `last_success_watermark` follows codex
|
|
319
|
+
* (preserved across later attempts; zero only counts while clean).
|
|
320
|
+
*/
|
|
321
|
+
phase2JobSnapshot() {
|
|
316
322
|
const row = this.db
|
|
317
|
-
.prepare(`SELECT status, finished_at, last_error, last_success_watermark FROM memory_jobs
|
|
323
|
+
.prepare(`SELECT status, finished_at, last_error, retry_at, last_success_watermark FROM memory_jobs
|
|
318
324
|
WHERE kind='memory_consolidate_global' AND job_key='global'`)
|
|
319
325
|
.get();
|
|
320
|
-
if (!row
|
|
326
|
+
if (!row)
|
|
321
327
|
return null;
|
|
322
328
|
const cleanSuccess = row.last_error === null &&
|
|
323
329
|
row.finished_at !== null &&
|
|
324
330
|
(row.status === "done" || row.status === "pending");
|
|
325
331
|
// Codex initializes pending global jobs with watermark 0, so zero proves a
|
|
326
332
|
// success only while the row itself is a clean completed attempt.
|
|
327
|
-
|
|
333
|
+
const watermark = row.last_success_watermark === null
|
|
334
|
+
? null
|
|
335
|
+
: row.last_success_watermark === 0 && !cleanSuccess
|
|
336
|
+
? null
|
|
337
|
+
: row.last_success_watermark;
|
|
338
|
+
return {
|
|
339
|
+
status: row.status,
|
|
340
|
+
last_error: row.last_error,
|
|
341
|
+
finished_at: row.finished_at,
|
|
342
|
+
retry_at: row.retry_at,
|
|
343
|
+
// Codex preserves last_success_watermark across later attempts, while the
|
|
344
|
+
// job finished_at describes only the latest attempt. Never label a failure
|
|
345
|
+
// timestamp as a success finish time.
|
|
346
|
+
success_finished_at: cleanSuccess ? row.finished_at : null,
|
|
347
|
+
last_success_watermark: watermark,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/** Last recorded phase-2 success info. Null when phase 2 never succeeded. */
|
|
351
|
+
phase2LastSuccess() {
|
|
352
|
+
const snap = this.phase2JobSnapshot();
|
|
353
|
+
if (!snap || snap.last_success_watermark === null)
|
|
328
354
|
return null;
|
|
329
355
|
return {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// so a failure timestamp is never labeled as a success timestamp.
|
|
333
|
-
finished_at: cleanSuccess ? row.finished_at : null,
|
|
334
|
-
last_success_watermark: row.last_success_watermark,
|
|
356
|
+
finished_at: snap.success_finished_at,
|
|
357
|
+
last_success_watermark: snap.last_success_watermark,
|
|
335
358
|
};
|
|
336
359
|
}
|
|
337
360
|
markPhase2Failed(ownershipToken, error) {
|
package/dist/tools/control.js
CHANGED
|
@@ -150,11 +150,22 @@ export const memory_reset = tool({
|
|
|
150
150
|
}
|
|
151
151
|
},
|
|
152
152
|
});
|
|
153
|
+
function fmtUnixSec(sec) {
|
|
154
|
+
return sec ? new Date(sec * 1000).toISOString() : "none";
|
|
155
|
+
}
|
|
156
|
+
function fmtWatermarkMs(ms) {
|
|
157
|
+
if (ms === 0)
|
|
158
|
+
return "0 (no consumed inputs)";
|
|
159
|
+
if (ms === null || ms === undefined)
|
|
160
|
+
return "none";
|
|
161
|
+
return new Date(ms).toISOString();
|
|
162
|
+
}
|
|
153
163
|
export const memory_inspect = tool({
|
|
154
|
-
description: "Inspect the current memory state. Returns: stage1_outputs count,
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"configuration
|
|
164
|
+
description: "Inspect the current memory state. Returns: stage1_outputs count, Phase 2 job status " +
|
|
165
|
+
"(including last error / retry time when failed), last Phase 2 success watermark, " +
|
|
166
|
+
"memory_summary token estimate (on-disk; injection caps at ~2500), a listing of the " +
|
|
167
|
+
"memories directory, the effective plugin options, and any configuration warnings " +
|
|
168
|
+
"(unknown/malformed options). Use it to verify the plugin configuration took effect. Read-only.",
|
|
158
169
|
args: {},
|
|
159
170
|
async execute() {
|
|
160
171
|
try {
|
|
@@ -171,20 +182,30 @@ export const memory_inspect = tool({
|
|
|
171
182
|
summaryTokens = estimateTokens(text);
|
|
172
183
|
}
|
|
173
184
|
const listing = listMemoriesDir();
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
:
|
|
181
|
-
|
|
185
|
+
const phase2 = store.phase2JobSnapshot();
|
|
186
|
+
const phase2Lines = phase2
|
|
187
|
+
? [
|
|
188
|
+
`phase2_status: ${phase2.status}`,
|
|
189
|
+
`phase2_last_error: ${phase2.last_error ?? "none"}`,
|
|
190
|
+
`phase2_retry_at: ${fmtUnixSec(phase2.retry_at)}`,
|
|
191
|
+
`phase2_last_attempt_finished_at: ${fmtUnixSec(phase2.finished_at)}`,
|
|
192
|
+
`phase2_last_success_watermark: ${fmtWatermarkMs(phase2.last_success_watermark)}`,
|
|
193
|
+
// Clean-success finish only — never a failure timestamp.
|
|
194
|
+
`phase2_last_success_finished_at: ${fmtUnixSec(phase2.success_finished_at)}`,
|
|
195
|
+
]
|
|
196
|
+
: [
|
|
197
|
+
"phase2_status: none",
|
|
198
|
+
"phase2_last_error: none",
|
|
199
|
+
"phase2_retry_at: none",
|
|
200
|
+
"phase2_last_attempt_finished_at: none",
|
|
201
|
+
"phase2_last_success_watermark: none",
|
|
202
|
+
"phase2_last_success_finished_at: none",
|
|
203
|
+
];
|
|
182
204
|
const out = [
|
|
183
205
|
`stage1_outputs: ${outputs.length}`,
|
|
184
|
-
|
|
185
|
-
`phase2_last_finished_at: ${finishedAt}`,
|
|
206
|
+
...phase2Lines,
|
|
186
207
|
`memory_summary_chars: ${summaryChars}`,
|
|
187
|
-
`memory_summary_tokens_est: ${summaryTokens}`,
|
|
208
|
+
`memory_summary_tokens_est: ${summaryTokens} (on disk; injection caps at ~2500)`,
|
|
188
209
|
`memories_dir_entries: ${listing.length}`,
|
|
189
210
|
"",
|
|
190
211
|
...renderEffectiveConfig(),
|
|
@@ -196,8 +217,14 @@ export const memory_inspect = tool({
|
|
|
196
217
|
output: out,
|
|
197
218
|
metadata: {
|
|
198
219
|
stage1_count: outputs.length,
|
|
220
|
+
phase2_status: phase2?.status ?? null,
|
|
221
|
+
phase2_last_error: phase2?.last_error ?? null,
|
|
222
|
+
phase2_retry_at: phase2?.retry_at ?? null,
|
|
223
|
+
phase2_last_attempt_finished_at: phase2?.finished_at ?? null,
|
|
199
224
|
phase2_last_success_watermark: phase2?.last_success_watermark ?? null,
|
|
200
|
-
|
|
225
|
+
phase2_last_success_finished_at: phase2?.success_finished_at ?? null,
|
|
226
|
+
// Back-compat aliases used by earlier inspect consumers.
|
|
227
|
+
phase2_last_finished_at: phase2?.success_finished_at ?? null,
|
|
201
228
|
summary_chars: summaryChars,
|
|
202
229
|
summary_tokens_est: summaryTokens,
|
|
203
230
|
files: listing,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-codex-memory",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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",
|