opencode-codex-memory 0.4.2 → 0.4.3

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 CHANGED
@@ -48,11 +48,11 @@ If you want the mental model before the details, jump to
48
48
 
49
49
  1. Add the plugin to your `~/.config/opencode/opencode.json`:
50
50
 
51
- ```json
52
- {
53
- "plugin": ["opencode-codex-memory@0.4.2"]
54
- }
55
- ```
51
+ ```json
52
+ {
53
+ "plugin": ["opencode-codex-memory@0.4.3"]
54
+ }
55
+ ```
56
56
 
57
57
  **Pin the version** (here and for any OpenCode plugin). OpenCode installs a
58
58
  plugin spec once into its package cache and never re-resolves it, so a bare
@@ -237,7 +237,7 @@ To set options, turn the plugin entry into a `[name, options]` pair:
237
237
  ```json
238
238
  {
239
239
  "plugin": [
240
- ["opencode-codex-memory@0.4.2", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
240
+ ["opencode-codex-memory@0.4.3", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
241
241
  ]
242
242
  }
243
243
  ```
@@ -296,7 +296,7 @@ directions:
296
296
  ```json
297
297
  {
298
298
  "plugin": [
299
- ["opencode-codex-memory@0.4.2", { "codex_interop": { "import": true, "export": true } }]
299
+ ["opencode-codex-memory@0.4.3", { "codex_interop": { "import": true, "export": true } }]
300
300
  ]
301
301
  }
302
302
  ```
package/dist/src/llm.d.ts CHANGED
@@ -15,6 +15,14 @@ export declare function isMemorySubSession(sessionId: string): boolean;
15
15
  export declare class SubagentTimeoutError extends Error {
16
16
  constructor(timeoutMs: number);
17
17
  }
18
+ /**
19
+ * Thrown when a sub-agent session could not be closed. Codex treats a failed
20
+ * consolidation-agent shutdown as "the agent may still be alive", so the caller
21
+ * must keep its job lease instead of completing the job (phase2.rs).
22
+ */
23
+ export declare class SubagentShutdownError extends Error {
24
+ constructor(sessionId: string);
25
+ }
18
26
  export interface ExtractOptions {
19
27
  cwd?: string;
20
28
  model?: string;
package/dist/src/llm.js CHANGED
@@ -81,6 +81,17 @@ export class SubagentTimeoutError extends Error {
81
81
  this.name = "SubagentTimeoutError";
82
82
  }
83
83
  }
84
+ /**
85
+ * Thrown when a sub-agent session could not be closed. Codex treats a failed
86
+ * consolidation-agent shutdown as "the agent may still be alive", so the caller
87
+ * must keep its job lease instead of completing the job (phase2.rs).
88
+ */
89
+ export class SubagentShutdownError extends Error {
90
+ constructor(sessionId) {
91
+ super(`failed to close memory sub-session ${sessionId}`);
92
+ this.name = "SubagentShutdownError";
93
+ }
94
+ }
84
95
  async function abortSession(sessionId) {
85
96
  const input = getPluginInput();
86
97
  const session = input?.client?.session;
@@ -224,6 +235,9 @@ export async function extractViaSubagent(sessionId, transcript, opts = {}) {
224
235
  return parseExtraction(extractAssistantText(data));
225
236
  }
226
237
  finally {
238
+ // Fire-and-forget on purpose (unlike consolidation): stage 1 has no codex
239
+ // agent-shutdown step, and memorize-extract has no write tools, so a
240
+ // lingering extract session cannot touch the memory root.
227
241
  void deleteSession(subId).catch(() => { });
228
242
  }
229
243
  }
