opencode-codex-memory 0.1.6 → 0.1.8
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/dist/opencode.json +2 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +7 -3
- package/dist/src/llm.d.ts +1 -0
- package/dist/src/llm.js +39 -2
- package/dist/src/phase2.js +23 -3
- package/dist/src/store.d.ts +10 -2
- package/dist/src/store.js +47 -6
- package/dist/src/templates/consolidation.md +629 -197
- package/dist/src/templates/read_path.md +68 -37
- package/dist/src/templates/stage_one_system.md +311 -75
- package/dist/src/workspace.d.ts +12 -0
- package/dist/src/workspace.js +30 -0
- package/opencode.json +2 -0
- package/package.json +1 -1
package/dist/opencode.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"mode": "subagent",
|
|
6
6
|
"prompt": "You are a memory consolidation agent. Read the workspace diff file and update MEMORY.md, memory_summary.md, and skills/ to reflect the latest memories. Keep memory_summary.md under 10000 chars (2500 tokens). Prune stale entries. Do not access the network.",
|
|
7
7
|
"permission": {
|
|
8
|
+
"*": "deny",
|
|
8
9
|
"bash": "deny",
|
|
9
10
|
"webfetch": "deny",
|
|
10
11
|
"websearch": "deny",
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"mode": "subagent",
|
|
22
23
|
"prompt": "You are a memory extraction agent. Read the session transcript and extract raw_memory, rollout_summary, and rollout_slug as JSON. Exclude AGENTS.md/instruction content. Redact secrets.",
|
|
23
24
|
"permission": {
|
|
25
|
+
"*": "deny",
|
|
24
26
|
"bash": "deny",
|
|
25
27
|
"webfetch": "deny",
|
|
26
28
|
"websearch": "deny",
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { MemoryStore } from "./store.js";
|
|
1
2
|
import type { PluginInput, PluginOptions } from "@opencode-ai/plugin";
|
|
2
3
|
export declare function takeNewCitations(partKey: string, ids: string[]): string[];
|
|
4
|
+
export declare function handleSessionDeleted(sessionId: string, store?: Pick<MemoryStore, "deleteSessionMemory">, schedulePhase2?: () => void): void;
|
|
3
5
|
declare const _default: {
|
|
4
6
|
id: string;
|
|
5
7
|
server(input: PluginInput, opts?: PluginOptions): Promise<{
|
package/dist/src/index.js
CHANGED
|
@@ -52,6 +52,10 @@ export function takeNewCitations(partKey, ids) {
|
|
|
52
52
|
seen.add(id);
|
|
53
53
|
return fresh;
|
|
54
54
|
}
|
|
55
|
+
export function handleSessionDeleted(sessionId, store = getStore(), schedulePhase2 = () => { void triggerPhase2(); }) {
|
|
56
|
+
if (store.deleteSessionMemory(sessionId))
|
|
57
|
+
schedulePhase2();
|
|
58
|
+
}
|
|
55
59
|
export default {
|
|
56
60
|
id: "opencode-codex-memory",
|
|
57
61
|
async server(input, opts) {
|
|
@@ -267,13 +271,13 @@ function buildHooks() {
|
|
|
267
271
|
}
|
|
268
272
|
if (ev.type === "session.deleted") {
|
|
269
273
|
// Mirrors codex delete_thread_memory: drop the extracted memory and
|
|
270
|
-
// its job when the session is deleted
|
|
271
|
-
//
|
|
274
|
+
// its job when the session is deleted. If phase 2 had consumed it,
|
|
275
|
+
// enqueue and attempt consolidation so the diff drives forgetting.
|
|
272
276
|
const props = ev.properties;
|
|
273
277
|
const sid = props?.info?.id;
|
|
274
278
|
if (sid) {
|
|
275
279
|
try {
|
|
276
|
-
|
|
280
|
+
handleSessionDeleted(sid);
|
|
277
281
|
}
|
|
278
282
|
catch (e) {
|
|
279
283
|
console.error("[opencode-codex-memory] deleteSessionMemory failed:", e);
|
package/dist/src/llm.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ export declare function extractViaSubagent(sessionId: string, transcript: string
|
|
|
15
15
|
export declare function consolidateViaSubagent(memoryRoot: string, diffFileName: string, model?: string): Promise<void>;
|
|
16
16
|
export declare function cleanupOldSubSessions(maxAgeMinutes?: number): Promise<void>;
|
|
17
17
|
export declare function fillTemplate(tmpl: string, vars: Record<string, string>): string;
|
|
18
|
+
export declare function buildConsolidationPrompt(memoryRoot: string, diffFileName: string): string;
|
|
18
19
|
/** Parses the stage-1 JSON output. Returns null for the all-empty no-op response. */
|
|
19
20
|
export declare function parseExtraction(raw: string): ExtractionResult | null;
|
package/dist/src/llm.js
CHANGED
|
@@ -209,10 +209,46 @@ function buildExtractionInput(sessionId, cwd, transcript) {
|
|
|
209
209
|
transcript,
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
// codex lib.rs prompt_blocks: rendered into consolidation.md's
|
|
213
|
+
// {{ memory_extensions_* }} placeholders when <memory_root>/extensions exists
|
|
214
|
+
// (prompts.rs build_consolidation_prompt), empty strings otherwise.
|
|
215
|
+
const EXTENSIONS_FOLDER_STRUCTURE = `
|
|
216
|
+
Memory extensions (under {{ memory_extensions_root }}/):
|
|
217
|
+
|
|
218
|
+
- <extension_name>/instructions.md
|
|
219
|
+
- Source-specific guidance for interpreting additional memory signals. If an
|
|
220
|
+
extension folder exists, you must read its instructions.md to determine how to use this memory
|
|
221
|
+
source.
|
|
222
|
+
|
|
223
|
+
If the user has any memory extensions, you MUST read the instructions for each extension to
|
|
224
|
+
determine how to use the memory source. If the workspace diff shows deleted extension resource files,
|
|
225
|
+
remove stale memories derived only from those resources. If it has no extension folders, continue
|
|
226
|
+
with the standard memory inputs only.
|
|
227
|
+
`;
|
|
228
|
+
const EXTENSIONS_PRIMARY_INPUTS = `
|
|
229
|
+
Optional source-specific inputs:
|
|
230
|
+
Under \`{{ memory_extensions_root }}/\`:
|
|
231
|
+
|
|
232
|
+
- \`<extension_name>/instructions.md\`
|
|
233
|
+
- If extension folders exist, read each instructions.md first and follow it when interpreting
|
|
234
|
+
that extension's memory source.
|
|
235
|
+
|
|
236
|
+
If the workspace diff shows deleted memory extension resources, use that extension-specific deletion
|
|
237
|
+
signal to remove stale memories derived only from those resources.
|
|
238
|
+
`;
|
|
239
|
+
export function buildConsolidationPrompt(memoryRoot, diffFileName) {
|
|
240
|
+
const extensionsRoot = path.join(memoryRoot, "extensions");
|
|
241
|
+
let extensionsExist = false;
|
|
242
|
+
try {
|
|
243
|
+
extensionsExist = fs.statSync(extensionsRoot).isDirectory();
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
const blockVars = { memory_extensions_root: extensionsRoot };
|
|
213
247
|
return fillTemplate(readTemplate("consolidation.md"), {
|
|
214
248
|
memory_root: memoryRoot,
|
|
215
249
|
phase2_workspace_diff_file: diffFileName,
|
|
250
|
+
memory_extensions_folder_structure: extensionsExist ? fillTemplate(EXTENSIONS_FOLDER_STRUCTURE, blockVars) : "",
|
|
251
|
+
memory_extensions_primary_inputs: extensionsExist ? fillTemplate(EXTENSIONS_PRIMARY_INPUTS, blockVars) : "",
|
|
216
252
|
});
|
|
217
253
|
}
|
|
218
254
|
function readTemplate(name) {
|
|
@@ -231,7 +267,8 @@ export function parseExtraction(raw) {
|
|
|
231
267
|
if (typeof obj.raw_memory !== "string" || typeof obj.rollout_summary !== "string") {
|
|
232
268
|
throw new Error("extraction response missing required fields");
|
|
233
269
|
}
|
|
234
|
-
|
|
270
|
+
// codex phase1: either field empty → SucceededNoOutput (not a partial upsert).
|
|
271
|
+
if (!obj.raw_memory.trim() || !obj.rollout_summary.trim()) {
|
|
235
272
|
return null;
|
|
236
273
|
}
|
|
237
274
|
// Guard against the model echoing the format skeleton from the system prompt.
|
package/dist/src/phase2.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ensureLayout, rebuildRawMemories, writeRolloutSummaries, pruneExtensionResources, writeWorkspaceDiff } from "./workspace.js";
|
|
1
|
+
import { ensureLayout, rebuildRawMemories, writeRolloutSummaries, pruneExtensionResources, writeWorkspaceDiff, validateConsolidationArtifacts, } from "./workspace.js";
|
|
2
2
|
import { ensureBaseline, captureWorkspaceDiff, resetBaseline, DIFF_ARTIFACT } from "./git-baseline.js";
|
|
3
3
|
import { consolidateViaSubagent } from "./llm.js";
|
|
4
4
|
import { invalidateCache } from "./source.js";
|
|
@@ -37,9 +37,16 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
|
37
37
|
writeRolloutSummaries(outputs);
|
|
38
38
|
pruneExtensionResources(opts.extensionRetentionDays);
|
|
39
39
|
const diff = await captureWorkspaceDiff();
|
|
40
|
+
// codex: early succeed only when there are no changes AND artifacts are
|
|
41
|
+
// already valid. Invalid/empty summary (e.g. ensureLayout's empty file)
|
|
42
|
+
// falls through so the consolidator can INIT/repair.
|
|
40
43
|
if (diff.changes.length === 0) {
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
const valid = validateConsolidationArtifacts();
|
|
45
|
+
if (valid.ok) {
|
|
46
|
+
store.markPhase2Succeeded(claim.ownershipToken, outputs);
|
|
47
|
+
return { status: "no_workspace_changes" };
|
|
48
|
+
}
|
|
49
|
+
console.warn("[opencode-codex-memory] no workspace changes but artifacts invalid; running consolidator:", valid.reason);
|
|
43
50
|
}
|
|
44
51
|
writeWorkspaceDiff(diff);
|
|
45
52
|
let heartbeatLost = false;
|
|
@@ -48,8 +55,10 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
|
48
55
|
heartbeatLost = true;
|
|
49
56
|
}
|
|
50
57
|
}, 90_000);
|
|
58
|
+
let agentCompleted = false;
|
|
51
59
|
try {
|
|
52
60
|
await consolidateViaSubagent(memoryRoot(), DIFF_ARTIFACT, opts.consolidationModel);
|
|
61
|
+
agentCompleted = true;
|
|
53
62
|
}
|
|
54
63
|
finally {
|
|
55
64
|
clearInterval(heartbeat);
|
|
@@ -64,6 +73,17 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
|
64
73
|
store.markPhase2Failed(claim.ownershipToken, "ownership lost");
|
|
65
74
|
return { status: "heartbeat_lost" };
|
|
66
75
|
}
|
|
76
|
+
if (!agentCompleted) {
|
|
77
|
+
store.markPhase2Failed(claim.ownershipToken, "failed_agent");
|
|
78
|
+
return { status: "failed_agent" };
|
|
79
|
+
}
|
|
80
|
+
// codex failed_invalid_artifacts: do not reset baseline on bad output so
|
|
81
|
+
// the next run still sees a diff / can re-INIT.
|
|
82
|
+
const artifacts = validateConsolidationArtifacts();
|
|
83
|
+
if (!artifacts.ok) {
|
|
84
|
+
store.markPhase2Failed(claim.ownershipToken, `failed_invalid_artifacts: ${artifacts.reason}`);
|
|
85
|
+
return { status: "failed_invalid_artifacts" };
|
|
86
|
+
}
|
|
67
87
|
if (!await resetBaseline()) {
|
|
68
88
|
store.markPhase2Failed(claim.ownershipToken, "baseline reset failed");
|
|
69
89
|
return { status: "baseline_reset_failed" };
|
package/dist/src/store.d.ts
CHANGED
|
@@ -56,6 +56,11 @@ export declare class MemoryStore {
|
|
|
56
56
|
/** Extraction succeeded but produced nothing worth keeping: finish the job and drop any stale output. */
|
|
57
57
|
markStage1SucceededNoOutput(sessionId: string, ownershipToken: string, sourceUpdatedAt: number): void;
|
|
58
58
|
markStage1Failed(sessionId: string, ownershipToken: string, error: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Enqueues global consolidation after stage-1 state changes. If phase 2 is
|
|
61
|
+
* already running, preserve its lease and advance only the input watermark.
|
|
62
|
+
*/
|
|
63
|
+
private enqueueGlobalConsolidation;
|
|
59
64
|
claimGlobalPhase2Job(): Phase2ClaimResult;
|
|
60
65
|
heartbeatPhase2Job(ownershipToken: string): boolean;
|
|
61
66
|
/**
|
|
@@ -74,8 +79,11 @@ export declare class MemoryStore {
|
|
|
74
79
|
* - ranked by usage, then recency
|
|
75
80
|
*/
|
|
76
81
|
getPhase2InputSelection(maxRaw: number, maxUnusedDays: number): Stage1Output[];
|
|
77
|
-
/**
|
|
78
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Mirrors codex delete_thread_memory: remove a deleted session's output and
|
|
84
|
+
* job, then enqueue forgetting if phase 2 had consumed that output.
|
|
85
|
+
*/
|
|
86
|
+
deleteSessionMemory(sessionId: string): boolean;
|
|
79
87
|
/**
|
|
80
88
|
* codex clear_memory_data deletes extracted memories and jobs but explicitly
|
|
81
89
|
* preserves per-session memory modes: a reset must not re-enable sessions
|
package/dist/src/store.js
CHANGED
|
@@ -146,8 +146,10 @@ export class MemoryStore {
|
|
|
146
146
|
.run(nowSec(), out.source_updated_at, sessionId, ownershipToken);
|
|
147
147
|
// Ownership lost (lease expired, job re-claimed): do not clobber the new
|
|
148
148
|
// owner's output. Mirrors codex mark_stage1_job_succeeded.
|
|
149
|
-
if (res.changes > 0)
|
|
149
|
+
if (res.changes > 0) {
|
|
150
150
|
this.upsertStage1Output(out);
|
|
151
|
+
this.enqueueGlobalConsolidation(out.source_updated_at);
|
|
152
|
+
}
|
|
151
153
|
}).immediate();
|
|
152
154
|
}
|
|
153
155
|
/** Extraction succeeded but produced nothing worth keeping: finish the job and drop any stale output. */
|
|
@@ -158,8 +160,11 @@ export class MemoryStore {
|
|
|
158
160
|
last_success_watermark=?, retry_at=NULL
|
|
159
161
|
WHERE kind='memory_stage1' AND job_key=? AND status='running' AND ownership_token=?`)
|
|
160
162
|
.run(nowSec(), sourceUpdatedAt, sessionId, ownershipToken);
|
|
161
|
-
if (res.changes
|
|
162
|
-
|
|
163
|
+
if (res.changes === 0)
|
|
164
|
+
return;
|
|
165
|
+
const deleted = this.db.prepare("DELETE FROM memory_stage1_outputs WHERE session_id = ?").run(sessionId);
|
|
166
|
+
if (deleted.changes > 0)
|
|
167
|
+
this.enqueueGlobalConsolidation(sourceUpdatedAt);
|
|
163
168
|
}).immediate();
|
|
164
169
|
}
|
|
165
170
|
markStage1Failed(sessionId, ownershipToken, error) {
|
|
@@ -174,6 +179,32 @@ export class MemoryStore {
|
|
|
174
179
|
WHERE kind='memory_stage1' AND job_key=? AND status='running' AND ownership_token=?`)
|
|
175
180
|
.run(error.slice(0, 4000), nowSec() + STAGE1_RETRY_DELAY_SECONDS, nowSec(), sessionId, ownershipToken);
|
|
176
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Enqueues global consolidation after stage-1 state changes. If phase 2 is
|
|
184
|
+
* already running, preserve its lease and advance only the input watermark.
|
|
185
|
+
*/
|
|
186
|
+
enqueueGlobalConsolidation(inputWatermark) {
|
|
187
|
+
this.db
|
|
188
|
+
.prepare(`INSERT INTO memory_jobs
|
|
189
|
+
(kind, job_key, status, retry_remaining, input_watermark, last_success_watermark)
|
|
190
|
+
VALUES ('memory_consolidate_global', 'global', 'pending', ?, ?, 0)
|
|
191
|
+
ON CONFLICT(kind, job_key) DO UPDATE SET
|
|
192
|
+
status = CASE
|
|
193
|
+
WHEN memory_jobs.status = 'running' THEN 'running'
|
|
194
|
+
ELSE 'pending'
|
|
195
|
+
END,
|
|
196
|
+
retry_at = CASE
|
|
197
|
+
WHEN memory_jobs.status = 'running' THEN memory_jobs.retry_at
|
|
198
|
+
ELSE NULL
|
|
199
|
+
END,
|
|
200
|
+
retry_remaining = MAX(memory_jobs.retry_remaining, excluded.retry_remaining),
|
|
201
|
+
input_watermark = CASE
|
|
202
|
+
WHEN excluded.input_watermark > COALESCE(memory_jobs.input_watermark, 0)
|
|
203
|
+
THEN excluded.input_watermark
|
|
204
|
+
ELSE COALESCE(memory_jobs.input_watermark, 0) + 1
|
|
205
|
+
END`)
|
|
206
|
+
.run(DEFAULT_RETRY_REMAINING, inputWatermark);
|
|
207
|
+
}
|
|
177
208
|
claimGlobalPhase2Job() {
|
|
178
209
|
const workerId = newId();
|
|
179
210
|
const ownershipToken = newId();
|
|
@@ -287,11 +318,21 @@ export class MemoryStore {
|
|
|
287
318
|
LIMIT ?`)
|
|
288
319
|
.all(cutoff, cutoff, maxRaw);
|
|
289
320
|
}
|
|
290
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Mirrors codex delete_thread_memory: remove a deleted session's output and
|
|
323
|
+
* job, then enqueue forgetting if phase 2 had consumed that output.
|
|
324
|
+
*/
|
|
291
325
|
deleteSessionMemory(sessionId) {
|
|
292
|
-
this.db.transaction(() => {
|
|
293
|
-
|
|
326
|
+
return this.db.transaction(() => {
|
|
327
|
+
const existing = this.db
|
|
328
|
+
.prepare("SELECT selected_for_phase2 FROM memory_stage1_outputs WHERE session_id = ?")
|
|
329
|
+
.get(sessionId);
|
|
330
|
+
const deleted = this.db.prepare("DELETE FROM memory_stage1_outputs WHERE session_id = ?").run(sessionId);
|
|
294
331
|
this.db.prepare("DELETE FROM memory_jobs WHERE kind='memory_stage1' AND job_key = ?").run(sessionId);
|
|
332
|
+
const shouldConsolidate = deleted.changes > 0 && existing !== null && existing.selected_for_phase2 !== 0;
|
|
333
|
+
if (shouldConsolidate)
|
|
334
|
+
this.enqueueGlobalConsolidation(now());
|
|
335
|
+
return shouldConsolidate;
|
|
295
336
|
}).immediate();
|
|
296
337
|
}
|
|
297
338
|
/**
|