opencode-codex-memory 0.6.4 → 0.7.0
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 +26 -59
- package/dist/opencode.json +1 -1
- package/dist/src/citation.d.ts +9 -0
- package/dist/src/citation.js +68 -11
- package/dist/src/db.js +10 -0
- package/dist/src/host-client.d.ts +1 -0
- package/dist/src/host-client.js +1 -0
- package/dist/src/index.d.ts +12 -2
- package/dist/src/index.js +41 -6
- package/dist/src/llm.d.ts +6 -0
- package/dist/src/llm.js +21 -10
- package/dist/src/phase2.d.ts +2 -0
- package/dist/src/phase2.js +1 -1
- package/dist/src/rollout-input.d.ts +6 -0
- package/dist/src/rollout-input.js +111 -0
- package/dist/src/store.d.ts +17 -1
- package/dist/src/store.js +90 -4
- package/dist/src/v2/agents.d.ts +53 -0
- package/dist/src/v2/agents.js +204 -0
- package/dist/src/v2/citation-overlay.d.ts +7 -0
- package/dist/src/v2/citation-overlay.js +52 -0
- package/dist/src/v2/index.d.ts +7 -0
- package/dist/src/v2/index.js +10 -0
- package/dist/src/v2/injection.d.ts +14 -0
- package/dist/src/v2/injection.js +19 -0
- package/dist/src/v2/plugin.d.ts +7 -0
- package/dist/src/v2/plugin.js +482 -0
- package/dist/src/v2/service.d.ts +74 -0
- package/dist/src/v2/service.js +173 -0
- package/dist/src/v2/shim.d.ts +47 -0
- package/dist/src/v2/shim.js +581 -0
- package/dist/src/v2/status-rpc.d.ts +197 -0
- package/dist/src/v2/status-rpc.js +159 -0
- package/dist/src/v2/status.d.ts +3 -0
- package/dist/src/v2/status.js +83 -0
- package/dist/src/v2/tools.d.ts +33 -0
- package/dist/src/v2/tools.js +57 -0
- package/dist/src/v2/tui.d.ts +3 -0
- package/dist/src/v2/tui.js +750 -0
- package/opencode.json +1 -1
- package/package.json +38 -2
package/README.md
CHANGED
|
@@ -43,8 +43,8 @@ worked and what didn't — and puts that context back in front of the agent in
|
|
|
43
43
|
later conversations. You don't manage any of it; OpenCode just gets more useful
|
|
44
44
|
the more you use it.
|
|
45
45
|
|
|
46
|
-
If you want the mental model
|
|
47
|
-
[How
|
|
46
|
+
If you want the mental model — learning, remembering, forgetting — see
|
|
47
|
+
[How OpenCode Codex Memory works](./docs/how-ai-memory-works.md).
|
|
48
48
|
|
|
49
49
|
## Install
|
|
50
50
|
|
|
@@ -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.
|
|
55
|
+
"plugin": ["opencode-codex-memory@0.7.0"]
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -67,6 +67,20 @@ learning starts immediately.
|
|
|
67
67
|
Requires OpenCode 1.18 or newer. Models and other options: see
|
|
68
68
|
[Configuration](#configuration).
|
|
69
69
|
|
|
70
|
+
**OpenCode 2:** the same package works on opencode2 — install it with the
|
|
71
|
+
V2 plugin syntax:
|
|
72
|
+
|
|
73
|
+
```jsonc
|
|
74
|
+
{
|
|
75
|
+
"plugins": [{ "package": "opencode-codex-memory@0.7.0" }],
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Same pipeline, not the same host APIs — see [docs/opencode2.md](./docs/opencode2.md)
|
|
80
|
+
for the limits (registered service, retained citation markup, no `small_model`).
|
|
81
|
+
On opencode2 you also get a **Memory** section in the session sidebar plus a
|
|
82
|
+
`/memory-status` command, served live from the same state as `memory_inspect`.
|
|
83
|
+
|
|
70
84
|
### Installation hints
|
|
71
85
|
|
|
72
86
|
To bump pins, copy
|
|
@@ -99,54 +113,6 @@ echo 'I prefer TypeScript strict mode and 2-space indentation.' \
|
|
|
99
113
|
> ~/.local/share/opencode/memories/memory_summary.md
|
|
100
114
|
```
|
|
101
115
|
|
|
102
|
-
## How it works
|
|
103
|
-
|
|
104
|
-
You don't need to know any of this to use the plugin. The design is Codex's,
|
|
105
|
-
ported as-is, and it does what any memory system has to do: decide what's worth
|
|
106
|
-
keeping, write it down so it can be found again, surface the right piece at the
|
|
107
|
-
right moment, and forget what stopped being useful.
|
|
108
|
-
|
|
109
|
-
Think of it as three jobs: two background writers and one reader. **Nothing here
|
|
110
|
-
runs while you're waiting for a reply** — an assistant that stops to take notes
|
|
111
|
-
mid-answer would be slower and more expensive, so the learning happens after the
|
|
112
|
-
fact, on transcripts of conversations that are already over.
|
|
113
|
-
|
|
114
|
-
**Phase 1 — read one finished session, write notes about it.** Once a
|
|
115
|
-
conversation has been idle long enough that it's clearly done (default 6 h), the
|
|
116
|
-
plugin fetches that transcript, strips secrets out of it, and hands it to a
|
|
117
|
-
cheap model with one question: *what from this is worth keeping?* The answer
|
|
118
|
-
comes back as structured data — a detailed note plus a short recap of the
|
|
119
|
-
session — and lands in a local SQLite database. One session in, one record out.
|
|
120
|
-
Sessions are independent, so this part is easy to parallelize and to retry when
|
|
121
|
-
it fails.
|
|
122
|
-
|
|
123
|
-
**Phase 2 — merge all those notes into one memory.** Every few hours at most
|
|
124
|
-
(and only one run at a time across all your OpenCode windows), a second pass
|
|
125
|
-
takes the most relevant per-session notes and rewrites the actual memory files:
|
|
126
|
-
`MEMORY.md` as the full index, `memory_summary.md` as the short version, and
|
|
127
|
-
`skills/` for procedures worth repeating. This is where the interesting work
|
|
128
|
-
happens — ten similar observations collapse into one rule, contradictions get
|
|
129
|
-
resolved, and notes nothing ever used age out. Forgetting is a feature: memory
|
|
130
|
-
that only grows is memory that stops being useful.
|
|
131
|
-
|
|
132
|
-
The split exists because the two halves have opposite needs. Phase 1 is
|
|
133
|
-
per-session and can run many at once; phase 2 touches the single shared memory,
|
|
134
|
-
so it has to be serialized. Keeping them apart means one slow or failing session
|
|
135
|
-
extraction can't corrupt or block the shared store.
|
|
136
|
-
|
|
137
|
-
**The read path — actually remembering.** Every turn, the short summary is
|
|
138
|
-
appended to the system prompt (capped at ~2500 tokens, so the cost is small and
|
|
139
|
-
predictable). That's the always-on layer. When a task looks related to past
|
|
140
|
-
work, the agent goes further and searches the full memory itself with the
|
|
141
|
-
`memory_*` tools — the equivalent of "I've seen this before, let me look it up"
|
|
142
|
-
rather than carrying everything around all the time.
|
|
143
|
-
|
|
144
|
-
**The feedback loop.** When the agent uses a memory, it cites it. The citation
|
|
145
|
-
is recorded and then stripped before it reaches your screen, and those usage
|
|
146
|
-
counts feed back into phase 2's ranking. Memories that keep proving useful get
|
|
147
|
-
kept and sharpened; memories nothing has touched in a month drop out. The system
|
|
148
|
-
finds out which of its own notes were worth writing.
|
|
149
|
-
|
|
150
116
|
## Where your data lives
|
|
151
117
|
|
|
152
118
|
```
|
|
@@ -232,7 +198,7 @@ To set options, turn the plugin entry into a `[name, options]` pair:
|
|
|
232
198
|
```json
|
|
233
199
|
{
|
|
234
200
|
"plugin": [
|
|
235
|
-
["opencode-codex-memory@0.
|
|
201
|
+
["opencode-codex-memory@0.7.0", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
|
|
236
202
|
]
|
|
237
203
|
}
|
|
238
204
|
```
|
|
@@ -252,9 +218,9 @@ the default value appearing where you expected your setting.
|
|
|
252
218
|
|
|
253
219
|
Model selection mirrors Codex's cheap-extraction / capable-consolidation
|
|
254
220
|
split using OpenCode's own concepts: when `extract_model` is unset, the
|
|
255
|
-
`small_model` from your `opencode.json` is used (Codex uses `gpt-5.
|
|
221
|
+
`small_model` from your `opencode.json` is used (Codex uses `gpt-5.6-luna`);
|
|
256
222
|
when `consolidation_model` is unset, your main `model` is used (Codex uses
|
|
257
|
-
`gpt-5.
|
|
223
|
+
`gpt-5.6-terra`). If neither is configured, the learning sub-agents fall back to
|
|
258
224
|
their own agent-level `model` (if you defined one), else the provider default.
|
|
259
225
|
(OpenCode's *automatic* small-model pick is internal to OpenCode and not
|
|
260
226
|
exposed to plugins — set `small_model` explicitly to get the cheap extraction
|
|
@@ -299,7 +265,7 @@ Off by default; no changes to Codex's own config are required.
|
|
|
299
265
|
{
|
|
300
266
|
"plugin": [
|
|
301
267
|
[
|
|
302
|
-
"opencode-codex-memory@0.
|
|
268
|
+
"opencode-codex-memory@0.7.0",
|
|
303
269
|
{ "codex_interop": { "import": true, "export": true } }
|
|
304
270
|
]
|
|
305
271
|
]
|
|
@@ -356,7 +322,7 @@ from the project memories Claude already keeps on your machine. **One-way only**
|
|
|
356
322
|
```json
|
|
357
323
|
{
|
|
358
324
|
"plugin": [
|
|
359
|
-
["opencode-codex-memory@0.
|
|
325
|
+
["opencode-codex-memory@0.7.0", { "claude_import": { "enabled": true } }]
|
|
360
326
|
]
|
|
361
327
|
}
|
|
362
328
|
```
|
|
@@ -383,7 +349,7 @@ Claude names each project with an opaque id (a folder under
|
|
|
383
349
|
{
|
|
384
350
|
"plugin": [
|
|
385
351
|
[
|
|
386
|
-
"opencode-codex-memory@0.
|
|
352
|
+
"opencode-codex-memory@0.7.0",
|
|
387
353
|
{
|
|
388
354
|
"claude_import": {
|
|
389
355
|
"enabled": true,
|
|
@@ -493,8 +459,9 @@ package cache.
|
|
|
493
459
|
## Contributing
|
|
494
460
|
|
|
495
461
|
The port follows Codex closely: same two-phase pipeline, same on-disk artifacts,
|
|
496
|
-
same prompts (adapted only where OpenCode differs).
|
|
497
|
-
|
|
462
|
+
same prompts (adapted only where OpenCode differs). Conceptual design:
|
|
463
|
+
[How OpenCode Codex Memory works](./docs/how-ai-memory-works.md). Implementation
|
|
464
|
+
map and workarounds: [`ARCHITECTURE.md`](./ARCHITECTURE.md). Contributor
|
|
498
465
|
guidance lives in [`CONTRIBUTING.md`](./CONTRIBUTING.md) and
|
|
499
466
|
[`AGENTS.md`](./AGENTS.md) — in short: this repo exists to port Codex's memory
|
|
500
467
|
system to OpenCode, and PRs that break that parity will be rejected.
|
package/dist/opencode.json
CHANGED
package/dist/src/citation.d.ts
CHANGED
|
@@ -9,6 +9,15 @@ export interface ParsedCitation {
|
|
|
9
9
|
entries: MemoryCitationEntry[];
|
|
10
10
|
raw: string;
|
|
11
11
|
}
|
|
12
|
+
/** Fence language the TUI code-block renderer is registered for. */
|
|
13
|
+
export declare const CITATION_FENCE_LANG = "memory-citation";
|
|
14
|
+
/** Parse the fenced body: entry lines plus an optional `sessions:` line. */
|
|
15
|
+
export declare function parseCitationBody(body: string): {
|
|
16
|
+
entries: MemoryCitationEntry[];
|
|
17
|
+
sessionIds: string[];
|
|
18
|
+
};
|
|
12
19
|
export declare function parseCitations(text: string): ParsedCitation[];
|
|
20
|
+
/** Cheap pre-check before running the full parser (either format). */
|
|
21
|
+
export declare function hasCitationMarkup(text: string): boolean;
|
|
13
22
|
export declare function extractCitedSessionIds(text: string): string[];
|
|
14
23
|
export declare function stripCitations(text: string): string;
|
package/dist/src/citation.js
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
/** Fence language the TUI code-block renderer is registered for. */
|
|
2
|
+
export const CITATION_FENCE_LANG = "memory-citation";
|
|
3
|
+
// Legacy bare-XML block (still accepted for persisted history) or the current
|
|
4
|
+
// fenced form ```memory-citation ... ``` which markdown clients render as a
|
|
5
|
+
// code block and the V2 TUI renders natively.
|
|
6
|
+
const CITATION_BLOCK_RE = /<memory-citation>[\s\S]*?<\/memory-citation>|^[ \t]{0,3}(`{3,})memory-citation[ \t]*\r?\n[\s\S]*?\r?\n[ \t]*\1[ \t]*$/gim;
|
|
7
|
+
const FENCE_RE = /^[ \t]{0,3}(`{3,})memory-citation[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*\1[ \t]*$/i;
|
|
8
|
+
const SESSIONS_LINE_RE = /^sessions?\s*:\s*(.*)$/i;
|
|
2
9
|
function extractSection(block, name) {
|
|
3
10
|
const m = block.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "i"));
|
|
4
11
|
return m ? m[1] : null;
|
|
5
12
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/** Parse the fenced body: entry lines plus an optional `sessions:` line. */
|
|
14
|
+
export function parseCitationBody(body) {
|
|
15
|
+
const entries = [];
|
|
16
|
+
const sessionIds = [];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const line of body.split(/\r?\n/)) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed)
|
|
21
|
+
continue;
|
|
22
|
+
const sessions = trimmed.match(SESSIONS_LINE_RE);
|
|
23
|
+
if (sessions) {
|
|
24
|
+
for (const id of sessions[1].split(/[\s,]+/).map((s) => s.trim()).filter(Boolean)) {
|
|
25
|
+
if (!seen.has(id)) {
|
|
26
|
+
seen.add(id);
|
|
27
|
+
sessionIds.push(id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const entry = parseFenceEntry(trimmed);
|
|
33
|
+
if (entry)
|
|
34
|
+
entries.push(entry);
|
|
35
|
+
}
|
|
36
|
+
return { entries, sessionIds };
|
|
37
|
+
}
|
|
38
|
+
function parseLocation(location, note) {
|
|
15
39
|
const colon = location.lastIndexOf(":");
|
|
16
40
|
if (colon === -1)
|
|
17
41
|
return null;
|
|
@@ -26,19 +50,48 @@ function parseEntry(line) {
|
|
|
26
50
|
return null;
|
|
27
51
|
return { path, lineStart, lineEnd, note };
|
|
28
52
|
}
|
|
53
|
+
function parseXmlEntry(line) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed)
|
|
56
|
+
return null;
|
|
57
|
+
const noteSplit = trimmed.lastIndexOf("|note=[");
|
|
58
|
+
if (noteSplit === -1 || !trimmed.endsWith("]"))
|
|
59
|
+
return null;
|
|
60
|
+
const note = trimmed.slice(noteSplit + "|note=[".length, -1).trim();
|
|
61
|
+
return parseLocation(trimmed.slice(0, noteSplit), note);
|
|
62
|
+
}
|
|
63
|
+
function parseFenceEntry(line) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed)
|
|
66
|
+
return null;
|
|
67
|
+
const noteSplit = trimmed.lastIndexOf("|note=");
|
|
68
|
+
if (noteSplit === -1)
|
|
69
|
+
return null;
|
|
70
|
+
let note = trimmed.slice(noteSplit + "|note=".length).trim();
|
|
71
|
+
if (note.startsWith("[") && note.endsWith("]"))
|
|
72
|
+
note = note.slice(1, -1).trim();
|
|
73
|
+
return parseLocation(trimmed.slice(0, noteSplit), note);
|
|
74
|
+
}
|
|
29
75
|
export function parseCitations(text) {
|
|
30
76
|
const results = [];
|
|
31
77
|
const re = new RegExp(CITATION_BLOCK_RE);
|
|
32
78
|
let m;
|
|
33
79
|
while ((m = re.exec(text)) !== null) {
|
|
34
80
|
const raw = m[0];
|
|
81
|
+
const fenced = raw.match(FENCE_RE);
|
|
82
|
+
if (fenced) {
|
|
83
|
+
const parsed = parseCitationBody(fenced[2]);
|
|
84
|
+
if (parsed.entries.length > 0 || parsed.sessionIds.length > 0)
|
|
85
|
+
results.push({ ...parsed, raw });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
35
88
|
const entries = [];
|
|
36
89
|
const sessionIds = [];
|
|
37
90
|
const seen = new Set();
|
|
38
91
|
const entriesBlock = extractSection(raw, "citation_entries");
|
|
39
92
|
if (entriesBlock) {
|
|
40
93
|
for (const line of entriesBlock.split(/\r?\n/)) {
|
|
41
|
-
const entry =
|
|
94
|
+
const entry = parseXmlEntry(line);
|
|
42
95
|
if (entry)
|
|
43
96
|
entries.push(entry);
|
|
44
97
|
}
|
|
@@ -68,6 +121,10 @@ export function parseCitations(text) {
|
|
|
68
121
|
}
|
|
69
122
|
return results;
|
|
70
123
|
}
|
|
124
|
+
/** Cheap pre-check before running the full parser (either format). */
|
|
125
|
+
export function hasCitationMarkup(text) {
|
|
126
|
+
return /<memory-citation>/i.test(text) || /^[ \t]{0,3}`{3,}memory-citation[ \t]*$/im.test(text);
|
|
127
|
+
}
|
|
71
128
|
export function extractCitedSessionIds(text) {
|
|
72
129
|
const seen = new Set();
|
|
73
130
|
for (const c of parseCitations(text)) {
|
package/dist/src/db.js
CHANGED
|
@@ -78,6 +78,16 @@ function runMigrations(db) {
|
|
|
78
78
|
db.run(stmt);
|
|
79
79
|
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(1, Date.now());
|
|
80
80
|
}
|
|
81
|
+
if (currentVersion < 2) {
|
|
82
|
+
db.run(`CREATE TABLE IF NOT EXISTS memory_citation_usage (
|
|
83
|
+
session_id TEXT NOT NULL,
|
|
84
|
+
assistant_message_id TEXT NOT NULL,
|
|
85
|
+
cited_session_id TEXT NOT NULL,
|
|
86
|
+
recorded_at INTEGER NOT NULL,
|
|
87
|
+
PRIMARY KEY (session_id, assistant_message_id, cited_session_id)
|
|
88
|
+
)`);
|
|
89
|
+
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(2, Date.now());
|
|
90
|
+
}
|
|
81
91
|
}).immediate();
|
|
82
92
|
}
|
|
83
93
|
export function closeDb() {
|
package/dist/src/host-client.js
CHANGED
|
@@ -96,6 +96,7 @@ export async function hostSessionPrompt(client, opts) {
|
|
|
96
96
|
...(opts.body.variant ? { variant: opts.body.variant } : {}),
|
|
97
97
|
parts: opts.body.parts,
|
|
98
98
|
},
|
|
99
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
99
100
|
});
|
|
100
101
|
}
|
|
101
102
|
/** Read AssistantMessage.structured when the host captured json_schema output. */
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import { MemoryStore } from "./store.js";
|
|
2
1
|
import type { PluginInput, PluginOptions } from "@opencode-ai/plugin";
|
|
3
2
|
/** Test seam: wait for all hook-launched work, including follow-up phase 2. */
|
|
4
3
|
export declare function waitForBackgroundTasks(): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Load the OpenCode 2 implementation only when a V2 host actually invokes
|
|
6
|
+
* setup(). OpenCode 1 imports this module to obtain server(); it must not need
|
|
7
|
+
* the V2 SDK or execute any V2 module initialization just to start.
|
|
8
|
+
*/
|
|
9
|
+
type PluginSetup = (() => void | Promise<void>) | void;
|
|
10
|
+
declare function setupV2(ctx: unknown): Promise<PluginSetup>;
|
|
5
11
|
export declare function takeNewCitations(partKey: string, ids: string[]): string[];
|
|
6
12
|
export declare function markTurnSeen(sessionId: string): boolean;
|
|
7
13
|
export declare function shouldHandleIdle(sessionId: string, now?: number): boolean;
|
|
8
|
-
|
|
14
|
+
interface SessionMemoryStore {
|
|
15
|
+
deleteSessionMemory(sessionId: string): boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function handleSessionDeleted(sessionId: string, store?: SessionMemoryStore, schedulePhase2?: () => void): void;
|
|
9
18
|
declare const _default: {
|
|
10
19
|
id: string;
|
|
20
|
+
setup: typeof setupV2;
|
|
11
21
|
server(input: PluginInput, opts?: PluginOptions): Promise<{
|
|
12
22
|
tool: {
|
|
13
23
|
memory_read: {
|
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ensureMemoryLayout, buildMemorySystemPrompt, invalidateCache } from "./source.js";
|
|
2
2
|
import { memoryRoot } from "./paths.js";
|
|
3
|
-
import { stripCitations, extractCitedSessionIds } from "./citation.js";
|
|
3
|
+
import { stripCitations, extractCitedSessionIds, hasCitationMarkup } from "./citation.js";
|
|
4
4
|
import { memory_read, memory_search, memory_list, memory_add_note } from "../tools/memory.js";
|
|
5
5
|
import { memory_reset, memory_inspect, memory_mode } from "../tools/control.js";
|
|
6
6
|
import { MemoryStore } from "./store.js";
|
|
@@ -39,6 +39,22 @@ const MCP_STATUS_TIMEOUT_MS = 1_000;
|
|
|
39
39
|
function getStore() {
|
|
40
40
|
return new MemoryStore();
|
|
41
41
|
}
|
|
42
|
+
function isV2PluginContext(ctx) {
|
|
43
|
+
if (!ctx || typeof ctx !== "object")
|
|
44
|
+
return false;
|
|
45
|
+
const session = ctx.session;
|
|
46
|
+
const directory = ctx.location?.directory;
|
|
47
|
+
return typeof session?.hook === "function" && typeof directory === "string";
|
|
48
|
+
}
|
|
49
|
+
async function setupV2(ctx) {
|
|
50
|
+
// OpenCode 1.x may still invoke a `setup` export if present. Running the
|
|
51
|
+
// V2 adapter there replaces the V1 client with the shim and breaks
|
|
52
|
+
// discovery/consolidation. Only a V2 Plugin.Context has session.hook.
|
|
53
|
+
if (!isV2PluginContext(ctx))
|
|
54
|
+
return;
|
|
55
|
+
const { setup } = await import("./v2/plugin.js");
|
|
56
|
+
return setup(ctx);
|
|
57
|
+
}
|
|
42
58
|
// Citation blocks are seen by both the text.complete hook (once, at
|
|
43
59
|
// completion) and message.part.updated (once per streaming delta), so the
|
|
44
60
|
// same block surfaces many times. Track which session ids were already
|
|
@@ -105,6 +121,9 @@ schedulePhase2 = () => { if (pluginOptions.generate_memories)
|
|
|
105
121
|
}
|
|
106
122
|
export default {
|
|
107
123
|
id: "opencode-codex-memory",
|
|
124
|
+
// opencode2 entry point (V2 reads id + setup(), ignoring server()).
|
|
125
|
+
// Added additively: V1 hosts keep calling server() exactly as before.
|
|
126
|
+
setup: setupV2,
|
|
108
127
|
async server(input, opts) {
|
|
109
128
|
// A reload after dispose must be able to run the pipeline again.
|
|
110
129
|
resetPluginLifecycle();
|
|
@@ -123,6 +142,14 @@ export default {
|
|
|
123
142
|
// Finish bounded reseeding before hooks can see a surviving memory
|
|
124
143
|
// sub-session after a plugin reload.
|
|
125
144
|
await cleanupOldSubSessions();
|
|
145
|
+
try {
|
|
146
|
+
if (getStore().releaseOrphanedPhase2Job()) {
|
|
147
|
+
console.warn("[opencode-codex-memory] released a consolidation lease orphaned by a dead process");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.warn("[opencode-codex-memory] orphaned phase2 sweep failed:", err);
|
|
152
|
+
}
|
|
126
153
|
return buildHooks();
|
|
127
154
|
},
|
|
128
155
|
};
|
|
@@ -326,6 +353,10 @@ async function classifyExternalContextTool(toolName) {
|
|
|
326
353
|
}
|
|
327
354
|
return false;
|
|
328
355
|
}
|
|
356
|
+
const TITLE_GENERATOR_MARKER = "You are a title generator. You output ONLY a thread title.";
|
|
357
|
+
function isTitleGenerationPrompt(system) {
|
|
358
|
+
return system.some((block) => block.includes(TITLE_GENERATOR_MARKER));
|
|
359
|
+
}
|
|
329
360
|
/**
|
|
330
361
|
* Registers the memorize / memorize-extract sub-agents through the config
|
|
331
362
|
* hook so installing the plugin requires no manual agent setup. Definitions
|
|
@@ -383,9 +414,13 @@ function buildHooks() {
|
|
|
383
414
|
if (!pluginOptions.use_memories)
|
|
384
415
|
return;
|
|
385
416
|
// OpenCode also invokes this hook while generating agent definitions,
|
|
386
|
-
// without a session
|
|
417
|
+
// without a session, and while naming a session (hidden `title` agent,
|
|
418
|
+
// same sessionID; hook has no agent field). Memory belongs only in
|
|
419
|
+
// real conversation prompts.
|
|
387
420
|
if (!input.sessionID || isMemorySubSession(input.sessionID))
|
|
388
421
|
return;
|
|
422
|
+
if (isTitleGenerationPrompt(output.system))
|
|
423
|
+
return;
|
|
389
424
|
ensureMemoryLayout();
|
|
390
425
|
const memoryPrompt = buildMemorySystemPrompt(pluginOptions.dedicated_tools);
|
|
391
426
|
if (memoryPrompt) {
|
|
@@ -409,7 +444,7 @@ function buildHooks() {
|
|
|
409
444
|
try {
|
|
410
445
|
if (isMemorySubSession(input.sessionID))
|
|
411
446
|
return;
|
|
412
|
-
if (!output.text
|
|
447
|
+
if (!hasCitationMarkup(output.text))
|
|
413
448
|
return;
|
|
414
449
|
try {
|
|
415
450
|
const ids = extractCitedSessionIds(output.text);
|
|
@@ -437,10 +472,10 @@ function buildHooks() {
|
|
|
437
472
|
if (msg.info?.role !== "assistant")
|
|
438
473
|
continue;
|
|
439
474
|
for (const part of msg.parts) {
|
|
440
|
-
if (part.type === "text" && typeof part.text === "string" && part.text
|
|
475
|
+
if (part.type === "text" && typeof part.text === "string" && hasCitationMarkup(part.text)) {
|
|
441
476
|
const before = part.text;
|
|
442
477
|
part.text = stripCitations(part.text);
|
|
443
|
-
if (part.text
|
|
478
|
+
if (hasCitationMarkup(part.text)) {
|
|
444
479
|
console.warn("[opencode-codex-memory] citation marker still present after stripCitations — hook contract may have changed");
|
|
445
480
|
}
|
|
446
481
|
}
|
|
@@ -519,7 +554,7 @@ function buildHooks() {
|
|
|
519
554
|
return;
|
|
520
555
|
if (part.sessionID && isMemorySubSession(part.sessionID))
|
|
521
556
|
return;
|
|
522
|
-
if (!part.text
|
|
557
|
+
if (!hasCitationMarkup(part.text))
|
|
523
558
|
return;
|
|
524
559
|
let ids = [];
|
|
525
560
|
try {
|
package/dist/src/llm.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export interface ExtractionResult {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function setPluginInput(input: PluginInput): void;
|
|
8
8
|
export declare function getPluginInput(): PluginInput | null;
|
|
9
|
+
/**
|
|
10
|
+
* V2 agent definitions are location-scoped. The V2 setup supplies the active
|
|
11
|
+
* location so helper sessions resolve the same agent; V1 leaves this unset
|
|
12
|
+
* and continues using the dedicated memory workspace as its session root.
|
|
13
|
+
*/
|
|
14
|
+
export declare function setSubSessionDirectory(directory?: string): void;
|
|
9
15
|
/** Test seam. */
|
|
10
16
|
export declare function setSubSessionCreateTimeoutForTest(ms?: number): void;
|
|
11
17
|
/** Test seam. */
|
package/dist/src/llm.js
CHANGED
|
@@ -8,6 +8,7 @@ import { SCAN_LIMIT } from "./store.js";
|
|
|
8
8
|
import { isProviderCapacityError, ProviderCapacityError } from "./ratelimit.js";
|
|
9
9
|
let inputRef = null;
|
|
10
10
|
let inputGeneration = 0;
|
|
11
|
+
let subSessionDirectoryOverride = null;
|
|
11
12
|
export function setPluginInput(input) {
|
|
12
13
|
inputRef = input;
|
|
13
14
|
inputGeneration++;
|
|
@@ -19,6 +20,14 @@ export function setPluginInput(input) {
|
|
|
19
20
|
export function getPluginInput() {
|
|
20
21
|
return inputRef;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* V2 agent definitions are location-scoped. The V2 setup supplies the active
|
|
25
|
+
* location so helper sessions resolve the same agent; V1 leaves this unset
|
|
26
|
+
* and continues using the dedicated memory workspace as its session root.
|
|
27
|
+
*/
|
|
28
|
+
export function setSubSessionDirectory(directory) {
|
|
29
|
+
subSessionDirectoryOverride = directory ?? null;
|
|
30
|
+
}
|
|
22
31
|
// Sessions this plugin spawned for extraction/consolidation. The main
|
|
23
32
|
// hooks skip these so the plugin never injects memory into (or memorizes) its
|
|
24
33
|
// own sub-agents.
|
|
@@ -59,16 +68,17 @@ export function isMemorySubSession(sessionId) {
|
|
|
59
68
|
/**
|
|
60
69
|
* Host directory for memory sub-sessions. Must exist: OpenCode resolves it in
|
|
61
70
|
* SystemPrompt.environment and fails the turn with UnknownError/ENOENT when
|
|
62
|
-
* missing.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
71
|
+
* missing. V1 uses the memory workspace; V2 supplies the active project
|
|
72
|
+
* location so its location-scoped `memorize` agent is available, with the
|
|
73
|
+
* memory workspace as the safe fallback.
|
|
65
74
|
*/
|
|
66
75
|
function resolveSubSessionDirectory() {
|
|
67
|
-
const
|
|
76
|
+
const configured = subSessionDirectoryOverride;
|
|
77
|
+
const root = configured && fs.existsSync(configured) ? configured : memoryRoot();
|
|
68
78
|
fs.mkdirSync(root, { recursive: true });
|
|
69
79
|
return root;
|
|
70
80
|
}
|
|
71
|
-
async function createSession(
|
|
81
|
+
async function createSession(title) {
|
|
72
82
|
const input = getPluginInput();
|
|
73
83
|
if (!input)
|
|
74
84
|
throw new Error("plugin input not initialized");
|
|
@@ -81,7 +91,7 @@ async function createSession(agent, title) {
|
|
|
81
91
|
const res = await withHostTimeout(hostSessionCreate(input.client, {
|
|
82
92
|
directory,
|
|
83
93
|
body: {
|
|
84
|
-
title
|
|
94
|
+
title,
|
|
85
95
|
metadata: { [SUBSESSION_METADATA_KEY]: true },
|
|
86
96
|
},
|
|
87
97
|
signal: controller.signal,
|
|
@@ -98,8 +108,8 @@ async function createSession(agent, title) {
|
|
|
98
108
|
/**
|
|
99
109
|
* opencode's config carries the same split codex expresses with provider
|
|
100
110
|
* model preferences: `small_model` for cheap background work (codex:
|
|
101
|
-
* memory_extraction_preferred_model = gpt-5.
|
|
102
|
-
* work (codex: memory_consolidation_preferred_model = gpt-5.
|
|
111
|
+
* memory_extraction_preferred_model = gpt-5.6-luna) and `model` for capable
|
|
112
|
+
* work (codex: memory_consolidation_preferred_model = gpt-5.6-terra). Cached per
|
|
103
113
|
* plugin instance — opencode reloads plugins on config change.
|
|
104
114
|
*/
|
|
105
115
|
let configModels = null;
|
|
@@ -265,6 +275,7 @@ async function runPrompt(sessionId, prompt, agent, opts = {}) {
|
|
|
265
275
|
const model = opts.model ? parseModelRef(opts.model) : null;
|
|
266
276
|
const promptPromise = hostSessionPrompt(input.client, {
|
|
267
277
|
sessionId,
|
|
278
|
+
signal: opts.signal,
|
|
268
279
|
body: {
|
|
269
280
|
agent,
|
|
270
281
|
...(opts.system ? { system: opts.system } : {}),
|
|
@@ -377,7 +388,7 @@ const EXTRACTION_SCHEMA = {
|
|
|
377
388
|
*/
|
|
378
389
|
export async function extractViaSubagent(sessionId, transcript, opts = {}) {
|
|
379
390
|
const agent = "memorize-extract";
|
|
380
|
-
const subId = await createSession(
|
|
391
|
+
const subId = await createSession(`codex-memory-extract-${sessionId}`);
|
|
381
392
|
try {
|
|
382
393
|
const prompt = buildExtractionInput(sessionId, opts.cwd ?? "unknown", transcript);
|
|
383
394
|
// extract_model option > opencode small_model > session default.
|
|
@@ -421,7 +432,7 @@ export async function extractViaSubagent(sessionId, transcript, opts = {}) {
|
|
|
421
432
|
const CONSOLIDATION_TIMEOUT_MS = 3600_000;
|
|
422
433
|
export async function consolidateViaSubagent(memoryRoot, diffFileName, model, signal) {
|
|
423
434
|
const agent = "memorize";
|
|
424
|
-
const subId = await createSession(
|
|
435
|
+
const subId = await createSession("codex-memory-consolidate");
|
|
425
436
|
let promptError;
|
|
426
437
|
let promptFailed = false;
|
|
427
438
|
try {
|
package/dist/src/phase2.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface Phase2Options {
|
|
|
10
10
|
claudeImport?: ClaudeImportOptions;
|
|
11
11
|
/** Override the 90s heartbeat interval (tests / advanced). */
|
|
12
12
|
heartbeatIntervalMs?: number;
|
|
13
|
+
/** User-requested run: skip the 6h success cooldown (lease/retry gates still apply). */
|
|
14
|
+
bypassCooldown?: boolean;
|
|
13
15
|
}
|
|
14
16
|
export declare const DEFAULT_PHASE2_OPTIONS: Phase2Options;
|
|
15
17
|
/**
|
package/dist/src/phase2.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
|
111
111
|
const rl = await checkRateLimit("phase2", consolidationModel);
|
|
112
112
|
if (!rl.ok)
|
|
113
113
|
return { status: "skipped_rate_limit" };
|
|
114
|
-
const claim = store.claimGlobalPhase2Job();
|
|
114
|
+
const claim = store.claimGlobalPhase2Job({ bypassCooldown: opts.bypassCooldown });
|
|
115
115
|
if (claim.type !== "claimed")
|
|
116
116
|
return { status: claim.type };
|
|
117
117
|
// Abort scope covers prep + consolidator so dispose during baseline/diff
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TranscriptMessage } from "./capture.js";
|
|
2
|
+
/**
|
|
3
|
+
* Human-first extract input. OpenCode parts, not Codex RolloutItem:
|
|
4
|
+
* no commentary phase, no request_user_input pairing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function serializeTieredInput(items: TranscriptMessage[], charLimit?: number): string;
|