opencode-codex-memory 0.1.7 → 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.
@@ -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",
@@ -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; the file disappears at the
271
- // next phase-2 rebuild and the diff drives forgetting.
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
- getStore().deleteSessionMemory(sid);
280
+ handleSessionDeleted(sid);
277
281
  }
278
282
  catch (e) {
279
283
  console.error("[opencode-codex-memory] deleteSessionMemory failed:", e);
@@ -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
- /** Mirrors codex delete_thread_memory: remove a deleted session's output + job. */
78
- deleteSessionMemory(sessionId: string): void;
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 > 0)
162
- this.db.prepare("DELETE FROM memory_stage1_outputs WHERE session_id = ?").run(sessionId);
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
- /** Mirrors codex delete_thread_memory: remove a deleted session's output + job. */
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
- this.db.prepare("DELETE FROM memory_stage1_outputs WHERE session_id = ?").run(sessionId);
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
  /**
package/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codex-memory",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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",