opencode-codex-memory 0.4.8 → 0.4.10
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 +3 -3
- package/dist/src/index.js +3 -8
- package/dist/src/lifecycle.d.ts +11 -5
- package/dist/src/lifecycle.js +18 -5
- package/dist/src/llm.d.ts +13 -1
- package/dist/src/llm.js +15 -1
- package/dist/src/phase1.js +4 -4
- package/dist/src/redact.js +5 -2
- package/dist/tools/memory.js +11 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ If you want the mental model before the details, jump to
|
|
|
52
52
|
|
|
53
53
|
```json
|
|
54
54
|
{
|
|
55
|
-
"plugin": ["opencode-codex-memory@0.4.
|
|
55
|
+
"plugin": ["opencode-codex-memory@0.4.10"]
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -239,7 +239,7 @@ To set options, turn the plugin entry into a `[name, options]` pair:
|
|
|
239
239
|
```json
|
|
240
240
|
{
|
|
241
241
|
"plugin": [
|
|
242
|
-
["opencode-codex-memory@0.4.
|
|
242
|
+
["opencode-codex-memory@0.4.10", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
|
|
243
243
|
]
|
|
244
244
|
}
|
|
245
245
|
```
|
|
@@ -298,7 +298,7 @@ directions:
|
|
|
298
298
|
```json
|
|
299
299
|
{
|
|
300
300
|
"plugin": [
|
|
301
|
-
["opencode-codex-memory@0.4.
|
|
301
|
+
["opencode-codex-memory@0.4.10", { "codex_interop": { "import": true, "export": true } }]
|
|
302
302
|
]
|
|
303
303
|
}
|
|
304
304
|
```
|
package/dist/src/index.js
CHANGED
|
@@ -291,14 +291,9 @@ export function injectAgentDefinitions(config) {
|
|
|
291
291
|
console.warn("[opencode-codex-memory] could not load bundled agent definitions:", err);
|
|
292
292
|
return;
|
|
293
293
|
}
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// trigger that ask. The bundled `"*": "deny"` matches it (permission rules
|
|
298
|
-
// are wildcard-on-name, last match wins), which would block consolidation
|
|
299
|
-
// entirely. Grant the memory root here rather than in opencode.json: the
|
|
300
|
-
// path is homedir/env-dependent (src/paths.ts is its single source of
|
|
301
|
-
// truth). Appended last so it out-ranks the wildcard deny.
|
|
294
|
+
// Sub-sessions use directory=memoryRoot (llm.ts), so memory paths are usually
|
|
295
|
+
// in-bounds. Keep an explicit external_directory allow for the memory root as
|
|
296
|
+
// belt-and-suspenders (path is homedir/env-dependent; out-ranks `"*": deny`).
|
|
302
297
|
const memorize = defs["memorize"];
|
|
303
298
|
if (memorize?.permission && !("external_directory" in memorize.permission)) {
|
|
304
299
|
memorize.permission["external_directory"] = { [path.join(memoryRoot(), "*")]: "allow" };
|
package/dist/src/lifecycle.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin process lifecycle: shutdown flag +
|
|
3
|
-
*
|
|
2
|
+
* Plugin process lifecycle: shutdown flag + abort signals shared by the entry
|
|
3
|
+
* dispose hook and the write pipeline.
|
|
4
4
|
*
|
|
5
5
|
* Opencode can reload plugins while a consolidator helper still holds write
|
|
6
6
|
* access to the memory root. dispose() sets the flag (so new pumps stop),
|
|
7
|
-
* aborts
|
|
8
|
-
*
|
|
7
|
+
* aborts pluginShutdownSignal (extract + any other waiters) and the in-flight
|
|
8
|
+
* phase-2 AbortSignal (consolidateViaSubagent), and best-effort session.aborts
|
|
9
|
+
* active sub-sessions (llm.ts abortActiveSubSessions).
|
|
10
|
+
*
|
|
11
|
+
* Phase-2 keeps its own scope so heartbeat loss can cancel the consolidator
|
|
12
|
+
* without marking the whole plugin as shutting down.
|
|
9
13
|
*/
|
|
10
14
|
export declare function isPluginShuttingDown(): boolean;
|
|
11
|
-
/**
|
|
15
|
+
/** Aborted when dispose begins; replaced on resetPluginLifecycle / re-boot. */
|
|
16
|
+
export declare function pluginShutdownSignal(): AbortSignal;
|
|
17
|
+
/** Begin shutdown: no new phase work, abort extract + consolidator waiters. */
|
|
12
18
|
export declare function beginPluginShutdown(): void;
|
|
13
19
|
/**
|
|
14
20
|
* Test / re-boot seam: a fresh server() call clears the previous dispose.
|
package/dist/src/lifecycle.js
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin process lifecycle: shutdown flag +
|
|
3
|
-
*
|
|
2
|
+
* Plugin process lifecycle: shutdown flag + abort signals shared by the entry
|
|
3
|
+
* dispose hook and the write pipeline.
|
|
4
4
|
*
|
|
5
5
|
* Opencode can reload plugins while a consolidator helper still holds write
|
|
6
6
|
* access to the memory root. dispose() sets the flag (so new pumps stop),
|
|
7
|
-
* aborts
|
|
8
|
-
*
|
|
7
|
+
* aborts pluginShutdownSignal (extract + any other waiters) and the in-flight
|
|
8
|
+
* phase-2 AbortSignal (consolidateViaSubagent), and best-effort session.aborts
|
|
9
|
+
* active sub-sessions (llm.ts abortActiveSubSessions).
|
|
10
|
+
*
|
|
11
|
+
* Phase-2 keeps its own scope so heartbeat loss can cancel the consolidator
|
|
12
|
+
* without marking the whole plugin as shutting down.
|
|
9
13
|
*/
|
|
10
14
|
let shuttingDown = false;
|
|
11
15
|
let phase2Abort = null;
|
|
16
|
+
/** Fresh each boot; aborted on dispose. Extract (and others) subscribe here. */
|
|
17
|
+
let shutdownAbort = new AbortController();
|
|
12
18
|
export function isPluginShuttingDown() {
|
|
13
19
|
return shuttingDown;
|
|
14
20
|
}
|
|
15
|
-
/**
|
|
21
|
+
/** Aborted when dispose begins; replaced on resetPluginLifecycle / re-boot. */
|
|
22
|
+
export function pluginShutdownSignal() {
|
|
23
|
+
return shutdownAbort.signal;
|
|
24
|
+
}
|
|
25
|
+
/** Begin shutdown: no new phase work, abort extract + consolidator waiters. */
|
|
16
26
|
export function beginPluginShutdown() {
|
|
17
27
|
shuttingDown = true;
|
|
28
|
+
if (!shutdownAbort.signal.aborted)
|
|
29
|
+
shutdownAbort.abort();
|
|
18
30
|
phase2Abort?.abort();
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
@@ -25,6 +37,7 @@ export function beginPluginShutdown() {
|
|
|
25
37
|
export function resetPluginLifecycle() {
|
|
26
38
|
phase2Abort?.abort();
|
|
27
39
|
phase2Abort = null;
|
|
40
|
+
shutdownAbort = new AbortController();
|
|
28
41
|
shuttingDown = false;
|
|
29
42
|
}
|
|
30
43
|
/**
|
package/dist/src/llm.d.ts
CHANGED
|
@@ -32,8 +32,20 @@ export interface ExtractOptions {
|
|
|
32
32
|
model?: string;
|
|
33
33
|
/** Override the default 1h extract timeout (tests / advanced). */
|
|
34
34
|
timeoutMs?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Cancel in-flight extract. Defaults to pluginShutdownSignal() so dispose
|
|
37
|
+
* unblocks phase1 without waiting on the host to reject session.prompt.
|
|
38
|
+
*/
|
|
39
|
+
signal?: AbortSignal;
|
|
35
40
|
}
|
|
36
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Returns null when the extractor reported a no-op (nothing worth remembering).
|
|
43
|
+
*
|
|
44
|
+
* Defaults to pluginShutdownSignal so dispose cancels the in-flight prompt
|
|
45
|
+
* race the same way as consolidateViaSubagent (phase-2 scope). dispose still
|
|
46
|
+
* session.aborts the sub-session for host-side cleanup; extractor has no FS
|
|
47
|
+
* write tools (D2) so a lingering server turn cannot dual-write the memory root.
|
|
48
|
+
*/
|
|
37
49
|
export declare function extractViaSubagent(sessionId: string, transcript: string, opts?: ExtractOptions): Promise<ExtractionResult | null>;
|
|
38
50
|
export declare function consolidateViaSubagent(memoryRoot: string, diffFileName: string, model?: string, signal?: AbortSignal): Promise<void>;
|
|
39
51
|
export declare function cleanupOldSubSessions(maxAgeMinutes?: number, timeoutMs?: number): Promise<void>;
|
package/dist/src/llm.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { memoryRoot } from "./paths.js";
|
|
4
4
|
import { hostSessionCreate, hostSessionDeletionConfirmed, hostSessionPrompt, hostStructuredOutput, } from "./host-client.js";
|
|
5
|
+
import { pluginShutdownSignal } from "./lifecycle.js";
|
|
5
6
|
let inputRef = null;
|
|
6
7
|
export function setPluginInput(input) {
|
|
7
8
|
inputRef = input;
|
|
@@ -171,8 +172,13 @@ async function runPrompt(sessionId, prompt, agent, opts = {}) {
|
|
|
171
172
|
const cancellation = new Promise((_, reject) => {
|
|
172
173
|
if (!opts.signal)
|
|
173
174
|
return;
|
|
175
|
+
// Abort may fire between the pre-check above and this setup (e.g. dispose
|
|
176
|
+
// during hostSessionPrompt construction). Already-aborted signals do not
|
|
177
|
+
// re-emit; observe current state after attaching the listener.
|
|
174
178
|
onAbort = () => reject(new SubagentCancelledError());
|
|
175
179
|
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
180
|
+
if (opts.signal.aborted)
|
|
181
|
+
onAbort();
|
|
176
182
|
});
|
|
177
183
|
const res = await Promise.race([
|
|
178
184
|
promptPromise,
|
|
@@ -243,7 +249,14 @@ const EXTRACTION_SCHEMA = {
|
|
|
243
249
|
},
|
|
244
250
|
required: ["raw_memory", "rollout_summary", "rollout_slug"],
|
|
245
251
|
};
|
|
246
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Returns null when the extractor reported a no-op (nothing worth remembering).
|
|
254
|
+
*
|
|
255
|
+
* Defaults to pluginShutdownSignal so dispose cancels the in-flight prompt
|
|
256
|
+
* race the same way as consolidateViaSubagent (phase-2 scope). dispose still
|
|
257
|
+
* session.aborts the sub-session for host-side cleanup; extractor has no FS
|
|
258
|
+
* write tools (D2) so a lingering server turn cannot dual-write the memory root.
|
|
259
|
+
*/
|
|
247
260
|
export async function extractViaSubagent(sessionId, transcript, opts = {}) {
|
|
248
261
|
const agent = "memorize-extract";
|
|
249
262
|
const subId = await createSession(agent, `codex-memory-extract-${sessionId}`);
|
|
@@ -258,6 +271,7 @@ export async function extractViaSubagent(sessionId, transcript, opts = {}) {
|
|
|
258
271
|
timeoutMs: opts.timeoutMs ?? 3600_000,
|
|
259
272
|
system: readTemplate("stage_one_system.md"),
|
|
260
273
|
model,
|
|
274
|
+
signal: opts.signal ?? pluginShutdownSignal(),
|
|
261
275
|
// opencode enforces json_schema output via a forced StructuredOutput tool
|
|
262
276
|
// call (toolChoice: required) — which is why memorize-extract must allow
|
|
263
277
|
// that one otherwise-denied tool.
|
package/dist/src/phase1.js
CHANGED
|
@@ -2,7 +2,7 @@ import { STAGE1_CONCURRENCY } from "./store.js";
|
|
|
2
2
|
import { loadTranscript, selectEligibleSessions } from "./capture.js";
|
|
3
3
|
import { redact, isMemoryExcludedFragment } from "./redact.js";
|
|
4
4
|
import { stripCitations } from "./citation.js";
|
|
5
|
-
import { extractViaSubagent } from "./llm.js";
|
|
5
|
+
import { extractViaSubagent, SubagentCancelledError } from "./llm.js";
|
|
6
6
|
import { checkRateLimit, markRateLimitUsed } from "./ratelimit.js";
|
|
7
7
|
import { isPluginShuttingDown } from "./lifecycle.js";
|
|
8
8
|
import { recordDiagnostic } from "./diagnostics.js";
|
|
@@ -95,9 +95,9 @@ export async function runPhase1(store, opts = DEFAULT_PHASE1_OPTIONS, rateLimitC
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
catch (err) {
|
|
98
|
-
// Aborted mid-extract on dispose
|
|
99
|
-
// burn a retry or impose the 1h failure backoff.
|
|
100
|
-
if (isPluginShuttingDown()) {
|
|
98
|
+
// Aborted mid-extract on dispose (shutdown signal / flag): release for
|
|
99
|
+
// immediate reclaim, do not burn a retry or impose the 1h failure backoff.
|
|
100
|
+
if (err instanceof SubagentCancelledError || isPluginShuttingDown()) {
|
|
101
101
|
store.releaseStage1OnShutdown(sid, claim.ownershipToken);
|
|
102
102
|
}
|
|
103
103
|
else {
|
package/dist/src/redact.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const REDACTIONS = [
|
|
2
|
+
// Bearer before key patterns (codex sanitizer order): a `Bearer sk-…` line
|
|
3
|
+
// redacts as one token instead of leaving a bare "Bearer " prefix.
|
|
4
|
+
// Word-boundary + space/tab only (not \s) avoids newline false positives;
|
|
5
|
+
// trailing =* covers base64 padding outside the 16-char body.
|
|
6
|
+
{ re: /\bBearer[ \t]+[A-Za-z0-9._~+/-]{16,}=*/gi, replacement: "Bearer [REDACTED]" },
|
|
2
7
|
{ re: /sk-ant-[A-Za-z0-9_\-]{20,}/g, replacement: "[REDACTED:anthropic-key]" },
|
|
3
8
|
{ re: /sk-[A-Za-z0-9]{20,}/g, replacement: "[REDACTED:openai-key]" },
|
|
4
9
|
{ re: /AKIA[0-9A-Z]{16}/g, replacement: "[REDACTED:aws-key]" },
|
|
5
10
|
{ re: /gh[pousr]_[A-Za-z0-9]{36,}/g, replacement: "[REDACTED:github-token]" },
|
|
6
11
|
{ re: /xox[baprs]-[A-Za-z0-9\-]{10,}/g, replacement: "[REDACTED:slack-token]" },
|
|
7
|
-
// Case-insensitive with a 16-char floor, matching codex's sanitizer.
|
|
8
|
-
{ re: /bearer\s+[A-Za-z0-9\-\._~+\/=]{16,}/gi, replacement: "Bearer [REDACTED]" },
|
|
9
12
|
{
|
|
10
13
|
re: /-----BEGIN [A-Z]+ PRIVATE KEY-----[\s\S]*?-----END [A-Z]+ PRIVATE KEY-----/g,
|
|
11
14
|
replacement: "[REDACTED:private-key]",
|
package/dist/tools/memory.js
CHANGED
|
@@ -69,27 +69,25 @@ export const memory_read = tool({
|
|
|
69
69
|
});
|
|
70
70
|
/** Skip hidden entries and symlinks, mirroring codex local/list.rs + local/search.rs walkers. */
|
|
71
71
|
function visibleEntries(dir) {
|
|
72
|
-
|
|
72
|
+
// Dirent file types (readdir withFileTypes) — codex read_sorted_dir_entries
|
|
73
|
+
// uses entry.file_type() so listing never follows symlinks.
|
|
74
|
+
let ents;
|
|
73
75
|
try {
|
|
74
|
-
|
|
76
|
+
ents = fs.readdirSync(dir, { withFileTypes: true });
|
|
75
77
|
}
|
|
76
78
|
catch {
|
|
77
79
|
return [];
|
|
78
80
|
}
|
|
79
81
|
const out = [];
|
|
80
|
-
for (const
|
|
81
|
-
if (name.startsWith("."))
|
|
82
|
+
for (const ent of ents) {
|
|
83
|
+
if (ent.name.startsWith("."))
|
|
82
84
|
continue;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
st = fs.lstatSync(path.join(dir, name));
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (st.isSymbolicLink())
|
|
85
|
+
if (ent.isSymbolicLink())
|
|
91
86
|
continue;
|
|
92
|
-
|
|
87
|
+
if (ent.isDirectory())
|
|
88
|
+
out.push({ name: ent.name, isDir: true });
|
|
89
|
+
else if (ent.isFile())
|
|
90
|
+
out.push({ name: ent.name, isDir: false });
|
|
93
91
|
}
|
|
94
92
|
return out;
|
|
95
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-codex-memory",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
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",
|