@@ -234,15 +248,28 @@ const CONSOLIDATION_TIMEOUT_MS = 3600_000;
234
248
  export async function consolidateViaSubagent(memoryRoot, diffFileName, model) {
235
249
  const agent = "memorize";
236
250
  const subId = await createSession(agent, "codex-memory-consolidate");
251
+ let promptError;
252
+ let promptFailed = false;
237
253
  try {
238
254
  const prompt = buildConsolidationPrompt(memoryRoot, diffFileName);
239
255
  // consolidation_model option > opencode model (main) > session default.
240
256
  const resolved = model ?? (await getConfigModels()).model;
241
257
  await promptSession(subId, prompt, agent, { model: resolved, timeoutMs: CONSOLIDATION_TIMEOUT_MS });
242
258
  }
243
- finally {
244
- void deleteSession(subId).catch(() => { });
259
+ catch (err) {
260
+ promptError = err;
261
+ promptFailed = true;
245
262
  }
263
+ // codex phase2.rs awaits the consolidation agent's shutdown BEFORE artifacts
264
+ // are validated and the job is finished, and a failed shutdown outranks the
265
+ // run result: the caller must keep its lease rather than release it to a
266
+ // worker that could race a consolidator which is still alive. The
267
+ // consolidation agent holds write access to the memory root, so this is the
268
+ // difference between one writer and two.
269
+ if (!(await deleteSession(subId)))
270
+ throw new SubagentShutdownError(subId);
271
+ if (promptFailed)
272
+ throw promptError;
246
273
  }
247
274
  // Must exceed the longest legitimate sub-session lifetime (consolidation may
248
275
  // run up to CONSOLIDATION_TIMEOUT_MS = 60min), or a second opencode instance /
@@ -294,25 +321,38 @@ export async function cleanupOldSubSessions(maxAgeMinutes = 90, timeoutMs = SUBS
294
321
  function isPluginSubSessionTitle(title) {
295
322
  return title === "codex-memory-consolidate" || /^codex-memory-extract-ses_[A-Za-z0-9]+$/.test(title ?? "");
296
323
  }
324
+ /**
325
+ * Closes a sub-session. Returns true when the delete call itself succeeded —
326
+ * the port's equivalent of codex's `shutdown_consolidation_agent` returning Ok
327
+ * (runtime.rs). A false return means the sub-agent may still be running.
328
+ *
329
+ * The 404 confirmation below is a separate, stricter question (is the session
330
+ * really gone?) and only governs ownership tracking, never the shutdown result:
331
+ * hosts without `session.get` would otherwise never report a clean shutdown.
332
+ */
297
333
  async function deleteSession(id) {
298
334
  const input = getPluginInput();
299
335
  if (!input)
300
- return;
336
+ return false;
301
337
  try {
302
338
  const res = await input.client.session.delete({ path: { id } });
303
339
  if (res.error) {
304
340
  console.warn(`[opencode-codex-memory] failed to delete sub-session ${id}: ${JSON.stringify(res.error)}`);
305
- return;
341
+ return false;
306
342
  }
307
343
  // OpenCode's Session.remove logs and swallows some internal failures while
308
344
  // the HTTP route still returns success. Only a confirmed 404 proves the
309
345
  // session is gone; otherwise retain ownership so hooks keep skipping it.
346
+ // codex runtime.rs drops the thread from its manager the same way: only
347
+ // after shutdown succeeded.
310
348
  if (await sessionDeletionConfirmed(input.client, id)) {
311
349
  activeSubSessions.delete(id);
312
350
  }
351
+ return true;
313
352
  }
314
353
  catch (err) {
315
354
  console.warn(`[opencode-codex-memory] error deleting sub-session ${id}:`, err);
355
+ return false;
316
356
  }
317
357
  }
318
358
  async function sessionDeletionConfirmed(client, id) {
@@ -1,6 +1,6 @@
1
1
  import { ensureLayout, rebuildRawMemories, writeRolloutSummaries, pruneExtensionResources, writeWorkspaceDiff, validateConsolidationArtifacts, } from "./workspace.js";
2
2
  import { ensureBaseline, captureWorkspaceDiff, resetBaseline, DIFF_ARTIFACT } from "./git-baseline.js";
3
- import { consolidateViaSubagent } from "./llm.js";
3
+ import { consolidateViaSubagent, SubagentShutdownError } from "./llm.js";
4
4
  import { invalidateCache } from "./source.js";
5
5
  import { memoryRoot } from "./paths.js";
6
6
  import { checkRateLimit } from "./ratelimit.js";
@@ -105,6 +105,17 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS, rateLimitC
105
105
  try {
106
106
  await consolidateViaSubagent(memoryRoot(), DIFF_ARTIFACT, opts.consolidationModel);
107
107
  }
108
+ catch (err) {
109
+ // codex phase2.rs: when the consolidation agent's shutdown fails, keep
110
+ // the existing lease until it expires so another worker cannot race a
111
+ // consolidator whose shutdown has not completed. Neither succeed nor
112
+ // fail the job — marking it failed would release the lease immediately.
113
+ if (err instanceof SubagentShutdownError) {
114
+ console.warn(`[opencode-codex-memory] ${err.message}; holding the phase2 lease until it expires`);
115
+ return { status: "shutdown_failed" };
116
+ }
117
+ throw err;
118
+ }
108
119
  finally {
109
120
  clearInterval(heartbeat);
110
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codex-memory",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",