opencode-codex-memory 0.4.7 → 0.4.9

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.
@@ -8,7 +8,9 @@ import { estimateTokens } from "../src/token.js";
8
8
  import { assertMemoryRootSafe, readRegularFileNoFollow } from "../src/path-guard.js";
9
9
  import { isPhase2InFlight } from "../src/phase2.js";
10
10
  import { pluginOptions, getConfigWarnings } from "../src/options.js";
11
- import { resolveCodexInterop } from "../src/codex-interop.js";
11
+ import { codexInteropMtimes, resolveCodexInterop } from "../src/codex-interop.js";
12
+ import { formatDiagnosticLine, getDiscoveryStatus, getRecentDiagnostics, } from "../src/diagnostics.js";
13
+ import { isPluginShuttingDown } from "../src/lifecycle.js";
12
14
  function isSymlinkedRoot() {
13
15
  try {
14
16
  assertMemoryRootSafe();
@@ -73,6 +75,11 @@ function renderEffectiveConfig() {
73
75
  else {
74
76
  const reachable = fs.existsSync(resolved.codexMemoryRoot);
75
77
  lines.push(` codex_interop: import=${ci.import} export=${ci.export}`, ` codex memories: ${resolved.codexMemoryRoot}${reachable ? "" : " (not found yet — nothing is imported/exported until Codex's memory feature creates it)"}`);
78
+ if (reachable) {
79
+ const mt = codexInteropMtimes(resolved.codexMemoryRoot);
80
+ const fmt = (ms) => (ms == null ? "none" : new Date(ms).toISOString());
81
+ lines.push(` last import mtimes: MEMORY.md=${fmt(mt.importMemoryMd)} summary=${fmt(mt.importSummary)}`, ` last export mtimes: MEMORY.md=${fmt(mt.exportMemoryMd)} summary=${fmt(mt.exportSummary)}`);
82
+ }
76
83
  }
77
84
  }
78
85
  const warnings = getConfigWarnings();
@@ -129,9 +136,11 @@ export const memory_reset = tool({
129
136
  }
130
137
  // A consolidation running in THIS process would recreate files right
131
138
  // after the wipe (the sub-agent edits live artifacts and resets the git
132
- // baseline). Refuse instead of racing it. Cross-process consolidators
133
- // are still ownership-guarded DB-side (the wiped job rows make their
134
- // final confirmation a no-op) but may leave stray files; same window
139
+ // baseline). Refuse instead of racing it. clearMemoryData also leaves a
140
+ // phase-2 cooldown marker so the next idle/chat hook cannot first-run-claim
141
+ // phase 2 and re-seed the root via ensureLayout. Cross-process consolidators
142
+ // already in flight remain ownership-guarded (their final mark becomes a
143
+ // no-op once the row is replaced) but may leave stray files — same window
135
144
  // codex has between CLI clear and a running daemon.
136
145
  if (isPhase2InFlight()) {
137
146
  return { output: "Reset refused: memory consolidation is currently running. Try again in a few minutes." };
@@ -161,11 +170,12 @@ function fmtWatermarkMs(ms) {
161
170
  return new Date(ms).toISOString();
162
171
  }
163
172
  export const memory_inspect = tool({
164
- description: "Inspect the current memory state. Returns: stage1_outputs count, Phase 2 job status " +
165
- "(including last error / retry time when failed), last Phase 2 success watermark, " +
166
- "memory_summary token estimate (on-disk; injection caps at ~2500), a listing of the " +
167
- "memories directory, the effective plugin options, and any configuration warnings " +
168
- "(unknown/malformed options). Use it to verify the plugin configuration took effect. Read-only.",
173
+ description: "Inspect the current memory state. Returns: stage1_outputs count, stage-1 job status " +
174
+ "breakdown and recent errors, Phase 2 job status (including last error / retry time), " +
175
+ "last discovery outcome, pipeline diagnostics, memory_summary token estimate " +
176
+ "(on-disk; injection caps at ~2500), a listing of the memories directory, the " +
177
+ "effective plugin options, and any configuration warnings. Use it to verify " +
178
+ "configuration and debug why memory is not building. Read-only.",
169
179
  args: {},
170
180
  async execute() {
171
181
  try {
@@ -173,6 +183,7 @@ export const memory_inspect = tool({
173
183
  assertMemoryRootSafe();
174
184
  const store = new MemoryStore();
175
185
  const outputs = store.stage1Outputs();
186
+ const stage1Jobs = store.stage1JobSnapshot();
176
187
  const summaryPath = memorySummaryPath();
177
188
  let summaryChars = 0;
178
189
  let summaryTokens = 0;
@@ -201,15 +212,44 @@ export const memory_inspect = tool({
201
212
  "phase2_last_success_watermark: none",
202
213
  "phase2_last_success_finished_at: none",
203
214
  ];
215
+ const stage1StatusParts = Object.entries(stage1Jobs.by_status)
216
+ .sort(([a], [b]) => a.localeCompare(b))
217
+ .map(([s, c]) => `${s}=${c}`);
218
+ const stage1Lines = [
219
+ `stage1_jobs: ${stage1StatusParts.length > 0 ? stage1StatusParts.join(" ") : "none"}`,
220
+ ...stage1Jobs.recent_errors.map((e) => ` stage1_error ${e.session_id} (${e.status}): ${e.last_error.slice(0, 200)}${e.retry_at ? ` retry_at=${fmtUnixSec(e.retry_at)}` : ""}`),
221
+ ];
222
+ const discovery = getDiscoveryStatus();
223
+ const discoveryLine = discovery
224
+ ? `discovery: ${discovery.ok ? "ok" : "failed"} count=${discovery.count} at=${new Date(discovery.at).toISOString()}${discovery.error ? ` error=${discovery.error}` : ""}`
225
+ : "discovery: never ran (no phase-1 pass yet this process)";
226
+ const idleHours = pluginOptions.min_rollout_idle_hours;
227
+ const eligibilityHint = `eligibility: sessions must be idle ≥ ${idleHours}h and younger than ${pluginOptions.max_rollout_age_days}d ` +
228
+ `(generate_memories=${pluginOptions.generate_memories}). ` +
229
+ `For faster local testing, set min_rollout_idle_hours to 1 (clamp floor).`;
230
+ const processLines = [
231
+ `phase2_in_flight: ${isPhase2InFlight()}`,
232
+ `plugin_shutting_down: ${isPluginShuttingDown()}`,
233
+ ];
234
+ const diagnostics = getRecentDiagnostics(12);
235
+ const diagnosticLines = diagnostics.length > 0
236
+ ? ["recent_events:", ...diagnostics.map((e) => ` ${formatDiagnosticLine(e)}`)]
237
+ : ["recent_events: none"];
204
238
  const out = [
205
239
  `stage1_outputs: ${outputs.length}`,
240
+ ...stage1Lines,
206
241
  ...phase2Lines,
242
+ discoveryLine,
243
+ eligibilityHint,
244
+ ...processLines,
207
245
  `memory_summary_chars: ${summaryChars}`,
208
246
  `memory_summary_tokens_est: ${summaryTokens} (on disk; injection caps at ~2500)`,
209
247
  `memories_dir_entries: ${listing.length}`,
210
248
  "",
211
249
  ...renderEffectiveConfig(),
212
250
  "",
251
+ ...diagnosticLines,
252
+ "",
213
253
  "Files:",
214
254
  listing.length > 0 ? listing.join("\n") : "(empty)",
215
255
  ].join("\n");
@@ -217,6 +257,8 @@ export const memory_inspect = tool({
217
257
  output: out,
218
258
  metadata: {
219
259
  stage1_count: outputs.length,
260
+ stage1_jobs: stage1Jobs.by_status,
261
+ stage1_recent_errors: stage1Jobs.recent_errors,
220
262
  phase2_status: phase2?.status ?? null,
221
263
  phase2_last_error: phase2?.last_error ?? null,
222
264
  phase2_retry_at: phase2?.retry_at ?? null,
@@ -225,11 +267,13 @@ export const memory_inspect = tool({
225
267
  phase2_last_success_finished_at: phase2?.success_finished_at ?? null,
226
268
  // Back-compat aliases used by earlier inspect consumers.
227
269
  phase2_last_finished_at: phase2?.success_finished_at ?? null,
270
+ discovery,
228
271
  summary_chars: summaryChars,
229
272
  summary_tokens_est: summaryTokens,
230
273
  files: listing,
231
274
  effective_options: { ...pluginOptions, codex_interop: { ...pluginOptions.codex_interop } },
232
275
  config_warnings: [...getConfigWarnings()],
276
+ recent_events: diagnostics,
233
277
  },
234
278
  };
235
279
  }
@@ -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
- let names;
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
- names = fs.readdirSync(dir);
76
+ ents = fs.readdirSync(dir, { withFileTypes: true });
75
77
  }
76
78
  catch {
77
79
  return [];
78
80
  }
79
81
  const out = [];
80
- for (const name of names) {
81
- if (name.startsWith("."))
82
+ for (const ent of ents) {
83
+ if (ent.name.startsWith("."))
82
84
  continue;
83
- let st;
84
- try {
85
- st = fs.lstatSync(path.join(dir, name));
86
- }
87
- catch {
85
+ if (ent.isSymbolicLink())
88
86
  continue;
89
- }
90
- if (st.isSymbolicLink())
91
- continue;
92
- out.push({ name, isDir: st.isDirectory() });
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
  }
@@ -106,7 +104,14 @@ export const memory_list = tool({
106
104
  const fullPath = safeResolveMemoryPath(args.path || ".");
107
105
  if (!fs.existsSync(fullPath))
108
106
  return { output: `Not found: ${args.path}` };
109
- if (!fs.statSync(fullPath).isDirectory())
107
+ // lstat: do not follow a TOCTOU symlink swap after safeResolve checked.
108
+ // Symlinked dirs are rejected by design (same as path-guard / codex), not
109
+ // a regression from the TOCTOU fix — report that explicitly.
110
+ const st = fs.lstatSync(fullPath);
111
+ if (st.isSymbolicLink()) {
112
+ return { output: `memory_list error: symlinks are not allowed in the memory workspace: ${args.path}` };
113
+ }
114
+ if (!st.isDirectory())
110
115
  return { output: `memory_list error: not a directory: ${args.path}` };
111
116
  const entries = visibleEntries(fullPath).sort((a, b) => a.name.localeCompare(b.name));
112
117
  const truncated = entries.length > args.max_results;
package/opencode.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "agent": {
4
4
  "memorize": {
5
5
  "mode": "subagent",
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.",
6
+ "prompt": "You are a memory consolidation agent. Read the workspace diff file and update MEMORY.md, memory_summary.md, and skills/ under the memory workspace only. Do not read or edit project source files outside that memory root. Keep memory_summary.md under 10000 chars (2500 tokens). Prune stale entries. Do not access the network.",
7
7
  "permission": {
8
8
  "*": "deny",
9
9
  "bash": "deny",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codex-memory",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
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",