opencode-memoir 1.1.3 → 1.1.5
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/capture-hooks.d.ts +7 -0
- package/dist/capture-hooks.d.ts.map +1 -0
- package/dist/capture.d.ts +2 -1
- package/dist/capture.d.ts.map +1 -1
- package/dist/commands.d.ts +12 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +545 -444
- package/dist/index.js.map +4 -4
- package/dist/memoir-ops.d.ts +7 -0
- package/dist/memoir-ops.d.ts.map +1 -0
- package/dist/recall-gate.d.ts +6 -2
- package/dist/recall-gate.d.ts.map +1 -1
- package/dist/session-context.d.ts +8 -0
- package/dist/session-context.d.ts.map +1 -0
- package/dist/store.d.ts +30 -4
- package/dist/store.d.ts.map +1 -1
- package/dist/tools.d.ts +49 -0
- package/dist/tools.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { mkdir as mkdir2, readFile, rm as rm2, writeFile } from "node:fs/promises";
|
|
5
|
-
import { join as join2 } from "node:path";
|
|
6
|
-
import { homedir as homedir2 } from "node:os";
|
|
7
|
-
import { tool } from "@opencode-ai/plugin";
|
|
8
|
-
|
|
9
1
|
// src/store.ts
|
|
10
2
|
import { execFile, execFileSync } from "node:child_process";
|
|
11
3
|
import { access, mkdir, rm } from "node:fs/promises";
|
|
@@ -62,9 +54,28 @@ var GIT_TIMEOUT_MS = 3e3;
|
|
|
62
54
|
var MEMOIR_TIMEOUT_MS = 15e3;
|
|
63
55
|
var MEMOIR_MAX_BUFFER = 1024 * 1024;
|
|
64
56
|
var NO_GIT_CACHE_MAX = 500;
|
|
65
|
-
var
|
|
57
|
+
var NO_GIT_CACHE_TTL = 5 * 60 * 1e3;
|
|
58
|
+
function evictOldestNoGit() {
|
|
59
|
+
const firstKey = _noGitCache.keys().next().value;
|
|
60
|
+
if (firstKey !== void 0) _noGitCache.delete(firstKey);
|
|
61
|
+
}
|
|
62
|
+
function sweepNoGitCache() {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
for (const [key, ts] of _noGitCache) {
|
|
65
|
+
if (now - ts >= NO_GIT_CACHE_TTL) _noGitCache.delete(key);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
var _noGitCache = /* @__PURE__ */ new Map();
|
|
69
|
+
function _checkNoGit(cwd) {
|
|
70
|
+
const ts = _noGitCache.get(cwd);
|
|
71
|
+
if (ts === void 0) return false;
|
|
72
|
+
const age = Date.now() - ts;
|
|
73
|
+
if (age < NO_GIT_CACHE_TTL) return true;
|
|
74
|
+
_noGitCache.delete(cwd);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
66
77
|
function _main_worktree_root(cwd) {
|
|
67
|
-
if (
|
|
78
|
+
if (_checkNoGit(cwd)) return "";
|
|
68
79
|
try {
|
|
69
80
|
const gitDir = execFileSync("git", ["rev-parse", "--git-dir"], { cwd, encoding: "utf8", timeout: GIT_TIMEOUT_MS }).trim();
|
|
70
81
|
const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], { cwd, encoding: "utf8", timeout: GIT_TIMEOUT_MS }).trim();
|
|
@@ -88,8 +99,9 @@ function _main_worktree_root(cwd) {
|
|
|
88
99
|
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", timeout: GIT_TIMEOUT_MS }).trim();
|
|
89
100
|
} catch (e) {
|
|
90
101
|
debugLog("_main_worktree_root: not a git repo or git error:", errorMessage(e));
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
sweepNoGitCache();
|
|
103
|
+
_noGitCache.set(cwd, Date.now());
|
|
104
|
+
if (_noGitCache.size > NO_GIT_CACHE_MAX) evictOldestNoGit();
|
|
93
105
|
return "";
|
|
94
106
|
}
|
|
95
107
|
}
|
|
@@ -140,8 +152,8 @@ async function ensureStore(store) {
|
|
|
140
152
|
await mkdir(tmpDir, { recursive: true });
|
|
141
153
|
await execFileAsync("git", ["init", "-q", tmpDir], { timeout: 5e3 });
|
|
142
154
|
const result = await runMemoir(["new", store, "--taxonomy-builtin"], { cwd: tmpDir });
|
|
143
|
-
if (result.
|
|
144
|
-
throw new Error(result);
|
|
155
|
+
if (!result.ok) {
|
|
156
|
+
throw new Error(result.error);
|
|
145
157
|
}
|
|
146
158
|
ensuredStores.add(store);
|
|
147
159
|
if (ensuredStores.size > ENSURED_STORES_MAX) ensuredStores.clear();
|
|
@@ -175,6 +187,7 @@ async function runMemoir(args, options = {}) {
|
|
|
175
187
|
if (winner) specs = reorderResolver(specs, winner);
|
|
176
188
|
}
|
|
177
189
|
let lastError = "";
|
|
190
|
+
let lastResolver;
|
|
178
191
|
for (const spec of specs) {
|
|
179
192
|
try {
|
|
180
193
|
const { stdout } = await execFileAsync(spec.command, spec.args, {
|
|
@@ -185,14 +198,15 @@ async function runMemoir(args, options = {}) {
|
|
|
185
198
|
// prevent hang if memoir CLI stalls
|
|
186
199
|
});
|
|
187
200
|
memoirResolved = spec.label;
|
|
188
|
-
return stdout.trim();
|
|
201
|
+
return { ok: true, stdout: stdout.trim() };
|
|
189
202
|
} catch (e) {
|
|
190
203
|
lastError = errorMessage(e);
|
|
204
|
+
lastResolver = spec.label;
|
|
191
205
|
debugLog("runMemoir: fallback", spec.label, "failed:", lastError);
|
|
192
206
|
if (memoirResolved === spec.label) memoirResolved = null;
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
|
-
return `Memoir command failed: ${lastError}
|
|
209
|
+
return { ok: false, error: `Memoir command failed: ${lastError}`, resolver: lastResolver };
|
|
196
210
|
}
|
|
197
211
|
function memoirSpawnSpecs(args) {
|
|
198
212
|
return [
|
|
@@ -203,8 +217,10 @@ function memoirSpawnSpecs(args) {
|
|
|
203
217
|
}
|
|
204
218
|
async function getCurrentBranch(store) {
|
|
205
219
|
try {
|
|
206
|
-
const
|
|
207
|
-
const
|
|
220
|
+
const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
|
|
221
|
+
const result = await runFn(["--json", "-s", store, "status"], { cwd: store });
|
|
222
|
+
if (!result.ok) return "unknown";
|
|
223
|
+
const data = JSON.parse(result.stdout);
|
|
208
224
|
return data.branch || "unknown";
|
|
209
225
|
} catch (e) {
|
|
210
226
|
debugLog("getCurrentBranch: failed:", errorMessage(e));
|
|
@@ -213,7 +229,8 @@ async function getCurrentBranch(store) {
|
|
|
213
229
|
}
|
|
214
230
|
async function codeGitBranch() {
|
|
215
231
|
try {
|
|
216
|
-
const
|
|
232
|
+
const execFn = _storeTestOverrides.execFileAsyncFn ?? execFileAsync;
|
|
233
|
+
const { stdout } = await execFn("git", ["branch", "--show-current"], { encoding: "utf8", timeout: GIT_TIMEOUT_MS });
|
|
217
234
|
return stdout.trim();
|
|
218
235
|
} catch (e) {
|
|
219
236
|
debugLog("codeGitBranch: failed:", errorMessage(e));
|
|
@@ -223,8 +240,10 @@ async function codeGitBranch() {
|
|
|
223
240
|
async function branchExistsInMemoir(store, name) {
|
|
224
241
|
if (!name) return false;
|
|
225
242
|
try {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
243
|
+
const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
|
|
244
|
+
const result = await runFn(["--json", "-s", store, "branch"], { cwd: store });
|
|
245
|
+
if (!result.ok) return false;
|
|
246
|
+
const data = JSON.parse(result.stdout);
|
|
228
247
|
const branches = data?.branches ?? [];
|
|
229
248
|
return branches.includes(name);
|
|
230
249
|
} catch (e) {
|
|
@@ -233,6 +252,7 @@ async function branchExistsInMemoir(store, name) {
|
|
|
233
252
|
}
|
|
234
253
|
}
|
|
235
254
|
async function autoMatchMemoirBranch(store) {
|
|
255
|
+
const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
|
|
236
256
|
const codeBranch = await codeGitBranch();
|
|
237
257
|
if (!codeBranch) {
|
|
238
258
|
return getCurrentBranch(store);
|
|
@@ -240,23 +260,25 @@ async function autoMatchMemoirBranch(store) {
|
|
|
240
260
|
const current = await getCurrentBranch(store);
|
|
241
261
|
if (current === codeBranch) return current;
|
|
242
262
|
if (!await branchExistsInMemoir(store, codeBranch)) {
|
|
243
|
-
const result2 = await
|
|
244
|
-
if (result2.
|
|
245
|
-
debugLog("autoMatchMemoirBranch: create branch failed:", result2);
|
|
263
|
+
const result2 = await runFn(["-s", store, "branch", codeBranch, "--from", "main"], { cwd: store });
|
|
264
|
+
if (!result2.ok) {
|
|
265
|
+
debugLog("autoMatchMemoirBranch: create branch failed:", result2.error);
|
|
246
266
|
return getCurrentBranch(store);
|
|
247
267
|
}
|
|
248
268
|
}
|
|
249
|
-
const result = await
|
|
250
|
-
if (result.
|
|
251
|
-
debugLog("autoMatchMemoirBranch: checkout failed:", result);
|
|
269
|
+
const result = await runFn(["-s", store, "checkout", codeBranch], { cwd: store });
|
|
270
|
+
if (!result.ok) {
|
|
271
|
+
debugLog("autoMatchMemoirBranch: checkout failed:", result.error);
|
|
252
272
|
return getCurrentBranch(store);
|
|
253
273
|
}
|
|
254
274
|
return codeBranch;
|
|
255
275
|
}
|
|
256
276
|
async function readMemoirValue(store, key, namespace = "default") {
|
|
257
277
|
try {
|
|
258
|
-
const
|
|
259
|
-
const
|
|
278
|
+
const runFn = _storeTestOverrides.runMemoirFn ?? runMemoir;
|
|
279
|
+
const result = await runFn(["--json", "-s", store, "get", key, "-n", namespace], { cwd: store });
|
|
280
|
+
if (!result.ok) return "";
|
|
281
|
+
const parsed = JSON.parse(result.stdout);
|
|
260
282
|
const items = parsed?.items ?? [];
|
|
261
283
|
const value = items[0]?.value?.content;
|
|
262
284
|
return typeof value === "string" ? value : "";
|
|
@@ -265,221 +287,17 @@ async function readMemoirValue(store, key, namespace = "default") {
|
|
|
265
287
|
return "";
|
|
266
288
|
}
|
|
267
289
|
}
|
|
290
|
+
var _storeTestOverrides = {};
|
|
268
291
|
|
|
269
|
-
// src/
|
|
270
|
-
|
|
271
|
-
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch", "ApplyPatch", "MultiFileEdit"]);
|
|
272
|
-
function evictOldest(map, maxSize) {
|
|
273
|
-
if (map.size <= maxSize) return;
|
|
274
|
-
const firstKey = map.keys().next().value;
|
|
275
|
-
if (firstKey !== void 0) map.delete(firstKey);
|
|
276
|
-
}
|
|
277
|
-
var METRICS_CODE_MAX = 1e3;
|
|
278
|
-
var pendingEditsBySession = /* @__PURE__ */ new Map();
|
|
279
|
-
var toolMetricsBySession = /* @__PURE__ */ new Map();
|
|
280
|
-
var cachedBranchBySession = /* @__PURE__ */ new Map();
|
|
281
|
-
function getCachedBranch(sessionID) {
|
|
282
|
-
return cachedBranchBySession.get(sessionID) ?? "unknown";
|
|
283
|
-
}
|
|
284
|
-
function setCachedBranch(sessionID, branch) {
|
|
285
|
-
cachedBranchBySession.set(sessionID, branch);
|
|
286
|
-
evictOldest(cachedBranchBySession, 200);
|
|
287
|
-
}
|
|
288
|
-
function recordEdit(sessionID, edit) {
|
|
289
|
-
let edits = pendingEditsBySession.get(sessionID);
|
|
290
|
-
if (!edits) {
|
|
291
|
-
edits = [];
|
|
292
|
-
pendingEditsBySession.set(sessionID, edits);
|
|
293
|
-
evictOldest(pendingEditsBySession, 500);
|
|
294
|
-
}
|
|
295
|
-
edits.push(edit);
|
|
296
|
-
}
|
|
297
|
-
function recordToolMetrics(sessionID, tool2, metrics) {
|
|
298
|
-
let sessionMetrics = toolMetricsBySession.get(sessionID);
|
|
299
|
-
if (!sessionMetrics) {
|
|
300
|
-
sessionMetrics = /* @__PURE__ */ new Map();
|
|
301
|
-
toolMetricsBySession.set(sessionID, sessionMetrics);
|
|
302
|
-
evictOldest(toolMetricsBySession, 500);
|
|
303
|
-
}
|
|
304
|
-
const prev = sessionMetrics.get(tool2) ?? { calls: 0, errors: 0 };
|
|
305
|
-
sessionMetrics.set(tool2, {
|
|
306
|
-
calls: prev.calls + metrics.calls,
|
|
307
|
-
errors: prev.errors + metrics.errors
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
function parseTurnMetrics(text) {
|
|
311
|
-
const result = /* @__PURE__ */ new Map();
|
|
312
|
-
if (!text) return result;
|
|
313
|
-
const parts = text.split("|");
|
|
314
|
-
for (const part of parts) {
|
|
315
|
-
const trimmed = part.trim();
|
|
316
|
-
if (!trimmed) continue;
|
|
317
|
-
const [tool2, callsStr, errorsStr] = trimmed.split(":");
|
|
318
|
-
if (!tool2) continue;
|
|
319
|
-
result.set(tool2, {
|
|
320
|
-
calls: parseInt(callsStr, 10) || 0,
|
|
321
|
-
errors: parseInt(errorsStr, 10) || 0
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
return result;
|
|
325
|
-
}
|
|
326
|
-
function serializeTurnMetrics(map) {
|
|
327
|
-
return [...map.entries()].map(([tool2, m]) => `${tool2}:${m.calls}:${m.errors}`).join(" | ");
|
|
328
|
-
}
|
|
329
|
-
var flushQueues = /* @__PURE__ */ new Map();
|
|
330
|
-
async function acquireFlushLock(store) {
|
|
331
|
-
const prev = flushQueues.get(store) ?? Promise.resolve();
|
|
332
|
-
let release;
|
|
333
|
-
const next = new Promise((resolve2) => {
|
|
334
|
-
release = resolve2;
|
|
335
|
-
});
|
|
336
|
-
flushQueues.set(store, next);
|
|
337
|
-
await prev;
|
|
338
|
-
return () => {
|
|
339
|
-
release();
|
|
340
|
-
if (flushQueues.get(store) === next) flushQueues.delete(store);
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
async function flushCapture(store, branch, sessionID) {
|
|
344
|
-
const targets = sessionID ? [sessionID] : [...pendingEditsBySession.keys()];
|
|
345
|
-
if (targets.length === 0) return;
|
|
346
|
-
try {
|
|
347
|
-
store = store ?? deriveStorePath();
|
|
348
|
-
if (!store) return;
|
|
349
|
-
await ensureStore(store);
|
|
350
|
-
const releaseLock = await acquireFlushLock(store);
|
|
351
|
-
try {
|
|
352
|
-
const perSession = [];
|
|
353
|
-
const swappedSids = /* @__PURE__ */ new Map();
|
|
354
|
-
for (const sid of targets) {
|
|
355
|
-
const idx = perSession.length;
|
|
356
|
-
const edits = pendingEditsBySession.get(sid) ?? [];
|
|
357
|
-
pendingEditsBySession.set(sid, []);
|
|
358
|
-
const metrics = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
359
|
-
toolMetricsBySession.set(sid, /* @__PURE__ */ new Map());
|
|
360
|
-
const sb = branch ?? cachedBranchBySession.get(sid);
|
|
361
|
-
perSession.push({ edits, metrics, sessionBranch: sb ?? "unknown" });
|
|
362
|
-
swappedSids.set(sid, idx);
|
|
363
|
-
}
|
|
364
|
-
if (!perSession.some((p) => p.edits.length > 0 || p.metrics.size > 0)) return;
|
|
365
|
-
try {
|
|
366
|
-
for (const p of perSession) {
|
|
367
|
-
if (p.sessionBranch === "unknown" || !p.sessionBranch) {
|
|
368
|
-
p.sessionBranch = await (_cliOverrides?.getCurrentBranch ?? getCurrentBranch)(store);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
for (const p of perSession) {
|
|
372
|
-
const { edits, metrics, sessionBranch: branchKey } = p;
|
|
373
|
-
if (edits.length === 0 && metrics.size === 0) continue;
|
|
374
|
-
const [prevCodeRaw, prevTurnRaw] = await Promise.all([
|
|
375
|
-
edits.length > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(store, `metrics.code.${branchKey}`) : Promise.resolve(""),
|
|
376
|
-
metrics.size > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(store, `metrics.turn.${branchKey}`) : Promise.resolve("")
|
|
377
|
-
]);
|
|
378
|
-
let codeWrite;
|
|
379
|
-
if (edits.length > 0) {
|
|
380
|
-
const files = [...new Set(edits.map((e) => e.filePath))];
|
|
381
|
-
const entry = {
|
|
382
|
-
timestamp: Date.now() / 1e3,
|
|
383
|
-
summary: `Changed ${edits.length} block(s) across ${files.length} file(s): ${files.join(", ")}`,
|
|
384
|
-
files
|
|
385
|
-
};
|
|
386
|
-
let acc = {
|
|
387
|
-
schema_version: 2,
|
|
388
|
-
entries: []
|
|
389
|
-
};
|
|
390
|
-
if (prevCodeRaw) {
|
|
391
|
-
try {
|
|
392
|
-
const parsed = JSON.parse(prevCodeRaw);
|
|
393
|
-
if (parsed?.entries && Array.isArray(parsed.entries)) {
|
|
394
|
-
acc = parsed;
|
|
395
|
-
if (acc.schema_version < 2) acc.schema_version = 2;
|
|
396
|
-
}
|
|
397
|
-
} catch (e) {
|
|
398
|
-
debugLog("flushCapture: failed to parse existing code metrics, starting fresh:", errorMessage(e));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
acc.entries.push(entry);
|
|
402
|
-
if (acc.entries.length > METRICS_CODE_MAX) acc.entries = acc.entries.slice(-METRICS_CODE_MAX);
|
|
403
|
-
codeWrite = (_cliOverrides?.runMemoir ?? runMemoir)(["-s", store, "remember", "--replace", JSON.stringify(acc), "-p", `metrics.code.${branchKey}`], { cwd: store });
|
|
404
|
-
}
|
|
405
|
-
let turnWrite;
|
|
406
|
-
if (metrics.size > 0) {
|
|
407
|
-
const existing = parseTurnMetrics(prevTurnRaw);
|
|
408
|
-
for (const [tool2, current] of metrics) {
|
|
409
|
-
const prev = existing.get(tool2) ?? { calls: 0, errors: 0 };
|
|
410
|
-
existing.set(tool2, { calls: prev.calls + current.calls, errors: prev.errors + current.errors });
|
|
411
|
-
}
|
|
412
|
-
turnWrite = (_cliOverrides?.runMemoir ?? runMemoir)(["-s", store, "remember", "--replace", serializeTurnMetrics(existing), "-p", `metrics.turn.${branchKey}`], { cwd: store });
|
|
413
|
-
}
|
|
414
|
-
const results = await Promise.all([codeWrite, turnWrite].filter(Boolean));
|
|
415
|
-
for (const result of results) {
|
|
416
|
-
if (typeof result === "string" && result.startsWith("Memoir command failed")) {
|
|
417
|
-
throw new Error(`Memoir write failed: ${result}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
} catch (e) {
|
|
422
|
-
for (const [sid, idx] of swappedSids) {
|
|
423
|
-
const p = perSession[idx];
|
|
424
|
-
const currentEdits = pendingEditsBySession.get(sid) ?? [];
|
|
425
|
-
pendingEditsBySession.set(sid, [...p.edits, ...currentEdits]);
|
|
426
|
-
const merged = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
427
|
-
for (const [tool2, m] of p.metrics) {
|
|
428
|
-
const prev = merged.get(tool2) ?? { calls: 0, errors: 0 };
|
|
429
|
-
merged.set(tool2, { calls: prev.calls + m.calls, errors: prev.errors + m.errors });
|
|
430
|
-
}
|
|
431
|
-
toolMetricsBySession.set(sid, merged);
|
|
432
|
-
}
|
|
433
|
-
throw e;
|
|
434
|
-
}
|
|
435
|
-
} finally {
|
|
436
|
-
releaseLock();
|
|
437
|
-
}
|
|
438
|
-
} catch (e) {
|
|
439
|
-
debugLog("flushCapture: failed:", errorMessage(e));
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// src/recall-gate.ts
|
|
444
|
-
function isSecretSanitizationEnabled() {
|
|
445
|
-
return process.env.MEMOIR_SANITIZE_SECRETS !== "0";
|
|
446
|
-
}
|
|
447
|
-
var pendingRecall = /* @__PURE__ */ new Set();
|
|
448
|
-
var ACK_PATTERN = /^(ok|thanks|thank you|sounds good|got it|👍|🙏|perfect|great|cool|nice|awesome|understood|makes sense|agree|right|sure|yes|no|done|nvm|never mind|lgtm|looks good|proceed|continue|good|fine)\b/i;
|
|
449
|
-
var EXPLICIT_RECALL_PATTERN = /\b(memoir:recall|memoir:remember|memoir-recall|memoir-remember)\b|(\/recall|\/remember)\b/i;
|
|
450
|
-
var RECALL_TRIGGER_PATTERNS = [
|
|
451
|
-
// Action verbs and domain nouns
|
|
452
|
-
/\b(add|build|implement|refactor|redesign|design|create|write|set( |-)up|wire( |-)up|integrate|migrate|rewrite|extract|extend|plumb|hook( |-)up|ship|scaffold|optimize|fix|debug|review|architect|model|schema|API|service|feature|module|system|pipeline|workflow|make|move|replace|convert|swap|remove|clean( |-)up|transform|investigate|explore|figure( |-)out|plan|decide|choose|pick|compare|walk( |-)?me( |-)?through|take( |-)?a( |-)?stab|help( |-)?me|harness|hook|prompt|test)\b/i,
|
|
453
|
-
// Question starts (how/why/what/where/when/should/can/could/would/is it/are we/do I/does it)
|
|
454
|
-
/^(how|why|what|where|when|should|can|could|would|is it|are we|do I|does it)\b.*\?/im,
|
|
455
|
-
// Code blocks (triple backticks)
|
|
456
|
-
/```/,
|
|
457
|
-
// Code definitions
|
|
458
|
-
/\b(def|function|class|import|export)\s+/,
|
|
459
|
-
// Memoir/recall keywords
|
|
460
|
-
/\b(memoir|recall|remember|memory)\b/i,
|
|
461
|
-
// File extensions
|
|
462
|
-
/\b\w+\.(py|js|ts|tsx|scala|java|go|rs|rb|md|json|yaml|yml|toml|sh|bash|css|html|kt|swift|c|cpp|h|hpp)\b/i,
|
|
463
|
-
// File paths (slash-containing tokens)
|
|
464
|
-
/\w+\/\w+/
|
|
465
|
-
];
|
|
466
|
-
function isAcknowledgement(text) {
|
|
467
|
-
const trimmed = text.trim().toLowerCase();
|
|
468
|
-
const words = trimmed.split(/\s+/);
|
|
469
|
-
return words.length <= 5 && ACK_PATTERN.test(trimmed);
|
|
470
|
-
}
|
|
471
|
-
function shouldTriggerRecall(text) {
|
|
472
|
-
const trimmed = text.trim();
|
|
473
|
-
if (!trimmed) return false;
|
|
474
|
-
if (EXPLICIT_RECALL_PATTERN.test(trimmed)) return true;
|
|
475
|
-
if (trimmed.length < 10) return false;
|
|
476
|
-
if (isAcknowledgement(trimmed)) return false;
|
|
477
|
-
if (trimmed.length >= 40 && RECALL_TRIGGER_PATTERNS.some((p) => p.test(trimmed))) return true;
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
var DEFAULT_RECALL_NAMESPACES = ["default", "project:onboard", "codebase:onboard"];
|
|
292
|
+
// src/tools.ts
|
|
293
|
+
import { tool } from "@opencode-ai/plugin";
|
|
481
294
|
|
|
482
|
-
// src/
|
|
295
|
+
// src/memoir-ops.ts
|
|
296
|
+
import { spawn } from "node:child_process";
|
|
297
|
+
import { createHash } from "node:crypto";
|
|
298
|
+
import { mkdir as mkdir2, readFile, rm as rm2, writeFile } from "node:fs/promises";
|
|
299
|
+
import { join as join2 } from "node:path";
|
|
300
|
+
import { homedir as homedir2 } from "node:os";
|
|
483
301
|
var UI_URL_DEADLINE_MS = 5e3;
|
|
484
302
|
var UI_URL_POLL_MS = 100;
|
|
485
303
|
async function ensureStoreOrError(store) {
|
|
@@ -493,15 +311,16 @@ async function ensureStoreOrError(store) {
|
|
|
493
311
|
async function statusJson(store) {
|
|
494
312
|
const storeErr = await ensureStoreOrError(store);
|
|
495
313
|
if (storeErr) return `Memoir command failed: ${storeErr}`;
|
|
496
|
-
const
|
|
314
|
+
const result = await runMemoir(["--json", "-s", store, "status"], { cwd: store });
|
|
315
|
+
if (!result.ok) return result.error;
|
|
497
316
|
try {
|
|
498
|
-
const data = JSON.parse(
|
|
317
|
+
const data = JSON.parse(result.stdout);
|
|
499
318
|
const branch = await codeGitBranch();
|
|
500
319
|
data.opencode = { store, project_git_root: process.cwd(), project_git_branch: branch };
|
|
501
320
|
return JSON.stringify(data, null, 2);
|
|
502
321
|
} catch (e) {
|
|
503
322
|
debugLog("statusJson: failed to parse JSON:", errorMessage(e));
|
|
504
|
-
return
|
|
323
|
+
return result.stdout;
|
|
505
324
|
}
|
|
506
325
|
}
|
|
507
326
|
async function launchUi(store) {
|
|
@@ -534,7 +353,10 @@ async function launchUi(store) {
|
|
|
534
353
|
const child = spawn(spec.command, spec.args, {
|
|
535
354
|
detached: true,
|
|
536
355
|
stdio: ["ignore", "pipe", "pipe"],
|
|
537
|
-
|
|
356
|
+
// Scrub process.env — only forward whitelisted vars to avoid leaking secrets
|
|
357
|
+
env: Object.fromEntries(
|
|
358
|
+
Object.entries({ PATH: process.env.PATH, MEMOIR_STORE: process.env.MEMOIR_STORE, MEMOIR_DEBUG: process.env.MEMOIR_DEBUG }).filter(([, v]) => v != null)
|
|
359
|
+
)
|
|
538
360
|
});
|
|
539
361
|
let output = "";
|
|
540
362
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -570,86 +392,70 @@ ${output.trim()}`.trim();
|
|
|
570
392
|
}
|
|
571
393
|
return `Memoir UI failed to start: ${lastError || "no launcher succeeded"}`;
|
|
572
394
|
}
|
|
573
|
-
function
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
var initContextFetched = false;
|
|
579
|
-
var SESSION_CONTEXT_TTL_MS = 60 * 60 * 1e3;
|
|
580
|
-
var sessionsWithContext = /* @__PURE__ */ new Map();
|
|
581
|
-
function hasSessionContext(sessionID) {
|
|
582
|
-
const ts = sessionsWithContext.get(sessionID);
|
|
583
|
-
if (ts === void 0) return false;
|
|
584
|
-
if (Date.now() - ts > SESSION_CONTEXT_TTL_MS) {
|
|
585
|
-
sessionsWithContext.delete(sessionID);
|
|
586
|
-
return false;
|
|
395
|
+
async function unmergedBranchesText(store) {
|
|
396
|
+
await ensureStore(store);
|
|
397
|
+
const branchResult = await runMemoir(["--json", "-s", store, "branch"], { cwd: store });
|
|
398
|
+
if (!branchResult.ok) {
|
|
399
|
+
return branchResult.error;
|
|
587
400
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
401
|
+
const data = JSON.parse(branchResult.stdout);
|
|
402
|
+
const branches = data?.branches ?? [];
|
|
403
|
+
const unmerged = [];
|
|
404
|
+
for (const branch of branches) {
|
|
405
|
+
if (branch === "main") continue;
|
|
406
|
+
const diffResult = await runMemoir(["-s", store, "diff", branch, "main", "--stat"], { cwd: store });
|
|
407
|
+
if (!diffResult.ok) {
|
|
408
|
+
debugLog("command.execute.before: memoir diff failed", diffResult.error);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (diffResult.stdout.trim()) {
|
|
412
|
+
unmerged.push(branch);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return unmerged.length > 0 ? `Unmerged branches:
|
|
416
|
+
${unmerged.join("\n")}` : "All branches are merged into main.";
|
|
592
417
|
}
|
|
593
|
-
function pruneStaleSessions() {
|
|
594
|
-
const now = Date.now();
|
|
595
|
-
for (const [id, ts] of sessionsWithContext) {
|
|
596
|
-
if (now - ts > SESSION_CONTEXT_TTL_MS) sessionsWithContext.delete(id);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
function registerCommands(config) {
|
|
600
|
-
config.command = config.command ?? {};
|
|
601
|
-
config.command["memoir:status"] = {
|
|
602
|
-
description: "Show Memoir status for the current OpenCode project",
|
|
603
|
-
template: "Show Memoir status for this OpenCode project."
|
|
604
|
-
};
|
|
605
|
-
config.command["memoir:ui"] = {
|
|
606
|
-
description: "Launch or reopen the Memoir web UI for this project store",
|
|
607
|
-
template: "Launch or reopen the Memoir UI for this project."
|
|
608
|
-
};
|
|
609
|
-
config.command["memoir:remember"] = {
|
|
610
|
-
description: "Save a durable fact, preference, rule, or decision to Memoir",
|
|
611
|
-
template: `Use Memoir to save this durable memory now.
|
|
612
|
-
|
|
613
|
-
USER REQUEST:
|
|
614
|
-
$ARGUMENTS
|
|
615
|
-
|
|
616
|
-
Extract the memory content, choose a semantic path if none is supplied, then call the memoir_remember tool. Never save secrets.`
|
|
617
|
-
};
|
|
618
|
-
config.command["memoir:recall"] = {
|
|
619
|
-
description: "Recall relevant facts from Memoir before answering",
|
|
620
|
-
template: `Recall relevant Memoir memories for this request.
|
|
621
|
-
|
|
622
|
-
USER REQUEST:
|
|
623
|
-
$ARGUMENTS
|
|
624
|
-
|
|
625
|
-
Use memoir_recall first. It checks default plus onboard namespaces unless a namespace is specified. Then call memoir_get with the matching namespace for exact values before answering.`
|
|
626
|
-
};
|
|
627
|
-
config.command["memoir:onboard"] = {
|
|
628
|
-
description: "Populate or refresh Memoir onboarding for this project",
|
|
629
|
-
template: `Populate or refresh Memoir onboarding for the CURRENT OpenCode project only.
|
|
630
|
-
|
|
631
|
-
USER REQUEST:
|
|
632
|
-
$ARGUMENTS
|
|
633
418
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
- Start studying from project documentation.
|
|
638
|
-
- Continue only based on what the tree and documentation show.
|
|
639
|
-
|
|
640
|
-
Memory rules:
|
|
641
|
-
- Record only verified facts from files/docs/code or explicit user statements.
|
|
642
|
-
- Do not write inferred user thoughts, intentions, preferences, or opinions.
|
|
643
|
-
- Do not use preferences.* paths unless the user explicitly stated a preference.
|
|
644
|
-
- If a fact is your interpretation, do not save it; report it as uncertain instead.
|
|
645
|
-
|
|
646
|
-
Then call memoir_remember with replace=true for durable onboarding facts. Use namespace codebase:onboard in git repositories and project:onboard outside git. Do not install or invoke separate skills/scripts.`
|
|
647
|
-
};
|
|
648
|
-
config.command["memoir:unmerged"] = {
|
|
649
|
-
description: "List memoir branches with changes not yet merged into main",
|
|
650
|
-
template: "List memoir branches that have diverged from main."
|
|
651
|
-
};
|
|
419
|
+
// src/recall-gate.ts
|
|
420
|
+
function isSecretSanitizationEnabled() {
|
|
421
|
+
return process.env.MEMOIR_SANITIZE_SECRETS !== "0";
|
|
652
422
|
}
|
|
423
|
+
var pendingRecall = /* @__PURE__ */ new Set();
|
|
424
|
+
var ACK_PATTERN = /^(ok|thanks|thank you|sounds good|got it|👍|🙏|perfect|great|cool|nice|awesome|understood|makes sense|agree|right|sure|yes|no|done|nvm|never mind|lgtm|looks good|proceed|continue|good|fine)\b/i;
|
|
425
|
+
var EXPLICIT_RECALL_PATTERN = /\b(memoir:recall|memoir:remember|memoir-recall|memoir-remember)\b|(\/recall|\/remember)\b/i;
|
|
426
|
+
var RECALL_TRIGGER_PATTERNS = [
|
|
427
|
+
// Action verbs and domain nouns
|
|
428
|
+
/\b(add|build|implement|refactor|redesign|design|create|write|set( |-)up|wire( |-)up|integrate|migrate|rewrite|extract|extend|plumb|hook( |-)up|ship|scaffold|optimize|fix|debug|review|architect|model|schema|API|service|feature|module|system|pipeline|workflow|make|move|replace|convert|swap|remove|clean( |-)up|transform|investigate|explore|figure( |-)out|plan|decide|choose|pick|compare|walk( |-)?me( |-)?through|take( |-)?a( |-)?stab|help( |-)?me|harness|hook|prompt|test)\b/i,
|
|
429
|
+
// Question starts (how/why/what/where/when/should/can/could/would/is it/are we/do I/does it)
|
|
430
|
+
/^(how|why|what|where|when|should|can|could|would|is it|are we|do I|does it)\b.*\?/im,
|
|
431
|
+
// Code blocks (triple backticks)
|
|
432
|
+
/```/,
|
|
433
|
+
// Code definitions
|
|
434
|
+
/\b(def|function|class|import|export)\s+/,
|
|
435
|
+
// Memoir/recall keywords
|
|
436
|
+
/\b(memoir|recall|remember|memory)\b/i,
|
|
437
|
+
// File extensions
|
|
438
|
+
/\b\w+\.(py|js|ts|tsx|scala|java|go|rs|rb|md|json|yaml|yml|toml|sh|bash|css|html|kt|swift|c|cpp|h|hpp)\b/i,
|
|
439
|
+
// File paths (slash-containing tokens)
|
|
440
|
+
/\w+\/\w+/
|
|
441
|
+
];
|
|
442
|
+
function isAcknowledgement(text) {
|
|
443
|
+
const trimmed = text.trim().toLowerCase();
|
|
444
|
+
const words = trimmed.split(/\s+/);
|
|
445
|
+
return words.length <= 5 && ACK_PATTERN.test(trimmed);
|
|
446
|
+
}
|
|
447
|
+
function shouldTriggerRecall(text) {
|
|
448
|
+
const trimmed = text.trim();
|
|
449
|
+
if (!trimmed) return false;
|
|
450
|
+
if (EXPLICIT_RECALL_PATTERN.test(trimmed)) return true;
|
|
451
|
+
if (trimmed.length < 10) return false;
|
|
452
|
+
if (isAcknowledgement(trimmed)) return false;
|
|
453
|
+
if (trimmed.length >= 40 && RECALL_TRIGGER_PATTERNS.some((p) => p.test(trimmed))) return true;
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
var DEFAULT_RECALL_NAMESPACES = ["default", "project:onboard", "codebase:onboard"];
|
|
457
|
+
|
|
458
|
+
// src/tools.ts
|
|
653
459
|
var memoirStatus = tool({
|
|
654
460
|
description: "Show Memoir status for the current OpenCode project store.",
|
|
655
461
|
args: {},
|
|
@@ -680,7 +486,9 @@ var memoirRemember = tool({
|
|
|
680
486
|
for (const p of paths) cliArgs.push("-p", p);
|
|
681
487
|
cliArgs.push("-n", args.namespace ?? "default");
|
|
682
488
|
if (args.replace) cliArgs.push("--replace");
|
|
683
|
-
|
|
489
|
+
const rememberResult = await runMemoir(cliArgs, { cwd: store });
|
|
490
|
+
if (!rememberResult.ok) return rememberResult.error;
|
|
491
|
+
return tryPrettyJson(rememberResult.stdout);
|
|
684
492
|
}
|
|
685
493
|
});
|
|
686
494
|
var memoirRecall = tool({
|
|
@@ -698,7 +506,9 @@ var memoirRecall = tool({
|
|
|
698
506
|
const namespaces = args.namespace ? [args.namespace] : args.namespaces && args.namespaces.length > 0 ? args.namespaces : DEFAULT_RECALL_NAMESPACES;
|
|
699
507
|
const sections = [];
|
|
700
508
|
for (const namespace of namespaces) {
|
|
701
|
-
const
|
|
509
|
+
const summarizeResult = await runMemoir(["--json", "-s", store, "summarize", "--depth", "3", "-n", namespace], { cwd: store });
|
|
510
|
+
if (!summarizeResult.ok) return summarizeResult.error;
|
|
511
|
+
const output = tryPrettyJson(summarizeResult.stdout);
|
|
702
512
|
sections.push(`## namespace: ${namespace}
|
|
703
513
|
${output}`);
|
|
704
514
|
}
|
|
@@ -725,59 +535,418 @@ var memoirGet = tool({
|
|
|
725
535
|
const store = deriveStorePath();
|
|
726
536
|
const storeError = await ensureStoreOrError(store);
|
|
727
537
|
if (storeError) return storeError;
|
|
728
|
-
|
|
538
|
+
const getResult = await runMemoir(["--json", "-s", store, "get", ...keys, "-n", args.namespace ?? "default"], { cwd: store });
|
|
539
|
+
if (!getResult.ok) return getResult.error;
|
|
540
|
+
return tryPrettyJson(getResult.stdout);
|
|
729
541
|
}
|
|
730
542
|
});
|
|
543
|
+
var memoirTools = {
|
|
544
|
+
memoir_status: memoirStatus,
|
|
545
|
+
memoir_remember: memoirRemember,
|
|
546
|
+
memoir_recall: memoirRecall,
|
|
547
|
+
memoir_get: memoirGet
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// src/commands.ts
|
|
551
|
+
function pushText(output, text) {
|
|
552
|
+
output.parts.length = 0;
|
|
553
|
+
output.parts.push({ type: "text", text });
|
|
554
|
+
}
|
|
555
|
+
function registerCommands(config) {
|
|
556
|
+
config.command = config.command ?? {};
|
|
557
|
+
config.command["memoir:status"] = {
|
|
558
|
+
description: "Show Memoir status for the current OpenCode project",
|
|
559
|
+
template: "Show Memoir status for this OpenCode project."
|
|
560
|
+
};
|
|
561
|
+
config.command["memoir:ui"] = {
|
|
562
|
+
description: "Launch or reopen the Memoir web UI for this project store",
|
|
563
|
+
template: "Launch or reopen the Memoir UI for this project."
|
|
564
|
+
};
|
|
565
|
+
config.command["memoir:remember"] = {
|
|
566
|
+
description: "Save a durable fact, preference, rule, or decision to Memoir",
|
|
567
|
+
template: `Use Memoir to save this durable memory now.
|
|
568
|
+
|
|
569
|
+
USER REQUEST:
|
|
570
|
+
$ARGUMENTS
|
|
571
|
+
|
|
572
|
+
Extract the memory content, choose a semantic path if none is supplied, then call the memoir_remember tool. Never save secrets.`
|
|
573
|
+
};
|
|
574
|
+
config.command["memoir:recall"] = {
|
|
575
|
+
description: "Recall relevant facts from Memoir before answering",
|
|
576
|
+
template: `Recall relevant Memoir memories for this request.
|
|
577
|
+
|
|
578
|
+
USER REQUEST:
|
|
579
|
+
$ARGUMENTS
|
|
580
|
+
|
|
581
|
+
Use memoir_recall first. It checks default plus onboard namespaces unless a namespace is specified. Then call memoir_get with the matching namespace for exact values before answering.`
|
|
582
|
+
};
|
|
583
|
+
config.command["memoir:onboard"] = {
|
|
584
|
+
description: "Populate or refresh Memoir onboarding for this project",
|
|
585
|
+
template: `Populate or refresh Memoir onboarding for the CURRENT OpenCode project only.
|
|
586
|
+
|
|
587
|
+
USER REQUEST:
|
|
588
|
+
$ARGUMENTS
|
|
589
|
+
|
|
590
|
+
Workflow:
|
|
591
|
+
- Stay inside the current project/worktree. Do not inspect parent directories.
|
|
592
|
+
- First obtain a project file tree to understand structure.
|
|
593
|
+
- Start studying from project documentation.
|
|
594
|
+
- Continue only based on what the tree and documentation show.
|
|
595
|
+
|
|
596
|
+
Memory rules:
|
|
597
|
+
- Record only verified facts from files/docs/code or explicit user statements.
|
|
598
|
+
- Do not write inferred user thoughts, intentions, preferences, or opinions.
|
|
599
|
+
- Do not use preferences.* paths unless the user explicitly stated a preference.
|
|
600
|
+
- If a fact is your interpretation, do not save it; report it as uncertain instead.
|
|
601
|
+
|
|
602
|
+
Then call memoir_remember with replace=true for durable onboarding facts. Use namespace codebase:onboard in git repositories and project:onboard outside git. Do not install or invoke separate skills/scripts.`
|
|
603
|
+
};
|
|
604
|
+
config.command["memoir:unmerged"] = {
|
|
605
|
+
description: "List memoir branches with changes not yet merged into main",
|
|
606
|
+
template: "List memoir branches that have diverged from main."
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async function handleCommandExecuteBefore(storeRoot, input, output) {
|
|
610
|
+
try {
|
|
611
|
+
if (input.command === "memoir:status") {
|
|
612
|
+
pushText(output, await statusJson(storeRoot));
|
|
613
|
+
}
|
|
614
|
+
if (input.command === "memoir:ui") {
|
|
615
|
+
pushText(output, await launchUi(storeRoot));
|
|
616
|
+
}
|
|
617
|
+
if (input.command === "memoir:remember") {
|
|
618
|
+
const args = input.arguments;
|
|
619
|
+
if (args && isSecretSanitizationEnabled() && SECRET_PATTERN.test(args)) {
|
|
620
|
+
pushText(output, "Memoir: cannot remember content that matches secret patterns. Please remove sensitive data and try again.");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (input.command === "memoir:unmerged") {
|
|
624
|
+
pushText(output, await unmergedBranchesText(storeRoot));
|
|
625
|
+
}
|
|
626
|
+
} catch (error) {
|
|
627
|
+
pushText(output, `Memoir command failed: ${errorMessage(error)}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/capture.ts
|
|
632
|
+
var _cliOverrides = null;
|
|
633
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch", "ApplyPatch", "MultiFileEdit"]);
|
|
634
|
+
function evictOldest(map, maxSize) {
|
|
635
|
+
if (map.size <= maxSize) return;
|
|
636
|
+
const firstKey = map.keys().next().value;
|
|
637
|
+
if (firstKey !== void 0) map.delete(firstKey);
|
|
638
|
+
}
|
|
639
|
+
var METRICS_CODE_MAX = 1e3;
|
|
640
|
+
var pendingEditsBySession = /* @__PURE__ */ new Map();
|
|
641
|
+
var toolMetricsBySession = /* @__PURE__ */ new Map();
|
|
642
|
+
var cachedBranchBySession = /* @__PURE__ */ new Map();
|
|
643
|
+
function getCachedBranch(sessionID) {
|
|
644
|
+
return cachedBranchBySession.get(sessionID) ?? "unknown";
|
|
645
|
+
}
|
|
646
|
+
function setCachedBranch(sessionID, branch) {
|
|
647
|
+
cachedBranchBySession.set(sessionID, branch);
|
|
648
|
+
evictOldest(cachedBranchBySession, 200);
|
|
649
|
+
}
|
|
650
|
+
function recordEdit(sessionID, edit) {
|
|
651
|
+
let edits = pendingEditsBySession.get(sessionID);
|
|
652
|
+
if (!edits) {
|
|
653
|
+
edits = [];
|
|
654
|
+
pendingEditsBySession.set(sessionID, edits);
|
|
655
|
+
evictOldest(pendingEditsBySession, 500);
|
|
656
|
+
}
|
|
657
|
+
edits.push(edit);
|
|
658
|
+
}
|
|
659
|
+
function recordToolMetrics(sessionID, tool2, metrics) {
|
|
660
|
+
let sessionMetrics = toolMetricsBySession.get(sessionID);
|
|
661
|
+
if (!sessionMetrics) {
|
|
662
|
+
sessionMetrics = /* @__PURE__ */ new Map();
|
|
663
|
+
toolMetricsBySession.set(sessionID, sessionMetrics);
|
|
664
|
+
evictOldest(toolMetricsBySession, 500);
|
|
665
|
+
}
|
|
666
|
+
const prev = sessionMetrics.get(tool2) ?? { calls: 0, errors: 0 };
|
|
667
|
+
sessionMetrics.set(tool2, {
|
|
668
|
+
calls: prev.calls + metrics.calls,
|
|
669
|
+
errors: prev.errors + metrics.errors
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function parseTurnMetrics(text) {
|
|
673
|
+
const result = /* @__PURE__ */ new Map();
|
|
674
|
+
if (!text) return result;
|
|
675
|
+
const parts = text.split("|");
|
|
676
|
+
for (const part of parts) {
|
|
677
|
+
const trimmed = part.trim();
|
|
678
|
+
if (!trimmed) continue;
|
|
679
|
+
const [tool2, callsStr, errorsStr] = trimmed.split(":");
|
|
680
|
+
if (!tool2) continue;
|
|
681
|
+
result.set(tool2, {
|
|
682
|
+
calls: parseInt(callsStr, 10) || 0,
|
|
683
|
+
errors: parseInt(errorsStr, 10) || 0
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
function serializeTurnMetrics(map) {
|
|
689
|
+
return [...map.entries()].map(([tool2, m]) => `${tool2}:${m.calls}:${m.errors}`).join(" | ");
|
|
690
|
+
}
|
|
691
|
+
var flushQueues = /* @__PURE__ */ new Map();
|
|
692
|
+
async function acquireFlushLock(store) {
|
|
693
|
+
const prev = flushQueues.get(store) ?? Promise.resolve();
|
|
694
|
+
let release;
|
|
695
|
+
const next = new Promise((resolve2) => {
|
|
696
|
+
release = resolve2;
|
|
697
|
+
});
|
|
698
|
+
flushQueues.set(store, next);
|
|
699
|
+
await prev;
|
|
700
|
+
return () => {
|
|
701
|
+
release();
|
|
702
|
+
if (flushQueues.get(store) === next) flushQueues.delete(store);
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async function flushCapture(store, branch, sessionID) {
|
|
706
|
+
const targets = sessionID ? [sessionID] : [...pendingEditsBySession.keys()];
|
|
707
|
+
if (targets.length === 0) return;
|
|
708
|
+
try {
|
|
709
|
+
store = store ?? deriveStorePath();
|
|
710
|
+
if (!store) return;
|
|
711
|
+
await ensureStore(store);
|
|
712
|
+
const releaseLock = await acquireFlushLock(store);
|
|
713
|
+
try {
|
|
714
|
+
const perSession = [];
|
|
715
|
+
const swappedSids = /* @__PURE__ */ new Map();
|
|
716
|
+
for (const sid of targets) {
|
|
717
|
+
const idx = perSession.length;
|
|
718
|
+
const edits = pendingEditsBySession.get(sid) ?? [];
|
|
719
|
+
pendingEditsBySession.set(sid, []);
|
|
720
|
+
const metrics = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
721
|
+
toolMetricsBySession.set(sid, /* @__PURE__ */ new Map());
|
|
722
|
+
const sb = branch ?? cachedBranchBySession.get(sid);
|
|
723
|
+
perSession.push({ edits, metrics, sessionBranch: sb ?? "unknown" });
|
|
724
|
+
swappedSids.set(sid, idx);
|
|
725
|
+
}
|
|
726
|
+
if (!perSession.some((p) => p.edits.length > 0 || p.metrics.size > 0)) return;
|
|
727
|
+
try {
|
|
728
|
+
for (const p of perSession) {
|
|
729
|
+
if (p.sessionBranch === "unknown" || !p.sessionBranch) {
|
|
730
|
+
p.sessionBranch = await (_cliOverrides?.getCurrentBranch ?? getCurrentBranch)(store);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const p of perSession) {
|
|
734
|
+
const { edits, metrics, sessionBranch: branchKey } = p;
|
|
735
|
+
if (edits.length === 0 && metrics.size === 0) continue;
|
|
736
|
+
const [prevCodeRaw, prevTurnRaw] = await Promise.all([
|
|
737
|
+
edits.length > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(store, `metrics.code.${branchKey}`) : Promise.resolve(""),
|
|
738
|
+
metrics.size > 0 ? (_cliOverrides?.readMemoirValue ?? readMemoirValue)(store, `metrics.turn.${branchKey}`) : Promise.resolve("")
|
|
739
|
+
]);
|
|
740
|
+
let codeWrite;
|
|
741
|
+
if (edits.length > 0) {
|
|
742
|
+
const files = [...new Set(edits.map((e) => e.filePath))];
|
|
743
|
+
const entry = {
|
|
744
|
+
timestamp: Date.now() / 1e3,
|
|
745
|
+
summary: `Changed ${edits.length} block(s) across ${files.length} file(s): ${files.join(", ")}`,
|
|
746
|
+
files
|
|
747
|
+
};
|
|
748
|
+
let acc = {
|
|
749
|
+
schema_version: 2,
|
|
750
|
+
entries: []
|
|
751
|
+
};
|
|
752
|
+
if (prevCodeRaw) {
|
|
753
|
+
try {
|
|
754
|
+
const parsed = JSON.parse(prevCodeRaw);
|
|
755
|
+
if (parsed?.entries && Array.isArray(parsed.entries)) {
|
|
756
|
+
acc = parsed;
|
|
757
|
+
if (acc.schema_version < 2) acc.schema_version = 2;
|
|
758
|
+
}
|
|
759
|
+
} catch (e) {
|
|
760
|
+
debugLog("flushCapture: failed to parse existing code metrics, starting fresh:", errorMessage(e));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
acc.entries.push(entry);
|
|
764
|
+
if (acc.entries.length > METRICS_CODE_MAX) acc.entries = acc.entries.slice(-METRICS_CODE_MAX);
|
|
765
|
+
codeWrite = (_cliOverrides?.runMemoir ?? runMemoir)(["-s", store, "remember", "--replace", JSON.stringify(acc), "-p", `metrics.code.${branchKey}`], { cwd: store });
|
|
766
|
+
}
|
|
767
|
+
let turnWrite;
|
|
768
|
+
if (metrics.size > 0) {
|
|
769
|
+
const existing = parseTurnMetrics(prevTurnRaw);
|
|
770
|
+
for (const [tool2, current] of metrics) {
|
|
771
|
+
const prev = existing.get(tool2) ?? { calls: 0, errors: 0 };
|
|
772
|
+
existing.set(tool2, { calls: prev.calls + current.calls, errors: prev.errors + current.errors });
|
|
773
|
+
}
|
|
774
|
+
turnWrite = (_cliOverrides?.runMemoir ?? runMemoir)(["-s", store, "remember", "--replace", serializeTurnMetrics(existing), "-p", `metrics.turn.${branchKey}`], { cwd: store });
|
|
775
|
+
}
|
|
776
|
+
const writes = [];
|
|
777
|
+
if (codeWrite) writes.push(codeWrite);
|
|
778
|
+
if (turnWrite) writes.push(turnWrite);
|
|
779
|
+
const results = await Promise.all(writes);
|
|
780
|
+
for (const result of results) {
|
|
781
|
+
if (!result.ok) {
|
|
782
|
+
throw new Error(`Memoir write failed: ${result.error}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
} catch (e) {
|
|
787
|
+
for (const [sid, idx] of swappedSids) {
|
|
788
|
+
const p = perSession[idx];
|
|
789
|
+
const currentEdits = pendingEditsBySession.get(sid) ?? [];
|
|
790
|
+
pendingEditsBySession.set(sid, [...p.edits, ...currentEdits]);
|
|
791
|
+
const merged = toolMetricsBySession.get(sid) ?? /* @__PURE__ */ new Map();
|
|
792
|
+
for (const [tool2, m] of p.metrics) {
|
|
793
|
+
const prev = merged.get(tool2) ?? { calls: 0, errors: 0 };
|
|
794
|
+
merged.set(tool2, { calls: prev.calls + m.calls, errors: prev.errors + m.errors });
|
|
795
|
+
}
|
|
796
|
+
toolMetricsBySession.set(sid, merged);
|
|
797
|
+
}
|
|
798
|
+
throw e;
|
|
799
|
+
}
|
|
800
|
+
} finally {
|
|
801
|
+
releaseLock();
|
|
802
|
+
}
|
|
803
|
+
} catch (e) {
|
|
804
|
+
debugLog("flushCapture: failed:", errorMessage(e));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/capture-hooks.ts
|
|
809
|
+
async function handleShellEnv(storeRoot, _input, output) {
|
|
810
|
+
try {
|
|
811
|
+
output.env.MEMOIR_STORE = storeRoot;
|
|
812
|
+
} catch (e) {
|
|
813
|
+
debugLog("shell.env: failed:", errorMessage(e));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function handleToolExecuteAfter(input, output) {
|
|
817
|
+
try {
|
|
818
|
+
const typedInput = input;
|
|
819
|
+
const typedOutput = output;
|
|
820
|
+
const sid = typedInput?.sessionID ?? "default";
|
|
821
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_METRICS !== "1") {
|
|
822
|
+
const calls = 1;
|
|
823
|
+
const errors = typedOutput?.metadata?.error || typedOutput?.output?.startsWith("Error:") ? 1 : 0;
|
|
824
|
+
recordToolMetrics(sid, typedInput?.tool ?? "", { calls, errors });
|
|
825
|
+
}
|
|
826
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_CODE_SUMMARY !== "1" && EDIT_TOOLS.has(typedInput?.tool ?? "")) {
|
|
827
|
+
const args = typedInput?.args ?? {};
|
|
828
|
+
const filePath = typeof args.filePath === "string" ? args.filePath : typeof args.path === "string" ? args.path : "";
|
|
829
|
+
if (filePath) {
|
|
830
|
+
recordEdit(sid, { tool: typedInput?.tool ?? "", filePath, snippet: "", timestamp: Date.now() });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
} catch (e) {
|
|
834
|
+
debugLog("tool.execute.after: failed:", errorMessage(e));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function handleChatMessage(storeRoot, input, output) {
|
|
838
|
+
try {
|
|
839
|
+
const typedInput = input;
|
|
840
|
+
const typedOutput = output;
|
|
841
|
+
const sid = typedInput?.sessionID ?? "default";
|
|
842
|
+
const text = (typedOutput?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
843
|
+
if (shouldTriggerRecall(text)) {
|
|
844
|
+
pendingRecall.add(sid);
|
|
845
|
+
}
|
|
846
|
+
const _cachedBranch = getCachedBranch(sid);
|
|
847
|
+
const prevBranch = _cachedBranch === "unknown" ? void 0 : _cachedBranch;
|
|
848
|
+
setCachedBranch(sid, await autoMatchMemoirBranch(storeRoot));
|
|
849
|
+
if (process.env.MEMOIR_NO_CAPTURE !== "1") {
|
|
850
|
+
await flushCapture(storeRoot, prevBranch, sid);
|
|
851
|
+
}
|
|
852
|
+
} catch (e) {
|
|
853
|
+
debugLog("chat.message: failed:", errorMessage(e));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async function flushAllCapture(storeRoot) {
|
|
857
|
+
await flushCapture(storeRoot);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/session-context.ts
|
|
861
|
+
var initContext = null;
|
|
862
|
+
var initContextFetched = false;
|
|
863
|
+
var MAX_TAXONOMY_RETRIES = 3;
|
|
864
|
+
var taxonomyFetchRetries = 0;
|
|
865
|
+
var SESSION_CONTEXT_TTL_MS = 60 * 60 * 1e3;
|
|
866
|
+
var sessionsWithContext = /* @__PURE__ */ new Map();
|
|
867
|
+
function hasSessionContext(sessionID) {
|
|
868
|
+
const ts = sessionsWithContext.get(sessionID);
|
|
869
|
+
if (ts === void 0) return false;
|
|
870
|
+
if (Date.now() - ts > SESSION_CONTEXT_TTL_MS) {
|
|
871
|
+
sessionsWithContext.delete(sessionID);
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
function markSessionContext(sessionID) {
|
|
877
|
+
sessionsWithContext.set(sessionID, Date.now());
|
|
878
|
+
}
|
|
879
|
+
function pruneStaleSessions() {
|
|
880
|
+
const now = Date.now();
|
|
881
|
+
for (const [id, ts] of sessionsWithContext) {
|
|
882
|
+
if (now - ts > SESSION_CONTEXT_TTL_MS) sessionsWithContext.delete(id);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function handleEvent(storeRoot, input) {
|
|
886
|
+
if (input.event.type === "session.created" && !initContextFetched) {
|
|
887
|
+
initContextFetched = true;
|
|
888
|
+
fetchTaxonomy(storeRoot).catch((e) => {
|
|
889
|
+
debugLog("event: taxonomy fetch failed, will retry on next session:", errorMessage(e));
|
|
890
|
+
taxonomyFetchRetries++;
|
|
891
|
+
if (taxonomyFetchRetries >= MAX_TAXONOMY_RETRIES) {
|
|
892
|
+
debugLog("memoir: taxonomy fetch failed after max retries, giving up");
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
initContextFetched = false;
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function fetchTaxonomy(storeRoot) {
|
|
900
|
+
const taxonomyResult = await runMemoir(
|
|
901
|
+
["--json", "-s", storeRoot, "summarize", "--depth", "3", "-n", "default"],
|
|
902
|
+
{ cwd: storeRoot }
|
|
903
|
+
);
|
|
904
|
+
if (!taxonomyResult.ok) {
|
|
905
|
+
throw new Error(taxonomyResult.error);
|
|
906
|
+
}
|
|
907
|
+
const pretty = tryPrettyJson(taxonomyResult.stdout);
|
|
908
|
+
initContext = `[memoir] Available taxonomy paths:
|
|
909
|
+
${pretty}`;
|
|
910
|
+
taxonomyFetchRetries = 0;
|
|
911
|
+
}
|
|
912
|
+
async function handleSystemTransform(input, output) {
|
|
913
|
+
try {
|
|
914
|
+
const typedInput = input;
|
|
915
|
+
const typedOutput = output;
|
|
916
|
+
if (typedInput?.sessionID && !hasSessionContext(typedInput.sessionID)) {
|
|
917
|
+
markSessionContext(typedInput.sessionID);
|
|
918
|
+
if (initContext) {
|
|
919
|
+
typedOutput?.system?.unshift(initContext);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (typedInput?.sessionID && pendingRecall.has(typedInput.sessionID)) {
|
|
923
|
+
pendingRecall.delete(typedInput.sessionID);
|
|
924
|
+
typedOutput?.system?.push(
|
|
925
|
+
"\n[memoir] The user may have relevant context in Memoir. Run memoir_recall to list available memories across default and onboard namespaces, then memoir_get with the matching namespace to fetch exact values."
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
} catch (e) {
|
|
929
|
+
debugLog("system.transform: failed:", errorMessage(e));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function clearSessionContext() {
|
|
933
|
+
pruneStaleSessions();
|
|
934
|
+
sessionsWithContext.clear();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/index.ts
|
|
731
938
|
var MemoirOpenCode = async (_input, rawOptions) => {
|
|
732
939
|
const opts = rawOptions ?? {};
|
|
733
940
|
if (opts.store) setPluginStoreOverride(opts.store);
|
|
734
941
|
const storeRoot = deriveStorePath();
|
|
735
942
|
return {
|
|
736
943
|
name: "memoir",
|
|
737
|
-
tool: {
|
|
738
|
-
memoir_status: memoirStatus,
|
|
739
|
-
memoir_remember: memoirRemember,
|
|
740
|
-
memoir_recall: memoirRecall,
|
|
741
|
-
memoir_get: memoirGet
|
|
742
|
-
},
|
|
944
|
+
tool: { ...memoirTools },
|
|
743
945
|
config: async (opencodeConfig) => {
|
|
744
946
|
registerCommands(opencodeConfig);
|
|
745
947
|
},
|
|
746
948
|
"command.execute.before": async (input, output) => {
|
|
747
|
-
|
|
748
|
-
if (input.command === "memoir:status") {
|
|
749
|
-
pushText(output, await statusJson(storeRoot));
|
|
750
|
-
}
|
|
751
|
-
if (input.command === "memoir:ui") {
|
|
752
|
-
pushText(output, await launchUi(storeRoot));
|
|
753
|
-
}
|
|
754
|
-
if (input.command === "memoir:unmerged") {
|
|
755
|
-
const store = storeRoot;
|
|
756
|
-
await ensureStore(store);
|
|
757
|
-
const raw = await runMemoir(["--json", "-s", store, "branch"], { cwd: store });
|
|
758
|
-
const data = JSON.parse(raw);
|
|
759
|
-
const branches = data?.branches ?? [];
|
|
760
|
-
const unmerged = [];
|
|
761
|
-
for (const branch of branches) {
|
|
762
|
-
if (branch === "main") continue;
|
|
763
|
-
const diffOut = await runMemoir(["-s", store, "diff", branch, "main", "--stat"], { cwd: store });
|
|
764
|
-
if (diffOut.startsWith("Memoir command failed")) {
|
|
765
|
-
debugLog("command.execute.before: memoir diff failed", diffOut);
|
|
766
|
-
continue;
|
|
767
|
-
}
|
|
768
|
-
if (diffOut.trim()) {
|
|
769
|
-
unmerged.push(branch);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
pushText(
|
|
773
|
-
output,
|
|
774
|
-
unmerged.length > 0 ? `Unmerged branches:
|
|
775
|
-
${unmerged.join("\n")}` : "All branches are merged into main."
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
} catch (error) {
|
|
779
|
-
pushText(output, `Memoir command failed: ${errorMessage(error)}`);
|
|
780
|
-
}
|
|
949
|
+
await handleCommandExecuteBefore(storeRoot, input, output);
|
|
781
950
|
},
|
|
782
951
|
/**
|
|
783
952
|
* Inject MEMOIR_STORE into every shell command's environment so any memoir
|
|
@@ -786,11 +955,7 @@ ${unmerged.join("\n")}` : "All branches are merged into main."
|
|
|
786
955
|
* on every shell command).
|
|
787
956
|
*/
|
|
788
957
|
"shell.env": async (_input2, output) => {
|
|
789
|
-
|
|
790
|
-
output.env.MEMOIR_STORE = storeRoot;
|
|
791
|
-
} catch (e) {
|
|
792
|
-
debugLog("shell.env: failed:", errorMessage(e));
|
|
793
|
-
}
|
|
958
|
+
await handleShellEnv(storeRoot, _input2, output);
|
|
794
959
|
},
|
|
795
960
|
/**
|
|
796
961
|
* Observe every tool execution for metrics and code-change tracking.
|
|
@@ -798,22 +963,7 @@ ${unmerged.join("\n")}` : "All branches are merged into main."
|
|
|
798
963
|
* Never modifies the tool output.
|
|
799
964
|
*/
|
|
800
965
|
"tool.execute.after": async (input, output) => {
|
|
801
|
-
|
|
802
|
-
const sid = input.sessionID ?? "default";
|
|
803
|
-
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_METRICS !== "1") {
|
|
804
|
-
const calls = 1;
|
|
805
|
-
const errors = output.metadata?.error || output.output?.startsWith("Error:") ? 1 : 0;
|
|
806
|
-
recordToolMetrics(sid, input.tool, { calls, errors });
|
|
807
|
-
}
|
|
808
|
-
if (process.env.MEMOIR_NO_CAPTURE !== "1" && process.env.MEMOIR_NO_CODE_SUMMARY !== "1" && EDIT_TOOLS.has(input.tool)) {
|
|
809
|
-
const filePath = typeof input.args?.filePath === "string" ? input.args.filePath : typeof input.args?.path === "string" ? input.args.path : "";
|
|
810
|
-
if (filePath) {
|
|
811
|
-
recordEdit(sid, { tool: input.tool, filePath, snippet: "", timestamp: Date.now() });
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
} catch (e) {
|
|
815
|
-
debugLog("tool.execute.after: failed:", errorMessage(e));
|
|
816
|
-
}
|
|
966
|
+
await handleToolExecuteAfter(input, output);
|
|
817
967
|
},
|
|
818
968
|
/**
|
|
819
969
|
* Fires on every incoming user message.
|
|
@@ -827,50 +977,15 @@ ${unmerged.join("\n")}` : "All branches are merged into main."
|
|
|
827
977
|
* Steps 1–2 are skipped when MEMOIR_NO_CAPTURE=1.
|
|
828
978
|
*/
|
|
829
979
|
"chat.message": async (input, output) => {
|
|
830
|
-
|
|
831
|
-
const sid = input.sessionID ?? "default";
|
|
832
|
-
const text = output.parts.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
833
|
-
if (shouldTriggerRecall(text)) {
|
|
834
|
-
pendingRecall.add(sid);
|
|
835
|
-
}
|
|
836
|
-
const _cachedBranch = getCachedBranch(sid);
|
|
837
|
-
const prevBranch = _cachedBranch === "unknown" ? void 0 : _cachedBranch;
|
|
838
|
-
setCachedBranch(sid, await autoMatchMemoirBranch(storeRoot));
|
|
839
|
-
if (process.env.MEMOIR_NO_CAPTURE !== "1") {
|
|
840
|
-
await flushCapture(storeRoot, prevBranch, sid);
|
|
841
|
-
}
|
|
842
|
-
} catch (e) {
|
|
843
|
-
debugLog("chat.message: failed:", errorMessage(e));
|
|
844
|
-
}
|
|
980
|
+
await handleChatMessage(storeRoot, input, output);
|
|
845
981
|
},
|
|
846
982
|
/**
|
|
847
983
|
* Fires on SDK events. On session.created, kick off a background fetch of
|
|
848
984
|
* the taxonomy overview so it's ready before the first LLM call.
|
|
849
985
|
* Mirrors Claude Code's SessionStart context injection.
|
|
850
986
|
*/
|
|
851
|
-
event: async (
|
|
852
|
-
|
|
853
|
-
initContextFetched = true;
|
|
854
|
-
(async () => {
|
|
855
|
-
try {
|
|
856
|
-
const taxonomy = await runMemoir(
|
|
857
|
-
["--json", "-s", storeRoot, "summarize", "--depth", "3", "-n", "default"],
|
|
858
|
-
{ cwd: storeRoot }
|
|
859
|
-
);
|
|
860
|
-
if (taxonomy.startsWith("Memoir command failed")) {
|
|
861
|
-
throw new Error(taxonomy);
|
|
862
|
-
}
|
|
863
|
-
const pretty = tryPrettyJson(taxonomy);
|
|
864
|
-
initContext = `[memoir] Available taxonomy paths:
|
|
865
|
-
${pretty}`;
|
|
866
|
-
} catch (e) {
|
|
867
|
-
debugLog("event: taxonomy fetch failed, will retry on next session:", errorMessage(e));
|
|
868
|
-
initContextFetched = false;
|
|
869
|
-
}
|
|
870
|
-
})().catch(() => {
|
|
871
|
-
initContextFetched = false;
|
|
872
|
-
});
|
|
873
|
-
}
|
|
987
|
+
event: async (input) => {
|
|
988
|
+
await handleEvent(storeRoot, input);
|
|
874
989
|
},
|
|
875
990
|
/**
|
|
876
991
|
* Fires before every LLM call.
|
|
@@ -880,22 +995,7 @@ ${pretty}`;
|
|
|
880
995
|
* telling the model to use memoir tools (one-shot per trigger).
|
|
881
996
|
*/
|
|
882
997
|
"experimental.chat.system.transform": async (input, output) => {
|
|
883
|
-
|
|
884
|
-
if (input.sessionID && !hasSessionContext(input.sessionID)) {
|
|
885
|
-
markSessionContext(input.sessionID);
|
|
886
|
-
if (initContext) {
|
|
887
|
-
output.system.unshift(initContext);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
if (input.sessionID && pendingRecall.has(input.sessionID)) {
|
|
891
|
-
pendingRecall.delete(input.sessionID);
|
|
892
|
-
output.system.push(
|
|
893
|
-
"\n[memoir] The user may have relevant context in Memoir. Run memoir_recall to list available memories across default and onboard namespaces, then memoir_get with the matching namespace to fetch exact values."
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
} catch (e) {
|
|
897
|
-
debugLog("system.transform: failed:", errorMessage(e));
|
|
898
|
-
}
|
|
998
|
+
await handleSystemTransform(input, output);
|
|
899
999
|
},
|
|
900
1000
|
/**
|
|
901
1001
|
* Fires when the plugin is shut down. Flushes any remaining code changes
|
|
@@ -903,11 +1003,12 @@ ${pretty}`;
|
|
|
903
1003
|
*/
|
|
904
1004
|
dispose: async () => {
|
|
905
1005
|
try {
|
|
906
|
-
await
|
|
907
|
-
|
|
908
|
-
sessionsWithContext.clear();
|
|
1006
|
+
await flushAllCapture(storeRoot);
|
|
1007
|
+
clearSessionContext();
|
|
909
1008
|
} catch (e) {
|
|
910
|
-
|
|
1009
|
+
const msg = `memoir: dispose error: ${errorMessage(e)}`;
|
|
1010
|
+
debugLog(msg);
|
|
1011
|
+
console.error(msg);
|
|
911
1012
|
}
|
|
912
1013
|
}
|
|
913
1014
|
};
|