@wrongstack/core 0.87.0 → 0.89.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/dist/{agent-bridge-C9P_HPez.d.ts → agent-bridge-DbVe1dHT.d.ts} +1 -1
- package/dist/{agent-subagent-runner-2Aq0jOSj.d.ts → agent-subagent-runner-C6eqID1t.d.ts} +477 -476
- package/dist/{events-DnRqXaZ3.d.ts → brain-s9DyD_Vf.d.ts} +185 -92
- package/dist/{compactor-CJq7LQev.d.ts → compactor-DXLxLcmU.d.ts} +2 -2
- package/dist/{config-_DZ7dN-T.d.ts → config-84VaZpH6.d.ts} +45 -38
- package/dist/{context-ToHAp4-U.d.ts → context-CNRYfhUv.d.ts} +1 -1
- package/dist/coordination/index.d.ts +16 -17
- package/dist/coordination/index.js +208 -93
- package/dist/coordination/index.js.map +1 -1
- package/dist/{default-config-DEXI4jsl.d.ts → default-config-CXsDvOmP.d.ts} +3 -1
- package/dist/defaults/index.d.ts +32 -34
- package/dist/defaults/index.js +744 -274
- package/dist/defaults/index.js.map +1 -1
- package/dist/{director-state-CgIc30qi.d.ts → director-state-BfeCUbmk.d.ts} +1 -1
- package/dist/execution/index.d.ts +18 -19
- package/dist/execution/index.js +79 -64
- package/dist/execution/index.js.map +1 -1
- package/dist/extension/index.d.ts +9 -10
- package/dist/extension/index.js +8 -6
- package/dist/extension/index.js.map +1 -1
- package/dist/{index-BNOLadHw.d.ts → index-CqrroYcU.d.ts} +13 -15
- package/dist/{index-N0_c4bHQ.d.ts → index-D3Nax3YU.d.ts} +11 -10
- package/dist/index.d.ts +191 -178
- package/dist/index.js +1419 -526
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +9 -9
- package/dist/infrastructure/index.js +16 -5
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +14 -16
- package/dist/kernel/index.js +17 -0
- package/dist/kernel/index.js.map +1 -1
- package/dist/{logger-B72yyPc6.d.ts → logger-B63L5bTg.d.ts} +1 -1
- package/dist/{logger-C_27pj9i.d.ts → logger-B9J5puGM.d.ts} +1 -1
- package/dist/{mcp-servers-Dck3T85_.d.ts → mcp-servers-D3E5ixAR.d.ts} +3 -3
- package/dist/{mode-CHo2XtHs.d.ts → mode-ARA3HrkY.d.ts} +1 -1
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +8 -6
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-Be3osGt5.d.ts → models-registry-B6_KfS65.d.ts} +1 -1
- package/dist/{models-registry-Boz639EI.d.ts → models-registry-DU64QxQa.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-DllpCVkF.d.ts → multi-agent-coordinator-DHNrjPaA.d.ts} +2 -2
- package/dist/{null-fleet-bus-BY0AN-sr.d.ts → null-fleet-bus-BzzciAc4.d.ts} +39 -32
- package/dist/observability/index.d.ts +3 -3
- package/dist/{observability-CoSNZdhX.d.ts → observability-D-HZN_mF.d.ts} +1 -1
- package/dist/{parallel-eternal-engine-D402RASp.d.ts → parallel-eternal-engine-CTpcrb9O.d.ts} +34 -33
- package/dist/{path-resolver-UPFTsDyD.d.ts → path-resolver-B7hgMAVe.d.ts} +3 -3
- package/dist/{permission-14CChMmO.d.ts → permission-BDv7z0mk.d.ts} +7 -2
- package/dist/{permission-policy-gW5htOo1.d.ts → permission-policy-dF74EpDp.d.ts} +2 -2
- package/dist/{system-prompt-C0rLCeyn.d.ts → pipeline-C1Ad9OjI.d.ts} +73 -73
- package/dist/{plan-templates-DRvPgkfZ.d.ts → plan-templates-CNphsz0n.d.ts} +79 -11
- package/dist/{provider-runner-COAJM8tC.d.ts → provider-runner-cqvlB_oS.d.ts} +5 -5
- package/dist/{retry-policy-DSu6O6rD.d.ts → retry-policy-BcmuT_V0.d.ts} +2 -2
- package/dist/sdd/index.d.ts +12 -33
- package/dist/sdd/index.js +110 -50
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-scrubber-yGBFQYju.d.ts → secret-vault-DrOhc2i5.d.ts} +7 -7
- package/dist/security/index.d.ts +4 -5
- package/dist/security/index.js +22 -4
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-11-fm95U.d.ts → selector-C7wcdqMA.d.ts} +1 -1
- package/dist/{session-event-bridge-D0u-x576.d.ts → session-event-bridge-IVzs2GpE.d.ts} +1 -1
- package/dist/{session-reader-BQU-toaN.d.ts → session-reader-DDz1Ek4V.d.ts} +2 -2
- package/dist/{skill-BJeF2DwY.d.ts → skill-BJxzIyyN.d.ts} +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.js +3 -9
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +134 -14
- package/dist/storage/index.js +982 -248
- package/dist/storage/index.js.map +1 -1
- package/dist/{task-graph-CikNdRTG.d.ts → task-graph-DJBqO0EY.d.ts} +1 -1
- package/dist/types/index.d.ts +26 -28
- package/dist/types/index.js +62 -37
- package/dist/types/index.js.map +1 -1
- package/dist/utils/expect-defined.d.ts +7 -0
- package/dist/utils/expect-defined.js +11 -0
- package/dist/utils/expect-defined.js.map +1 -0
- package/dist/utils/index.d.ts +11 -20
- package/dist/utils/index.js +61 -43
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-BQMvEllz.d.ts → wstack-paths-_lqjzErq.d.ts} +1 -1
- package/package.json +5 -1
- package/dist/memory-CEXuo7sz.d.ts +0 -16
- package/dist/secret-scrubber-3MHDDAtm.d.ts +0 -6
package/dist/storage/index.js
CHANGED
|
@@ -2,13 +2,15 @@ import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
|
2
2
|
import * as fsp from 'fs/promises';
|
|
3
3
|
import * as path2 from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
|
+
import { hostname } from 'os';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
// src/utils/expect-defined.ts
|
|
8
|
+
function expectDefined(value, label) {
|
|
9
|
+
if (value === null || value === void 0) {
|
|
10
|
+
throw new Error("Expected value to be defined");
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
12
14
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
13
15
|
const dir = path2.dirname(targetPath);
|
|
14
16
|
await fsp.mkdir(dir, { recursive: true });
|
|
@@ -50,6 +52,49 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
50
52
|
async function ensureDir(dir) {
|
|
51
53
|
await fsp.mkdir(dir, { recursive: true });
|
|
52
54
|
}
|
|
55
|
+
async function withFileLock(targetPath, fn, opts = {}) {
|
|
56
|
+
const dir = path2.dirname(targetPath);
|
|
57
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
58
|
+
const lockPath = path2.join(dir, `.${path2.basename(targetPath)}.lock`);
|
|
59
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
60
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
61
|
+
const started = Date.now();
|
|
62
|
+
let handle;
|
|
63
|
+
for (; ; ) {
|
|
64
|
+
try {
|
|
65
|
+
handle = await fsp.open(lockPath, "wx");
|
|
66
|
+
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
67
|
+
break;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err.code !== "EEXIST") throw err;
|
|
70
|
+
try {
|
|
71
|
+
const stat5 = await fsp.stat(lockPath);
|
|
72
|
+
if (Date.now() - stat5.mtimeMs > staleMs) {
|
|
73
|
+
await fsp.unlink(lockPath);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (Date.now() - started >= timeoutMs) {
|
|
80
|
+
throw new Error(`Timed out waiting for file lock: ${targetPath}`);
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve4) => setTimeout(resolve4, 25));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return await fn();
|
|
87
|
+
} finally {
|
|
88
|
+
try {
|
|
89
|
+
await handle?.close();
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await fsp.unlink(lockPath);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
53
98
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
54
99
|
async function renameWithRetry(from, to) {
|
|
55
100
|
if (process.platform !== "win32") {
|
|
@@ -75,12 +120,6 @@ async function renameWithRetry(from, to) {
|
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
// src/utils/message-invariants.ts
|
|
78
|
-
function expectDefined(value) {
|
|
79
|
-
if (value === null || value === void 0) {
|
|
80
|
-
throw new Error("Expected value to be defined");
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
123
|
function repairToolUseAdjacency(messages) {
|
|
85
124
|
const removedToolUses = [];
|
|
86
125
|
const removedToolResults = [];
|
|
@@ -173,12 +212,6 @@ function isEmptyMessage(msg) {
|
|
|
173
212
|
}
|
|
174
213
|
|
|
175
214
|
// src/storage/session-store.ts
|
|
176
|
-
function expectDefined2(value) {
|
|
177
|
-
if (value === null || value === void 0) {
|
|
178
|
-
throw new Error("Expected value to be defined");
|
|
179
|
-
}
|
|
180
|
-
return value;
|
|
181
|
-
}
|
|
182
215
|
function sanitizeModel(model) {
|
|
183
216
|
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
184
217
|
}
|
|
@@ -889,7 +922,7 @@ var FileSessionWriter = class {
|
|
|
889
922
|
let targetCheckpointLine = -1;
|
|
890
923
|
let afterTarget = false;
|
|
891
924
|
for (let i = 0; i < lines.length; i++) {
|
|
892
|
-
const line =
|
|
925
|
+
const line = expectDefined(lines[i]);
|
|
893
926
|
if (!line.trim()) continue;
|
|
894
927
|
let event;
|
|
895
928
|
try {
|
|
@@ -1178,9 +1211,235 @@ function mergeAdjacentText(blocks) {
|
|
|
1178
1211
|
}
|
|
1179
1212
|
return out;
|
|
1180
1213
|
}
|
|
1214
|
+
|
|
1215
|
+
// src/types/memory.ts
|
|
1216
|
+
var MEMORY_TYPE_LABELS = {
|
|
1217
|
+
fact: "Fact",
|
|
1218
|
+
decision: "Decision",
|
|
1219
|
+
convention: "Convention",
|
|
1220
|
+
preference: "Preference",
|
|
1221
|
+
reference: "Reference",
|
|
1222
|
+
anti_pattern: "Anti-pattern"
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// src/storage/memory-backend.ts
|
|
1226
|
+
var TYPE_PRIORITY_RE = /^\[(\w+)\|(\w+)\]\s+/;
|
|
1227
|
+
var TAG_RE = /#([\w-]+)/g;
|
|
1228
|
+
function formatMetadata(entry) {
|
|
1229
|
+
const parts = [];
|
|
1230
|
+
if (entry.type && entry.priority) {
|
|
1231
|
+
parts.push(`[${entry.type}|${entry.priority}]`);
|
|
1232
|
+
} else if (entry.type) {
|
|
1233
|
+
parts.push(`[${entry.type}]`);
|
|
1234
|
+
} else if (entry.priority) {
|
|
1235
|
+
parts.push(`[${entry.priority}]`);
|
|
1236
|
+
}
|
|
1237
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
1238
|
+
parts.push(entry.tags.map((t) => `#${t}`).join(" "));
|
|
1239
|
+
}
|
|
1240
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
1241
|
+
}
|
|
1242
|
+
function lineToEntry(line, scope) {
|
|
1243
|
+
const trimmed = line.trim();
|
|
1244
|
+
if (!trimmed.startsWith("- [")) return null;
|
|
1245
|
+
const tsMatch = trimmed.match(/^-\s*\[([^\]]+)\]/);
|
|
1246
|
+
if (!tsMatch) return null;
|
|
1247
|
+
const ts = tsMatch[1] ?? "";
|
|
1248
|
+
let rest = trimmed.slice(tsMatch[0].length).trim();
|
|
1249
|
+
let type;
|
|
1250
|
+
let priority;
|
|
1251
|
+
const tpMatch = rest.match(TYPE_PRIORITY_RE);
|
|
1252
|
+
if (tpMatch) {
|
|
1253
|
+
const a = tpMatch[1] ?? "";
|
|
1254
|
+
const b = tpMatch[2] ?? "";
|
|
1255
|
+
if (isMemoryType(a)) {
|
|
1256
|
+
type = a;
|
|
1257
|
+
priority = isPriority(b) ? b : void 0;
|
|
1258
|
+
} else if (isPriority(a)) {
|
|
1259
|
+
priority = a;
|
|
1260
|
+
}
|
|
1261
|
+
rest = rest.slice(tpMatch[0].length).trim();
|
|
1262
|
+
}
|
|
1263
|
+
const idMatch = rest.match(/^mem_\d+_\w+\s+/);
|
|
1264
|
+
let text;
|
|
1265
|
+
if (idMatch) {
|
|
1266
|
+
text = rest.slice(idMatch[0].length).trim();
|
|
1267
|
+
} else {
|
|
1268
|
+
text = rest.trim();
|
|
1269
|
+
}
|
|
1270
|
+
const tags = [];
|
|
1271
|
+
let tagMatch;
|
|
1272
|
+
TAG_RE.lastIndex = 0;
|
|
1273
|
+
while ((tagMatch = TAG_RE.exec(text)) !== null) {
|
|
1274
|
+
tags.push(tagMatch[1] ?? "");
|
|
1275
|
+
}
|
|
1276
|
+
const cleanText = text.replace(TAG_RE, "").replace(/\s{2,}/g, " ").trim();
|
|
1277
|
+
if (!cleanText) return null;
|
|
1278
|
+
return {
|
|
1279
|
+
scope,
|
|
1280
|
+
text: cleanText,
|
|
1281
|
+
ts,
|
|
1282
|
+
type,
|
|
1283
|
+
priority,
|
|
1284
|
+
tags: tags.length > 0 ? tags : void 0
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function isMemoryType(s) {
|
|
1288
|
+
return s in MEMORY_TYPE_LABELS;
|
|
1289
|
+
}
|
|
1290
|
+
function isPriority(s) {
|
|
1291
|
+
return s === "critical" || s === "high" || s === "medium" || s === "low";
|
|
1292
|
+
}
|
|
1293
|
+
var FileMemoryBackend = class {
|
|
1294
|
+
kind = "file";
|
|
1295
|
+
files;
|
|
1296
|
+
constructor(opts) {
|
|
1297
|
+
this.files = {
|
|
1298
|
+
"project-agents": opts.paths.inProjectAgentsFile,
|
|
1299
|
+
"project-memory": opts.paths.projectMemory,
|
|
1300
|
+
"user-memory": opts.paths.globalMemory
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
resolveFile(filePath, scope) {
|
|
1304
|
+
return filePath || this.files[scope];
|
|
1305
|
+
}
|
|
1306
|
+
async remember(scope, entry, filePath) {
|
|
1307
|
+
const file = this.resolveFile(filePath, scope);
|
|
1308
|
+
await ensureDir(path2.dirname(file));
|
|
1309
|
+
let existing = "";
|
|
1310
|
+
try {
|
|
1311
|
+
existing = await fsp.readFile(file, "utf8");
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
const id = `mem_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
1315
|
+
const meta = formatMetadata(entry);
|
|
1316
|
+
const line = `
|
|
1317
|
+
- [${entry.ts}] ${id}${meta} ${entry.text.replace(/\n/g, " ")}
|
|
1318
|
+
`;
|
|
1319
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + line : `# WrongStack Memory
|
|
1320
|
+
${line}`;
|
|
1321
|
+
await atomicWrite(file, next);
|
|
1322
|
+
}
|
|
1323
|
+
async forget(scope, query, filePath) {
|
|
1324
|
+
const file = this.resolveFile(filePath, scope);
|
|
1325
|
+
let existing;
|
|
1326
|
+
try {
|
|
1327
|
+
existing = await fsp.readFile(file, "utf8");
|
|
1328
|
+
} catch {
|
|
1329
|
+
return 0;
|
|
1330
|
+
}
|
|
1331
|
+
const needle = query.toLowerCase();
|
|
1332
|
+
const idMatcher = /mem_\d+_\w+/;
|
|
1333
|
+
let removed = 0;
|
|
1334
|
+
const lines = existing.split("\n").filter((line) => {
|
|
1335
|
+
const trimmed = line.trim();
|
|
1336
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
1337
|
+
if (idMatcher.test(query)) {
|
|
1338
|
+
const entryIdMatch = /mem_\d+_\w+/.exec(trimmed);
|
|
1339
|
+
if (entryIdMatch && entryIdMatch[0] === query) {
|
|
1340
|
+
removed++;
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (trimmed.toLowerCase().includes(needle)) {
|
|
1345
|
+
removed++;
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
return true;
|
|
1349
|
+
});
|
|
1350
|
+
if (removed > 0) {
|
|
1351
|
+
if (lines.length === 0 || lines.length === 1 && !lines[0]?.trim()) {
|
|
1352
|
+
await atomicWrite(file, "");
|
|
1353
|
+
} else {
|
|
1354
|
+
await atomicWrite(file, lines.join("\n"));
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return removed;
|
|
1358
|
+
}
|
|
1359
|
+
async readAll(scope, filePath) {
|
|
1360
|
+
const file = this.resolveFile(filePath, scope);
|
|
1361
|
+
try {
|
|
1362
|
+
return await fsp.readFile(file, "utf8");
|
|
1363
|
+
} catch {
|
|
1364
|
+
return "";
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async list(scope, filePath, limit) {
|
|
1368
|
+
const raw = await this.readAll(scope, filePath);
|
|
1369
|
+
if (!raw.trim()) return [];
|
|
1370
|
+
const entries = parseEntries(raw, scope);
|
|
1371
|
+
return limit ? entries.slice(0, limit) : entries;
|
|
1372
|
+
}
|
|
1373
|
+
async search(scope, query, filePath, limit) {
|
|
1374
|
+
const entries = await this.list(scope, filePath);
|
|
1375
|
+
const needle = query.toLowerCase().split(/\s+/);
|
|
1376
|
+
const scored = entries.map((e) => {
|
|
1377
|
+
const words = e.text.toLowerCase().split(/\s+/);
|
|
1378
|
+
let score = 0;
|
|
1379
|
+
for (const n of needle) {
|
|
1380
|
+
if (words.some((w) => w.includes(n))) score += 1;
|
|
1381
|
+
if (e.tags?.some((t) => t.toLowerCase().includes(n))) score += 2;
|
|
1382
|
+
}
|
|
1383
|
+
return { entry: e, score };
|
|
1384
|
+
});
|
|
1385
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1386
|
+
const matched = scored.filter((s) => s.score > 0).map((s) => s.entry);
|
|
1387
|
+
return limit ? matched.slice(0, limit) : matched;
|
|
1388
|
+
}
|
|
1389
|
+
async clear(scope, filePath) {
|
|
1390
|
+
const file = this.resolveFile(filePath, scope);
|
|
1391
|
+
await atomicWrite(file, "");
|
|
1392
|
+
}
|
|
1393
|
+
async consolidate(scope, filePath) {
|
|
1394
|
+
const file = this.resolveFile(filePath, scope);
|
|
1395
|
+
let existing;
|
|
1396
|
+
try {
|
|
1397
|
+
existing = await fsp.readFile(file, "utf8");
|
|
1398
|
+
} catch {
|
|
1399
|
+
return 0;
|
|
1400
|
+
}
|
|
1401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1402
|
+
let removed = 0;
|
|
1403
|
+
const lines = existing.split("\n").filter((line) => {
|
|
1404
|
+
const trimmed = line.trim();
|
|
1405
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
1406
|
+
const norm = trimmed.replace(/\[[^\]]+\]/g, "").replace(/\bmem_\d+_\w+\s*/, "").replace(/#[\w-]+/g, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1407
|
+
if (seen.has(norm)) {
|
|
1408
|
+
removed++;
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
seen.add(norm);
|
|
1412
|
+
return true;
|
|
1413
|
+
});
|
|
1414
|
+
const next = lines.join("\n");
|
|
1415
|
+
const backup = `${file}.bak.${Date.now()}`;
|
|
1416
|
+
try {
|
|
1417
|
+
await fsp.copyFile(file, backup);
|
|
1418
|
+
} catch {
|
|
1419
|
+
}
|
|
1420
|
+
try {
|
|
1421
|
+
await atomicWrite(file, next);
|
|
1422
|
+
} catch {
|
|
1423
|
+
return 0;
|
|
1424
|
+
}
|
|
1425
|
+
return removed;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
function parseEntries(raw, scope = "project-memory") {
|
|
1429
|
+
const entries = [];
|
|
1430
|
+
for (const line of raw.split("\n")) {
|
|
1431
|
+
const entry = lineToEntry(line, scope);
|
|
1432
|
+
if (entry) entries.push(entry);
|
|
1433
|
+
}
|
|
1434
|
+
return entries.reverse();
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// src/storage/memory-store.ts
|
|
1181
1438
|
var MAX_BYTES_TOTAL = 32e3;
|
|
1182
1439
|
var DefaultMemoryStore = class {
|
|
1183
1440
|
files;
|
|
1441
|
+
events;
|
|
1442
|
+
backend;
|
|
1184
1443
|
/**
|
|
1185
1444
|
* Per-scope serialization queue. `remember` / `forget` / `consolidate` /
|
|
1186
1445
|
* `clear` are read-modify-write against a single file; without a lock,
|
|
@@ -1197,12 +1456,29 @@ var DefaultMemoryStore = class {
|
|
|
1197
1456
|
writeChain = /* @__PURE__ */ new Map();
|
|
1198
1457
|
/** Last write error per scope — surfaced as warnings on the next readAll(). */
|
|
1199
1458
|
writeErrors = /* @__PURE__ */ new Map();
|
|
1459
|
+
/**
|
|
1460
|
+
* When the global root is a temporary directory (opencode, CI sandboxes),
|
|
1461
|
+
* memory files are also mirrored to the project tree so they survive
|
|
1462
|
+
* session cleanup. The primary path stays in the temp root for isolation;
|
|
1463
|
+
* this backup ensures memory is never lost.
|
|
1464
|
+
*/
|
|
1465
|
+
persistBackup;
|
|
1466
|
+
backupDir;
|
|
1200
1467
|
constructor(opts) {
|
|
1201
1468
|
this.files = {
|
|
1202
1469
|
"project-agents": opts.paths.inProjectAgentsFile,
|
|
1203
1470
|
"project-memory": opts.paths.projectMemory,
|
|
1204
1471
|
"user-memory": opts.paths.globalMemory
|
|
1205
1472
|
};
|
|
1473
|
+
this.events = opts.events;
|
|
1474
|
+
this.backend = opts.backend ?? new FileMemoryBackend({ paths: opts.paths });
|
|
1475
|
+
const root = opts.paths.globalRoot.toLowerCase();
|
|
1476
|
+
this.persistBackup = /[/\\](tmp|temp|cache)[/\\]/.test(root);
|
|
1477
|
+
this.backupDir = this.persistBackup ? opts.paths.inProjectAgentsFile.replace(/AGENTS\.md$/, "memory-persist") : "";
|
|
1478
|
+
}
|
|
1479
|
+
/** Expose the backend for plugin introspection and advanced queries. */
|
|
1480
|
+
getBackend() {
|
|
1481
|
+
return this.backend;
|
|
1206
1482
|
}
|
|
1207
1483
|
async runSerialized(scope, work) {
|
|
1208
1484
|
const prior = this.writeChain.get(scope) ?? Promise.resolve();
|
|
@@ -1229,7 +1505,7 @@ var DefaultMemoryStore = class {
|
|
|
1229
1505
|
if (writeErr) {
|
|
1230
1506
|
parts.push(`> \u26A0\uFE0F Memory write error (${labelOf(scope)}): ${writeErr.message}`);
|
|
1231
1507
|
}
|
|
1232
|
-
const body = await this.
|
|
1508
|
+
const body = await this.backend.readAll(scope, this.files[scope]);
|
|
1233
1509
|
if (body.trim()) parts.push(`## ${labelOf(scope)}
|
|
1234
1510
|
|
|
1235
1511
|
${body.trim()}`);
|
|
@@ -1237,117 +1513,214 @@ ${body.trim()}`);
|
|
|
1237
1513
|
return parts.join("\n\n");
|
|
1238
1514
|
}
|
|
1239
1515
|
async read(scope) {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1516
|
+
return this.backend.readAll(scope, this.files[scope]);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* List entries from a scope, newest first. Delegates to the backend
|
|
1520
|
+
* so graph/semantic backends can return enriched or filtered results.
|
|
1521
|
+
*/
|
|
1522
|
+
async list(scope = "project-memory", limit) {
|
|
1523
|
+
return this.backend.list(scope, this.files[scope], limit);
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Find memories related to the given text via graph traversal.
|
|
1527
|
+
* Falls back to content search when no graph backend is available.
|
|
1528
|
+
*/
|
|
1529
|
+
async findRelated(text, scope = "project-memory", limit = 5) {
|
|
1530
|
+
if (this.backend.findRelated) {
|
|
1531
|
+
return this.backend.findRelated(scope, this.files[scope], text, limit);
|
|
1244
1532
|
}
|
|
1533
|
+
return this.search(text, scope, limit);
|
|
1534
|
+
}
|
|
1535
|
+
async search(query, scope = "project-memory", limit) {
|
|
1536
|
+
return this.backend.search(scope, query, this.files[scope], limit);
|
|
1245
1537
|
}
|
|
1246
|
-
async remember(text, scope = "project-memory") {
|
|
1538
|
+
async remember(text, scope = "project-memory", metadata) {
|
|
1539
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1247
1540
|
return this.runSerialized(scope, async () => {
|
|
1248
|
-
const
|
|
1249
|
-
await
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
`;
|
|
1260
|
-
const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
|
|
1261
|
-
${entry}`;
|
|
1262
|
-
await atomicWrite(file, next);
|
|
1263
|
-
const buf = Buffer.byteLength(next, "utf8");
|
|
1264
|
-
if (buf > MAX_BYTES_TOTAL) {
|
|
1265
|
-
await this.consolidateUnsafe(scope);
|
|
1541
|
+
const entry = { scope, text, ts, ...metadata };
|
|
1542
|
+
await this.backend.remember(scope, entry, this.files[scope]);
|
|
1543
|
+
const raw = await this.backend.readAll(scope, this.files[scope]);
|
|
1544
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_BYTES_TOTAL) {
|
|
1545
|
+
const removed = await this.backend.consolidate(scope, this.files[scope]);
|
|
1546
|
+
if (removed > 0) {
|
|
1547
|
+
this.events?.emit("memory.consolidated", {
|
|
1548
|
+
scope,
|
|
1549
|
+
removed
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1266
1552
|
}
|
|
1553
|
+
await this.mirrorBackup(scope);
|
|
1554
|
+
this.events?.emit("memory.remembered", {
|
|
1555
|
+
scope,
|
|
1556
|
+
text,
|
|
1557
|
+
ts,
|
|
1558
|
+
type: entry.type,
|
|
1559
|
+
tags: entry.tags,
|
|
1560
|
+
priority: entry.priority
|
|
1561
|
+
});
|
|
1267
1562
|
});
|
|
1268
1563
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1564
|
+
/**
|
|
1565
|
+
* Score and rank memories by relevance to the current context.
|
|
1566
|
+
* Returns entries with score >= MIN_RELEVANCE_SCORE, sorted highest first.
|
|
1567
|
+
*/
|
|
1568
|
+
async scoreRelevant(ctx, scope = "project-memory", limit = 8) {
|
|
1569
|
+
const all = await this.list(scope);
|
|
1570
|
+
if (all.length === 0) return [];
|
|
1571
|
+
const taskWords = ctx.currentTask.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
1572
|
+
const skillWords = (ctx.activeSkills ?? []).flatMap((s) => s.split("-"));
|
|
1573
|
+
const toolWords = (ctx.toolNames ?? []).flatMap((t) => t.toLowerCase().split("_"));
|
|
1574
|
+
const now = Date.now();
|
|
1575
|
+
const scored = [];
|
|
1576
|
+
for (const entry of all) {
|
|
1577
|
+
let score = 0;
|
|
1578
|
+
const reasons = [];
|
|
1579
|
+
const textLower = entry.text.toLowerCase();
|
|
1580
|
+
const tagsLower = (entry.tags ?? []).map((t) => t.toLowerCase());
|
|
1581
|
+
let taskHits = 0;
|
|
1582
|
+
for (const w of taskWords) {
|
|
1583
|
+
if (textLower.includes(w)) {
|
|
1584
|
+
taskHits++;
|
|
1585
|
+
score += 2;
|
|
1586
|
+
}
|
|
1587
|
+
if (tagsLower.some((t) => t.includes(w))) {
|
|
1588
|
+
taskHits++;
|
|
1589
|
+
score += 3;
|
|
1295
1590
|
}
|
|
1296
1591
|
}
|
|
1297
|
-
if (
|
|
1298
|
-
|
|
1299
|
-
|
|
1592
|
+
if (taskHits > 0) reasons.push(`task match (${taskHits})`);
|
|
1593
|
+
let skillHits = 0;
|
|
1594
|
+
for (const w of skillWords) {
|
|
1595
|
+
if (w.length > 2 && (textLower.includes(w) || tagsLower.some((t) => t.includes(w)))) {
|
|
1596
|
+
skillHits++;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
score += skillHits;
|
|
1600
|
+
if (skillHits > 0) reasons.push(`skill match (${skillHits})`);
|
|
1601
|
+
for (const w of toolWords) {
|
|
1602
|
+
if (w.length > 2 && (textLower.includes(w) || tagsLower.some((t) => t.includes(w)))) {
|
|
1603
|
+
score += 1;
|
|
1604
|
+
reasons.push(`tool mention: ${w}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
switch (entry.priority) {
|
|
1608
|
+
case "critical":
|
|
1609
|
+
score += 5;
|
|
1610
|
+
reasons.push("critical");
|
|
1611
|
+
break;
|
|
1612
|
+
case "high":
|
|
1613
|
+
score += 3;
|
|
1614
|
+
reasons.push("high priority");
|
|
1615
|
+
break;
|
|
1616
|
+
case "medium":
|
|
1617
|
+
score += 1;
|
|
1618
|
+
break;
|
|
1619
|
+
case "low":
|
|
1620
|
+
score -= 2;
|
|
1621
|
+
reasons.push("low priority");
|
|
1622
|
+
break;
|
|
1623
|
+
}
|
|
1624
|
+
switch (entry.type) {
|
|
1625
|
+
case "decision":
|
|
1626
|
+
score += 2;
|
|
1627
|
+
reasons.push("decision");
|
|
1628
|
+
break;
|
|
1629
|
+
case "convention":
|
|
1630
|
+
score += 2;
|
|
1631
|
+
reasons.push("convention");
|
|
1632
|
+
break;
|
|
1633
|
+
case "anti_pattern":
|
|
1634
|
+
score += 3;
|
|
1635
|
+
reasons.push("anti-pattern");
|
|
1636
|
+
break;
|
|
1637
|
+
case "preference":
|
|
1638
|
+
score += 1;
|
|
1639
|
+
reasons.push("preference");
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
const ageDays = (now - new Date(entry.ts).getTime()) / (1e3 * 60 * 60 * 24);
|
|
1643
|
+
if (ageDays < 1) score += 1;
|
|
1644
|
+
else if (ageDays > 30) score -= 1;
|
|
1645
|
+
if (entry.confidence !== void 0 && entry.confidence < 0.5) {
|
|
1646
|
+
score -= 2;
|
|
1647
|
+
reasons.push("low confidence");
|
|
1648
|
+
}
|
|
1649
|
+
if (entry.lastAccessed) {
|
|
1650
|
+
const hoursSinceAccess = (now - new Date(entry.lastAccessed).getTime()) / (1e3 * 60 * 60);
|
|
1651
|
+
if (hoursSinceAccess < 1) score -= 1;
|
|
1652
|
+
}
|
|
1653
|
+
if (score > 0) {
|
|
1654
|
+
scored.push({
|
|
1655
|
+
...entry,
|
|
1656
|
+
score,
|
|
1657
|
+
matchReason: reasons.join(", ") || "keyword match"
|
|
1658
|
+
});
|
|
1300
1659
|
}
|
|
1301
|
-
return true;
|
|
1302
|
-
});
|
|
1303
|
-
if (removed > 0) {
|
|
1304
|
-
await atomicWrite(file, lines.join("\n"));
|
|
1305
1660
|
}
|
|
1306
|
-
|
|
1661
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1662
|
+
const threshold = 2;
|
|
1663
|
+
const relevant = scored.filter(
|
|
1664
|
+
(s) => s.score >= threshold || s.priority === "critical" || s.priority === "high"
|
|
1665
|
+
);
|
|
1666
|
+
return relevant.slice(0, Math.min(limit, 15));
|
|
1307
1667
|
}
|
|
1308
|
-
async
|
|
1309
|
-
return this.runSerialized(scope, async () =>
|
|
1668
|
+
async forget(query, scope = "project-memory") {
|
|
1669
|
+
return this.runSerialized(scope, async () => {
|
|
1670
|
+
const removed = await this.backend.forget(scope, query, this.files[scope]);
|
|
1671
|
+
if (removed > 0) {
|
|
1672
|
+
this.events?.emit("memory.forgotten", {
|
|
1673
|
+
scope,
|
|
1674
|
+
query,
|
|
1675
|
+
removed
|
|
1676
|
+
});
|
|
1677
|
+
await this.mirrorBackup(scope);
|
|
1678
|
+
}
|
|
1679
|
+
return removed;
|
|
1680
|
+
});
|
|
1310
1681
|
}
|
|
1311
|
-
async
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const trimmed = line.trim();
|
|
1322
|
-
if (!trimmed.startsWith("- ")) return true;
|
|
1323
|
-
const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
|
|
1324
|
-
if (seen.has(norm)) return false;
|
|
1325
|
-
seen.add(norm);
|
|
1326
|
-
return true;
|
|
1682
|
+
async consolidate(scope) {
|
|
1683
|
+
return this.runSerialized(scope, async () => {
|
|
1684
|
+
const removed = await this.backend.consolidate(scope, this.files[scope]);
|
|
1685
|
+
if (removed > 0) {
|
|
1686
|
+
this.events?.emit("memory.consolidated", {
|
|
1687
|
+
scope,
|
|
1688
|
+
removed
|
|
1689
|
+
});
|
|
1690
|
+
await this.mirrorBackup(scope);
|
|
1691
|
+
}
|
|
1327
1692
|
});
|
|
1328
|
-
const next = lines.join("\n");
|
|
1329
|
-
const backup = `${file}.bak.${Date.now()}`;
|
|
1330
|
-
try {
|
|
1331
|
-
await fsp.copyFile(file, backup);
|
|
1332
|
-
} catch {
|
|
1333
|
-
}
|
|
1334
|
-
try {
|
|
1335
|
-
await atomicWrite(file, next);
|
|
1336
|
-
} catch {
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
1693
|
}
|
|
1340
1694
|
async clear(scope) {
|
|
1341
1695
|
if (scope) {
|
|
1342
|
-
await this.runSerialized(scope, async () =>
|
|
1696
|
+
await this.runSerialized(scope, async () => {
|
|
1697
|
+
await this.backend.clear(scope, this.files[scope]);
|
|
1698
|
+
this.events?.emit("memory.cleared", { scope });
|
|
1699
|
+
await this.mirrorBackup(scope);
|
|
1700
|
+
});
|
|
1343
1701
|
return;
|
|
1344
1702
|
}
|
|
1345
1703
|
await Promise.all(
|
|
1346
1704
|
["project-agents", "project-memory", "user-memory"].map(
|
|
1347
|
-
(s) => this.runSerialized(s, async () =>
|
|
1705
|
+
(s) => this.runSerialized(s, async () => {
|
|
1706
|
+
await this.backend.clear(s, this.files[s]);
|
|
1707
|
+
this.events?.emit("memory.cleared", { scope: s });
|
|
1708
|
+
await this.mirrorBackup(s);
|
|
1709
|
+
})
|
|
1348
1710
|
)
|
|
1349
1711
|
);
|
|
1350
1712
|
}
|
|
1713
|
+
/** Mirror current memory content to the persistent backup directory. */
|
|
1714
|
+
async mirrorBackup(scope) {
|
|
1715
|
+
if (!this.persistBackup || scope === "project-agents") return;
|
|
1716
|
+
try {
|
|
1717
|
+
const content = await this.backend.readAll(scope, this.files[scope]);
|
|
1718
|
+
const { writeFile: writeFile6, mkdir: mkdir5 } = await import('fs/promises');
|
|
1719
|
+
await mkdir5(this.backupDir, { recursive: true });
|
|
1720
|
+
await writeFile6(`${this.backupDir}/${scope}.md`, content, "utf8");
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1351
1724
|
};
|
|
1352
1725
|
function labelOf(scope) {
|
|
1353
1726
|
switch (scope) {
|
|
@@ -1359,6 +1732,384 @@ function labelOf(scope) {
|
|
|
1359
1732
|
return "User memory";
|
|
1360
1733
|
}
|
|
1361
1734
|
}
|
|
1735
|
+
var GraphMemoryBackend = class {
|
|
1736
|
+
kind = "graph";
|
|
1737
|
+
file;
|
|
1738
|
+
graphFile;
|
|
1739
|
+
// In-memory graph state — lazily loaded, saved on mutation.
|
|
1740
|
+
nodes = /* @__PURE__ */ new Map();
|
|
1741
|
+
edges = [];
|
|
1742
|
+
loadedScope = null;
|
|
1743
|
+
loaded = false;
|
|
1744
|
+
constructor(opts) {
|
|
1745
|
+
this.file = new FileMemoryBackend({ paths: opts.paths });
|
|
1746
|
+
this.graphFile = opts.graphPath ?? `${opts.paths.projectDir}/memory-graph.json`;
|
|
1747
|
+
}
|
|
1748
|
+
// ── Backend interface ──────────────────────────────────────────────
|
|
1749
|
+
async remember(scope, entry, filePath) {
|
|
1750
|
+
await this.file.remember(scope, entry, filePath);
|
|
1751
|
+
await this.loadGraph(scope);
|
|
1752
|
+
const nodeId = this.nodeId(entry);
|
|
1753
|
+
const existing = this.nodes.get(nodeId);
|
|
1754
|
+
if (existing) {
|
|
1755
|
+
existing.count++;
|
|
1756
|
+
existing.entry = entry;
|
|
1757
|
+
existing.type = entry.type;
|
|
1758
|
+
existing.tags = entry.tags;
|
|
1759
|
+
existing.priority = entry.priority;
|
|
1760
|
+
} else {
|
|
1761
|
+
this.nodes.set(nodeId, {
|
|
1762
|
+
id: nodeId,
|
|
1763
|
+
entry,
|
|
1764
|
+
firstSeen: entry.ts,
|
|
1765
|
+
count: 1,
|
|
1766
|
+
type: entry.type,
|
|
1767
|
+
tags: entry.tags,
|
|
1768
|
+
priority: entry.priority
|
|
1769
|
+
});
|
|
1770
|
+
for (const [, other] of this.nodes) {
|
|
1771
|
+
if (other.id === nodeId) continue;
|
|
1772
|
+
const sim = wordOverlap(entry.text, other.entry.text);
|
|
1773
|
+
const tagSim = sharedTags(entry.tags ?? [], other.tags ?? []);
|
|
1774
|
+
const weight = Math.max(sim, tagSim * 0.5);
|
|
1775
|
+
if (weight > 0.15) {
|
|
1776
|
+
this.edges.push({
|
|
1777
|
+
from: nodeId,
|
|
1778
|
+
to: other.id,
|
|
1779
|
+
relation: sim >= tagSim ? "similar" : "same_turn",
|
|
1780
|
+
weight,
|
|
1781
|
+
ts: entry.ts
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
await this.saveGraph(scope);
|
|
1787
|
+
}
|
|
1788
|
+
async forget(scope, query, filePath) {
|
|
1789
|
+
const removed = await this.file.forget(scope, query, filePath);
|
|
1790
|
+
if (removed > 0) {
|
|
1791
|
+
await this.loadGraph(scope);
|
|
1792
|
+
const n = query.toLowerCase();
|
|
1793
|
+
const toRemove = [];
|
|
1794
|
+
for (const [id, node] of this.nodes) {
|
|
1795
|
+
if (node.entry.text.toLowerCase().includes(n)) {
|
|
1796
|
+
toRemove.push(id);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
for (const id of toRemove) this.nodes.delete(id);
|
|
1800
|
+
this.edges = this.edges.filter((e) => !toRemove.includes(e.from) && !toRemove.includes(e.to));
|
|
1801
|
+
await this.saveGraph(scope);
|
|
1802
|
+
}
|
|
1803
|
+
return removed;
|
|
1804
|
+
}
|
|
1805
|
+
async readAll(scope, filePath) {
|
|
1806
|
+
return this.file.readAll(scope, filePath);
|
|
1807
|
+
}
|
|
1808
|
+
async list(scope, filePath, limit) {
|
|
1809
|
+
await this.loadGraph(scope);
|
|
1810
|
+
const fileEntries = await this.file.list(scope, filePath);
|
|
1811
|
+
const nodeMap = new Map(this.nodes.entries());
|
|
1812
|
+
const enriched = fileEntries.map((fe) => {
|
|
1813
|
+
const nodeId = this.nodeId(fe);
|
|
1814
|
+
const node = nodeMap.get(nodeId);
|
|
1815
|
+
if (node) {
|
|
1816
|
+
return {
|
|
1817
|
+
...fe,
|
|
1818
|
+
type: node.type ?? fe.type,
|
|
1819
|
+
tags: node.tags ?? fe.tags,
|
|
1820
|
+
priority: node.priority ?? fe.priority
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
return fe;
|
|
1824
|
+
});
|
|
1825
|
+
const fileIds = new Set(fileEntries.map((e) => this.nodeId(e)));
|
|
1826
|
+
for (const [id, node] of this.nodes) {
|
|
1827
|
+
if (!fileIds.has(id)) {
|
|
1828
|
+
enriched.push(node.entry);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
enriched.sort((a, b) => b.ts.localeCompare(a.ts));
|
|
1832
|
+
return limit ? enriched.slice(0, limit) : enriched;
|
|
1833
|
+
}
|
|
1834
|
+
async search(scope, query, filePath, limit) {
|
|
1835
|
+
await this.loadGraph(scope);
|
|
1836
|
+
const needle = query.toLowerCase().split(/\s+/);
|
|
1837
|
+
const all = await this.list(scope, filePath);
|
|
1838
|
+
const scored = all.map((entry) => {
|
|
1839
|
+
const words = entry.text.toLowerCase().split(/\s+/);
|
|
1840
|
+
let score = 0;
|
|
1841
|
+
for (const n of needle) {
|
|
1842
|
+
if (words.some((w) => w.includes(n))) score += 1;
|
|
1843
|
+
if (entry.tags?.some((t) => t.toLowerCase().includes(n))) score += 2;
|
|
1844
|
+
}
|
|
1845
|
+
const node = this.nodes.get(this.nodeId(entry));
|
|
1846
|
+
if (node) {
|
|
1847
|
+
if (node.priority === "critical") score += 3;
|
|
1848
|
+
else if (node.priority === "high") score += 2;
|
|
1849
|
+
score += node.count * 0.5;
|
|
1850
|
+
}
|
|
1851
|
+
return { entry, score };
|
|
1852
|
+
});
|
|
1853
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1854
|
+
const matched = scored.filter((s) => s.score > 0).map((s) => s.entry);
|
|
1855
|
+
return limit ? matched.slice(0, limit) : matched;
|
|
1856
|
+
}
|
|
1857
|
+
async clear(scope, filePath) {
|
|
1858
|
+
await this.file.clear(scope, filePath);
|
|
1859
|
+
this.nodes.clear();
|
|
1860
|
+
this.edges = [];
|
|
1861
|
+
this.loadedScope = scope;
|
|
1862
|
+
this.loaded = true;
|
|
1863
|
+
try {
|
|
1864
|
+
await fsp.unlink(this.graphFile);
|
|
1865
|
+
} catch {
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
async consolidate(scope, filePath) {
|
|
1869
|
+
return this.file.consolidate(scope, filePath);
|
|
1870
|
+
}
|
|
1871
|
+
// ── Graph-specific queries ─────────────────────────────────────────
|
|
1872
|
+
/**
|
|
1873
|
+
* Find memories related to the given entry, ordered by edge weight.
|
|
1874
|
+
*/
|
|
1875
|
+
async findRelated(scope, _filePath, entryText, limit = 5) {
|
|
1876
|
+
await this.loadGraph(scope);
|
|
1877
|
+
const targetId = this.nodeId({ scope, text: entryText, ts: "" });
|
|
1878
|
+
const related = this.edges.filter((e) => e.from === targetId || e.to === targetId).sort((a, b) => b.weight - a.weight).slice(0, limit);
|
|
1879
|
+
const result = [];
|
|
1880
|
+
for (const edge of related) {
|
|
1881
|
+
const otherId = edge.from === targetId ? edge.to : edge.from;
|
|
1882
|
+
const node = this.nodes.get(otherId);
|
|
1883
|
+
if (node) result.push(node.entry);
|
|
1884
|
+
}
|
|
1885
|
+
return result;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Get all edges for visualization or traversal.
|
|
1889
|
+
*/
|
|
1890
|
+
getGraph() {
|
|
1891
|
+
return {
|
|
1892
|
+
nodes: [...this.nodes.values()],
|
|
1893
|
+
edges: [...this.edges]
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
// ── Persistence ────────────────────────────────────────────────────
|
|
1897
|
+
nodeId(entry) {
|
|
1898
|
+
const norm = entry.text.toLowerCase().trim().replace(/\s+/g, " ");
|
|
1899
|
+
return `${entry.scope ?? "mem"}::${simpleHash(norm)}`;
|
|
1900
|
+
}
|
|
1901
|
+
async loadGraph(scope) {
|
|
1902
|
+
if (this.loaded && this.loadedScope === scope) return;
|
|
1903
|
+
try {
|
|
1904
|
+
const raw = await fsp.readFile(this.graphFile, "utf8");
|
|
1905
|
+
const data = JSON.parse(raw);
|
|
1906
|
+
this.nodes = new Map(data.nodes);
|
|
1907
|
+
this.edges = data.edges;
|
|
1908
|
+
} catch {
|
|
1909
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
1910
|
+
this.edges = [];
|
|
1911
|
+
}
|
|
1912
|
+
this.loadedScope = scope;
|
|
1913
|
+
this.loaded = true;
|
|
1914
|
+
}
|
|
1915
|
+
async saveGraph(scope) {
|
|
1916
|
+
this.loadedScope = scope;
|
|
1917
|
+
this.loaded = true;
|
|
1918
|
+
try {
|
|
1919
|
+
const data = {
|
|
1920
|
+
nodes: [...this.nodes.entries()],
|
|
1921
|
+
edges: this.edges
|
|
1922
|
+
};
|
|
1923
|
+
await fsp.mkdir(
|
|
1924
|
+
this.graphFile.substring(0, this.graphFile.lastIndexOf("/")),
|
|
1925
|
+
{ recursive: true }
|
|
1926
|
+
);
|
|
1927
|
+
const tmp = `${this.graphFile}.tmp`;
|
|
1928
|
+
await fsp.writeFile(tmp, JSON.stringify(data));
|
|
1929
|
+
await fsp.rename(tmp, this.graphFile);
|
|
1930
|
+
} catch {
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
function wordOverlap(a, b) {
|
|
1935
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1936
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
1937
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
1938
|
+
let intersection = 0;
|
|
1939
|
+
for (const w of wordsA) {
|
|
1940
|
+
if (wordsB.has(w)) intersection++;
|
|
1941
|
+
}
|
|
1942
|
+
return intersection / Math.max(wordsA.size, wordsB.size);
|
|
1943
|
+
}
|
|
1944
|
+
function sharedTags(a, b) {
|
|
1945
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
1946
|
+
const setB = new Set(b);
|
|
1947
|
+
let shared = 0;
|
|
1948
|
+
for (const t of a) if (setB.has(t)) shared++;
|
|
1949
|
+
return shared / Math.max(a.length, b.length);
|
|
1950
|
+
}
|
|
1951
|
+
function simpleHash(s) {
|
|
1952
|
+
let h = 0;
|
|
1953
|
+
for (let i = 0; i < s.length; i++) {
|
|
1954
|
+
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
1955
|
+
}
|
|
1956
|
+
return Math.abs(h).toString(36);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/storage/memory-consolidator.ts
|
|
1960
|
+
function buildConsolidationPrompt(finalText, iterations, existingEntries) {
|
|
1961
|
+
const existingBlock = existingEntries.length > 0 ? `
|
|
1962
|
+
|
|
1963
|
+
Existing memory entries:
|
|
1964
|
+
${existingEntries.map((e) => `- [${e.ts.slice(0, 10)}] ${e.text}`).join("\n")}` : "";
|
|
1965
|
+
return `You are a memory consolidator. Review the following session summary and decide what key facts, conventions, decisions, or learnings should be persisted to long-term memory.
|
|
1966
|
+
|
|
1967
|
+
Session summary (${iterations} iterations):
|
|
1968
|
+
${finalText.slice(0, 3e3)}${existingBlock}
|
|
1969
|
+
|
|
1970
|
+
Return a JSON object with an "operations" array. Each operation must have an "action" field:
|
|
1971
|
+
- "add": create a new memory entry. Include "text", and optionally "type", "tags", "priority".
|
|
1972
|
+
- "edit": replace an existing entry. Include "query" (to match) and "text" (replacement).
|
|
1973
|
+
- "delete": remove an entry. Include "query" (to match).
|
|
1974
|
+
|
|
1975
|
+
Memory types:
|
|
1976
|
+
- "fact": Objective truth about the project (e.g. "uses pnpm workspaces")
|
|
1977
|
+
- "decision": A choice that was made (e.g. "decided to use biome over eslint")
|
|
1978
|
+
- "convention": A recurring pattern or standard (e.g. "commit messages use conventional format")
|
|
1979
|
+
- "preference": User or team preference (e.g. "prefers short variable names")
|
|
1980
|
+
- "reference": Pointer to a file or location (e.g. "auth logic in packages/core/src/auth/")
|
|
1981
|
+
- "anti_pattern": Something to avoid (e.g. "never use any in TypeScript")
|
|
1982
|
+
|
|
1983
|
+
Priority levels:
|
|
1984
|
+
- "critical": Must always be known (e.g. security constraints)
|
|
1985
|
+
- "high": Important for most tasks
|
|
1986
|
+
- "medium": Useful context
|
|
1987
|
+
- "low": Nice to know
|
|
1988
|
+
|
|
1989
|
+
Rules:
|
|
1990
|
+
- Only persist facts likely useful across multiple future sessions.
|
|
1991
|
+
- Do NOT persist task progress, temporary state, or one-off observations.
|
|
1992
|
+
- Prefer "add" over "edit" unless the existing entry is clearly outdated.
|
|
1993
|
+
- Assign a type and priority to every "add" operation.
|
|
1994
|
+
- Use 1-3 hashtag tags for each entry (e.g. #typescript #build).
|
|
1995
|
+
- Be concise \u2014 each memory entry should be one clear sentence.
|
|
1996
|
+
|
|
1997
|
+
Return ONLY valid JSON, no markdown, no explanation:
|
|
1998
|
+
{
|
|
1999
|
+
"operations": [
|
|
2000
|
+
{
|
|
2001
|
+
"action": "add",
|
|
2002
|
+
"text": "Project uses pnpm workspaces with TypeScript strict mode",
|
|
2003
|
+
"type": "convention",
|
|
2004
|
+
"priority": "high",
|
|
2005
|
+
"tags": ["pnpm", "typescript", "build"]
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
"action": "edit",
|
|
2009
|
+
"query": "pnpm",
|
|
2010
|
+
"text": "Project uses pnpm v9+ with ESM-only modules",
|
|
2011
|
+
"type": "fact",
|
|
2012
|
+
"priority": "medium"
|
|
2013
|
+
},
|
|
2014
|
+
{ "action": "delete", "query": "outdated convention" }
|
|
2015
|
+
]
|
|
2016
|
+
}`;
|
|
2017
|
+
}
|
|
2018
|
+
var SessionMemoryConsolidator = class {
|
|
2019
|
+
name = "session-memory-consolidator";
|
|
2020
|
+
owner = "core";
|
|
2021
|
+
memoryStore;
|
|
2022
|
+
provider;
|
|
2023
|
+
model;
|
|
2024
|
+
minIterations;
|
|
2025
|
+
maxExistingEntries;
|
|
2026
|
+
constructor(opts) {
|
|
2027
|
+
this.memoryStore = opts.memoryStore;
|
|
2028
|
+
this.provider = opts.provider;
|
|
2029
|
+
this.model = opts.model;
|
|
2030
|
+
this.minIterations = opts.minIterations ?? 2;
|
|
2031
|
+
this.maxExistingEntries = opts.maxExistingEntries ?? 15;
|
|
2032
|
+
}
|
|
2033
|
+
afterRun = async (ctx, result) => {
|
|
2034
|
+
if (result.status !== "done") return;
|
|
2035
|
+
if (!result.finalText || result.finalText.trim().length < 20) return;
|
|
2036
|
+
if (result.iterations < this.minIterations) return;
|
|
2037
|
+
const provider = this.provider ?? ctx.provider;
|
|
2038
|
+
if (!provider || !provider.complete) return;
|
|
2039
|
+
try {
|
|
2040
|
+
const existingEntries = await this.memoryStore.list("project-memory", this.maxExistingEntries);
|
|
2041
|
+
const prompt = buildConsolidationPrompt(
|
|
2042
|
+
result.finalText,
|
|
2043
|
+
result.iterations,
|
|
2044
|
+
existingEntries
|
|
2045
|
+
);
|
|
2046
|
+
const signal = AbortSignal.timeout(15e3);
|
|
2047
|
+
const response = await provider.complete(
|
|
2048
|
+
{
|
|
2049
|
+
model: this.model ?? ctx.model,
|
|
2050
|
+
system: [{ type: "text", text: prompt }],
|
|
2051
|
+
messages: [
|
|
2052
|
+
{ role: "user", content: "Review the session and return memory operations as JSON." }
|
|
2053
|
+
],
|
|
2054
|
+
maxTokens: 500
|
|
2055
|
+
},
|
|
2056
|
+
{ signal }
|
|
2057
|
+
);
|
|
2058
|
+
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
2059
|
+
if (!text) return;
|
|
2060
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
2061
|
+
if (!jsonMatch) return;
|
|
2062
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2063
|
+
if (!Array.isArray(parsed.operations) || parsed.operations.length === 0) return;
|
|
2064
|
+
let added = 0;
|
|
2065
|
+
let edited = 0;
|
|
2066
|
+
let deleted = 0;
|
|
2067
|
+
for (const op of parsed.operations) {
|
|
2068
|
+
switch (op.action) {
|
|
2069
|
+
case "add": {
|
|
2070
|
+
if (op.text && op.text.trim()) {
|
|
2071
|
+
await this.memoryStore.remember(op.text.trim(), void 0, {
|
|
2072
|
+
type: op.type,
|
|
2073
|
+
tags: op.tags,
|
|
2074
|
+
priority: op.priority
|
|
2075
|
+
});
|
|
2076
|
+
added++;
|
|
2077
|
+
}
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
case "edit": {
|
|
2081
|
+
if (op.query && op.text && op.text.trim()) {
|
|
2082
|
+
await this.memoryStore.forget(op.query);
|
|
2083
|
+
await this.memoryStore.remember(op.text.trim(), void 0, {
|
|
2084
|
+
type: op.type,
|
|
2085
|
+
tags: op.tags,
|
|
2086
|
+
priority: op.priority
|
|
2087
|
+
});
|
|
2088
|
+
edited++;
|
|
2089
|
+
}
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
case "delete": {
|
|
2093
|
+
if (op.query) {
|
|
2094
|
+
const n = await this.memoryStore.forget(op.query);
|
|
2095
|
+
deleted += n;
|
|
2096
|
+
}
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (added > 0 || edited > 0 || deleted > 0) {
|
|
2102
|
+
const parts = [];
|
|
2103
|
+
if (added) parts.push(`${added} added`);
|
|
2104
|
+
if (edited) parts.push(`${edited} edited`);
|
|
2105
|
+
if (deleted) parts.push(`${deleted} deleted`);
|
|
2106
|
+
process.stderr.write(`[memory] Session consolidation: ${parts.join(", ")}
|
|
2107
|
+
`);
|
|
2108
|
+
}
|
|
2109
|
+
} catch {
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
};
|
|
1362
2113
|
|
|
1363
2114
|
// src/storage/config-store.ts
|
|
1364
2115
|
function stripEphemeralFields(cfg) {
|
|
@@ -1589,6 +2340,13 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
1589
2340
|
function envBool(v) {
|
|
1590
2341
|
return !/^(0|false|no|off)$/i.test(v.trim());
|
|
1591
2342
|
}
|
|
2343
|
+
function envBoolOptional(v) {
|
|
2344
|
+
return v !== void 0 && envBool(v);
|
|
2345
|
+
}
|
|
2346
|
+
var LOG_LEVELS = /* @__PURE__ */ new Set(["error", "warn", "info", "debug", "trace"]);
|
|
2347
|
+
function envLogLevel(v) {
|
|
2348
|
+
return LOG_LEVELS.has(v) ? v : "info";
|
|
2349
|
+
}
|
|
1592
2350
|
var ENV_MAP = {
|
|
1593
2351
|
WRONGSTACK_PROVIDER: (c, v) => {
|
|
1594
2352
|
c.provider = v;
|
|
@@ -1612,7 +2370,7 @@ var ENV_MAP = {
|
|
|
1612
2370
|
},
|
|
1613
2371
|
WRONGSTACK_LOG_LEVEL: (c, v) => {
|
|
1614
2372
|
if (!c.log) c.log = { level: "info" };
|
|
1615
|
-
c.log.level = v;
|
|
2373
|
+
c.log.level = envLogLevel(v);
|
|
1616
2374
|
},
|
|
1617
2375
|
WRONGSTACK_INDEX_ON_START: (c, v) => {
|
|
1618
2376
|
c.indexing = { ...defaultIndexing, ...c.indexing, onSessionStart: envBool(v) };
|
|
@@ -1654,7 +2412,7 @@ function deepMerge(base, patch) {
|
|
|
1654
2412
|
out[k] = [.../* @__PURE__ */ new Set([...existing, ...v])];
|
|
1655
2413
|
} else {
|
|
1656
2414
|
out[k] = v;
|
|
1657
|
-
if (process.env.WRONGSTACK_DEBUG_CONFIG) {
|
|
2415
|
+
if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
|
|
1658
2416
|
console.warn(
|
|
1659
2417
|
`[config] Non-primitive array for "${k}" replaced (global + local config merge). Global entries: ${existing?.length ?? 0}, local entries: ${v.length}.`
|
|
1660
2418
|
);
|
|
@@ -2047,12 +2805,6 @@ function compileUserRegex(pattern, flags) {
|
|
|
2047
2805
|
}
|
|
2048
2806
|
|
|
2049
2807
|
// src/storage/session-reader.ts
|
|
2050
|
-
function expectDefined3(value) {
|
|
2051
|
-
if (value === null || value === void 0) {
|
|
2052
|
-
throw new Error("Expected value to be defined");
|
|
2053
|
-
}
|
|
2054
|
-
return value;
|
|
2055
|
-
}
|
|
2056
2808
|
var DefaultSessionReader = class {
|
|
2057
2809
|
store;
|
|
2058
2810
|
constructor(opts) {
|
|
@@ -2114,7 +2866,7 @@ var DefaultSessionReader = class {
|
|
|
2114
2866
|
continue;
|
|
2115
2867
|
}
|
|
2116
2868
|
for (let i = 0; i < data.events.length; i++) {
|
|
2117
|
-
const ev =
|
|
2869
|
+
const ev = expectDefined(data.events[i]);
|
|
2118
2870
|
if (allowedTypes && !allowedTypes.has(ev.type)) continue;
|
|
2119
2871
|
const text = eventText(ev);
|
|
2120
2872
|
if (text === null) continue;
|
|
@@ -2327,12 +3079,6 @@ function renderPlainText(meta, events) {
|
|
|
2327
3079
|
}
|
|
2328
3080
|
return lines.join("\n");
|
|
2329
3081
|
}
|
|
2330
|
-
function expectDefined4(value) {
|
|
2331
|
-
if (value === null || value === void 0) {
|
|
2332
|
-
throw new Error("Expected value to be defined");
|
|
2333
|
-
}
|
|
2334
|
-
return value;
|
|
2335
|
-
}
|
|
2336
3082
|
var FILE_VERSION = 1;
|
|
2337
3083
|
var MAX_TEXT_LENGTH = 2e3;
|
|
2338
3084
|
var MAX_ANNOTATIONS = 1e3;
|
|
@@ -2373,9 +3119,7 @@ var AnnotationsStore = class {
|
|
|
2373
3119
|
throw new Error("Annotation text must be non-empty");
|
|
2374
3120
|
}
|
|
2375
3121
|
if (text.length > MAX_TEXT_LENGTH) {
|
|
2376
|
-
throw new Error(
|
|
2377
|
-
`Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`
|
|
2378
|
-
);
|
|
3122
|
+
throw new Error(`Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`);
|
|
2379
3123
|
}
|
|
2380
3124
|
if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
|
|
2381
3125
|
throw new Error("atEventIndex must be a non-negative integer");
|
|
@@ -2391,20 +3135,22 @@ var AnnotationsStore = class {
|
|
|
2391
3135
|
resolved: false
|
|
2392
3136
|
};
|
|
2393
3137
|
await this.enqueue(input.sessionId, async () => {
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
3138
|
+
await withFileLock(this.filePath(input.sessionId), async () => {
|
|
3139
|
+
const all = await this.list(input.sessionId);
|
|
3140
|
+
all.push(annotation);
|
|
3141
|
+
if (all.length > MAX_ANNOTATIONS) {
|
|
3142
|
+
const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
|
|
3143
|
+
if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
|
|
3144
|
+
return x.a.createdAt.localeCompare(y.a.createdAt);
|
|
3145
|
+
});
|
|
3146
|
+
const evictCount = all.length - MAX_ANNOTATIONS;
|
|
3147
|
+
const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
|
|
3148
|
+
const kept = all.filter((a) => !toEvict.has(a.id));
|
|
3149
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
|
|
3150
|
+
} else {
|
|
3151
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
2408
3154
|
});
|
|
2409
3155
|
return annotation;
|
|
2410
3156
|
}
|
|
@@ -2417,21 +3163,23 @@ var AnnotationsStore = class {
|
|
|
2417
3163
|
async resolve(input) {
|
|
2418
3164
|
let updated = null;
|
|
2419
3165
|
await this.enqueue(input.sessionId, async () => {
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
3166
|
+
await withFileLock(this.filePath(input.sessionId), async () => {
|
|
3167
|
+
const all = await this.list(input.sessionId);
|
|
3168
|
+
const idx = all.findIndex((a) => a.id === input.annotationId);
|
|
3169
|
+
if (idx === -1) {
|
|
3170
|
+
updated = null;
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const next = {
|
|
3174
|
+
...expectDefined(all[idx]),
|
|
3175
|
+
resolved: true,
|
|
3176
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3177
|
+
resolvedBy: input.resolvedBy
|
|
3178
|
+
};
|
|
3179
|
+
all[idx] = next;
|
|
3180
|
+
await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
|
|
3181
|
+
updated = next;
|
|
3182
|
+
});
|
|
2435
3183
|
});
|
|
2436
3184
|
return updated;
|
|
2437
3185
|
}
|
|
@@ -2529,22 +3277,22 @@ var ReplayLogStore = class {
|
|
|
2529
3277
|
async record(input) {
|
|
2530
3278
|
const hash = hashRequest(input.request);
|
|
2531
3279
|
await this.enqueue(input.sessionId, async () => {
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
this.
|
|
2547
|
-
}
|
|
3280
|
+
await withFileLock(this.filePath(input.sessionId), async () => {
|
|
3281
|
+
const entries = await this.readAll(input.sessionId);
|
|
3282
|
+
if (entries.some((entry2) => entry2.hash === hash)) return;
|
|
3283
|
+
const entry = {
|
|
3284
|
+
hash,
|
|
3285
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3286
|
+
request: input.request,
|
|
3287
|
+
response: input.response
|
|
3288
|
+
};
|
|
3289
|
+
entries.push(entry);
|
|
3290
|
+
const keep = entries.slice(-this.maxEntries);
|
|
3291
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3292
|
+
for (const e of keep) cache.set(e.hash, e);
|
|
3293
|
+
this.cache.set(input.sessionId, cache);
|
|
3294
|
+
await this.rewriteCache(input.sessionId, cache);
|
|
3295
|
+
});
|
|
2548
3296
|
});
|
|
2549
3297
|
return hash;
|
|
2550
3298
|
}
|
|
@@ -2553,7 +3301,7 @@ var ReplayLogStore = class {
|
|
|
2553
3301
|
* Called when entry count exceeds the cap. Rewrites the entire file
|
|
2554
3302
|
* but only happens O(n / maxEntries) times per session.
|
|
2555
3303
|
*/
|
|
2556
|
-
async
|
|
3304
|
+
async rewriteCache(sessionId, cache) {
|
|
2557
3305
|
const all = [...cache.values()];
|
|
2558
3306
|
const keep = all.slice(-this.maxEntries);
|
|
2559
3307
|
await this.writeAll(sessionId, keep);
|
|
@@ -2617,11 +3365,12 @@ var ReplayLogStore = class {
|
|
|
2617
3365
|
for (const line of raw.split("\n")) {
|
|
2618
3366
|
if (!line.trim()) continue;
|
|
2619
3367
|
try {
|
|
2620
|
-
const parsed =
|
|
2621
|
-
if (
|
|
2622
|
-
|
|
3368
|
+
const parsed = safeParse(line);
|
|
3369
|
+
if (!parsed.ok || !parsed.value) continue;
|
|
3370
|
+
if ("entry" in parsed.value && parsed.value.entry) {
|
|
3371
|
+
out.push(parsed.value.entry);
|
|
2623
3372
|
} else {
|
|
2624
|
-
out.push(parsed);
|
|
3373
|
+
out.push(parsed.value);
|
|
2625
3374
|
}
|
|
2626
3375
|
} catch {
|
|
2627
3376
|
}
|
|
@@ -2657,12 +3406,6 @@ var ReplayLogStore = class {
|
|
|
2657
3406
|
return next;
|
|
2658
3407
|
}
|
|
2659
3408
|
};
|
|
2660
|
-
function expectDefined5(value) {
|
|
2661
|
-
if (value === null || value === void 0) {
|
|
2662
|
-
throw new Error("Expected value to be defined");
|
|
2663
|
-
}
|
|
2664
|
-
return value;
|
|
2665
|
-
}
|
|
2666
3409
|
var SessionRecovery = class {
|
|
2667
3410
|
constructor(dir) {
|
|
2668
3411
|
this.dir = dir;
|
|
@@ -2703,7 +3446,7 @@ var SessionRecovery = class {
|
|
|
2703
3446
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
2704
3447
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2705
3448
|
try {
|
|
2706
|
-
const ev = JSON.parse(
|
|
3449
|
+
const ev = JSON.parse(expectDefined(lines[i]));
|
|
2707
3450
|
if (ev.type === "in_flight_start") {
|
|
2708
3451
|
return {
|
|
2709
3452
|
sessionId,
|
|
@@ -2756,12 +3499,12 @@ var SessionRecovery = class {
|
|
|
2756
3499
|
let lastCheckpointIdx = -1;
|
|
2757
3500
|
for (let i = 0; i < events.length; i++) {
|
|
2758
3501
|
if (events[i]?.type === "checkpoint") {
|
|
2759
|
-
lastCheckpoint =
|
|
3502
|
+
lastCheckpoint = expectDefined(events[i]);
|
|
2760
3503
|
lastCheckpointIdx = i;
|
|
2761
3504
|
}
|
|
2762
3505
|
}
|
|
2763
3506
|
const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
|
|
2764
|
-
const lastEv =
|
|
3507
|
+
const lastEv = expectDefined(events[events.length - 1]);
|
|
2765
3508
|
const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
|
|
2766
3509
|
const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
|
|
2767
3510
|
return {
|
|
@@ -2804,12 +3547,6 @@ var SessionRecovery = class {
|
|
|
2804
3547
|
return path2.join(this.dir, `${sessionId}.jsonl`);
|
|
2805
3548
|
}
|
|
2806
3549
|
};
|
|
2807
|
-
function expectDefined6(value) {
|
|
2808
|
-
if (value === null || value === void 0) {
|
|
2809
|
-
throw new Error("Expected value to be defined");
|
|
2810
|
-
}
|
|
2811
|
-
return value;
|
|
2812
|
-
}
|
|
2813
3550
|
var GENESIS_PREV = "0".repeat(64);
|
|
2814
3551
|
var DEFAULT_FSYNC_EVERY = 100;
|
|
2815
3552
|
var ToolAuditLog = class {
|
|
@@ -2835,37 +3572,42 @@ var ToolAuditLog = class {
|
|
|
2835
3572
|
async record(input) {
|
|
2836
3573
|
let entry = null;
|
|
2837
3574
|
await this.enqueue(input.sessionId, async () => {
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
id
|
|
2844
|
-
ts
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
3575
|
+
await withFileLock(this.filePath(input.sessionId), async () => {
|
|
3576
|
+
const entries = await this.readAll(input.sessionId);
|
|
3577
|
+
const prev = entries.at(-1);
|
|
3578
|
+
const prevHash = prev?.hash ?? GENESIS_PREV;
|
|
3579
|
+
const index = prev ? prev.index + 1 : 0;
|
|
3580
|
+
const id = randomUUID();
|
|
3581
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3582
|
+
const content = {
|
|
3583
|
+
id,
|
|
3584
|
+
ts,
|
|
3585
|
+
prevHash,
|
|
3586
|
+
toolName: input.toolName,
|
|
3587
|
+
toolUseId: input.toolUseId,
|
|
3588
|
+
input: input.input,
|
|
3589
|
+
output: input.output,
|
|
3590
|
+
isError: input.isError,
|
|
3591
|
+
index
|
|
3592
|
+
};
|
|
3593
|
+
const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
|
|
3594
|
+
entry = {
|
|
3595
|
+
id,
|
|
3596
|
+
ts,
|
|
3597
|
+
prevHash,
|
|
3598
|
+
hash,
|
|
3599
|
+
toolName: input.toolName,
|
|
3600
|
+
toolUseId: input.toolUseId,
|
|
3601
|
+
input: input.input,
|
|
3602
|
+
output: input.output,
|
|
3603
|
+
isError: input.isError,
|
|
3604
|
+
index
|
|
3605
|
+
};
|
|
3606
|
+
entries.push(entry);
|
|
3607
|
+
await this.writeAll(input.sessionId, entries);
|
|
3608
|
+
this.tailHash.set(input.sessionId, hash);
|
|
3609
|
+
this.tailIndex.set(input.sessionId, index + 1);
|
|
3610
|
+
});
|
|
2869
3611
|
});
|
|
2870
3612
|
return entry;
|
|
2871
3613
|
}
|
|
@@ -2885,7 +3627,7 @@ var ToolAuditLog = class {
|
|
|
2885
3627
|
}
|
|
2886
3628
|
let prevHash = GENESIS_PREV;
|
|
2887
3629
|
for (let i = 0; i < entries.length; i++) {
|
|
2888
|
-
const e =
|
|
3630
|
+
const e = expectDefined(entries[i]);
|
|
2889
3631
|
if (e.prevHash !== prevHash) {
|
|
2890
3632
|
return {
|
|
2891
3633
|
ok: false,
|
|
@@ -2935,7 +3677,8 @@ var ToolAuditLog = class {
|
|
|
2935
3677
|
for (const line of raw.split("\n")) {
|
|
2936
3678
|
if (!line.trim()) continue;
|
|
2937
3679
|
try {
|
|
2938
|
-
|
|
3680
|
+
const parsed = safeParse(line);
|
|
3681
|
+
if (parsed.ok && parsed.value) out.push(parsed.value);
|
|
2939
3682
|
} catch {
|
|
2940
3683
|
}
|
|
2941
3684
|
}
|
|
@@ -2945,10 +3688,10 @@ var ToolAuditLog = class {
|
|
|
2945
3688
|
return [];
|
|
2946
3689
|
}
|
|
2947
3690
|
}
|
|
2948
|
-
async
|
|
3691
|
+
async writeAll(sessionId, entries) {
|
|
2949
3692
|
const fp = this.filePath(sessionId);
|
|
2950
|
-
const line = JSON.stringify(
|
|
2951
|
-
await
|
|
3693
|
+
const line = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
3694
|
+
await atomicWrite(fp, line, { mode: 384 });
|
|
2952
3695
|
const count = (this.unSyncedWrites.get(sessionId) ?? 0) + 1;
|
|
2953
3696
|
this.unSyncedWrites.set(sessionId, count);
|
|
2954
3697
|
if (this.fsyncEvery !== Number.POSITIVE_INFINITY && count % this.fsyncEvery === 0) {
|
|
@@ -3101,12 +3844,6 @@ var SessionAnalyzer = class {
|
|
|
3101
3844
|
return last - first;
|
|
3102
3845
|
}
|
|
3103
3846
|
};
|
|
3104
|
-
function expectDefined7(value) {
|
|
3105
|
-
if (value === null || value === void 0) {
|
|
3106
|
-
throw new Error("Expected value to be defined");
|
|
3107
|
-
}
|
|
3108
|
-
return value;
|
|
3109
|
-
}
|
|
3110
3847
|
var DefaultSessionRewinder = class {
|
|
3111
3848
|
constructor(sessionsDir, projectRoot) {
|
|
3112
3849
|
this.sessionsDir = sessionsDir;
|
|
@@ -3145,7 +3882,7 @@ var DefaultSessionRewinder = class {
|
|
|
3145
3882
|
const events = parseEvents(raw);
|
|
3146
3883
|
let targetIdx = -1;
|
|
3147
3884
|
for (let i = 0; i < events.length; i++) {
|
|
3148
|
-
const event =
|
|
3885
|
+
const event = expectDefined(events[i]);
|
|
3149
3886
|
if (event.type === "checkpoint") {
|
|
3150
3887
|
const checkpointEvent = event;
|
|
3151
3888
|
if (checkpointEvent.promptIndex === checkpointIndex) {
|
|
@@ -3159,7 +3896,7 @@ var DefaultSessionRewinder = class {
|
|
|
3159
3896
|
}
|
|
3160
3897
|
const snapshotsToRevert = [];
|
|
3161
3898
|
for (let i = targetIdx + 1; i < events.length; i++) {
|
|
3162
|
-
const event =
|
|
3899
|
+
const event = expectDefined(events[i]);
|
|
3163
3900
|
if (event.type === "checkpoint") {
|
|
3164
3901
|
break;
|
|
3165
3902
|
}
|
|
@@ -3306,7 +4043,10 @@ function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
|
3306
4043
|
let pending = null;
|
|
3307
4044
|
let writeChain = Promise.resolve();
|
|
3308
4045
|
const enqueueWrite = (todos) => {
|
|
3309
|
-
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos))
|
|
4046
|
+
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos)).catch((err) => {
|
|
4047
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4048
|
+
console.error(`[TodosCheckpoint] save failed for session ${sessionId}: ${msg}`);
|
|
4049
|
+
});
|
|
3310
4050
|
return writeChain;
|
|
3311
4051
|
};
|
|
3312
4052
|
const flush = () => {
|
|
@@ -3604,7 +4344,7 @@ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
|
3604
4344
|
}
|
|
3605
4345
|
const lock = {
|
|
3606
4346
|
pid: processId,
|
|
3607
|
-
hostname:
|
|
4347
|
+
hostname: hostname(),
|
|
3608
4348
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3609
4349
|
};
|
|
3610
4350
|
await atomicWrite(lockPath, JSON.stringify(lock), { mode: 384 });
|
|
@@ -3974,12 +4714,6 @@ var DefaultPromptStore = class {
|
|
|
3974
4714
|
};
|
|
3975
4715
|
}
|
|
3976
4716
|
};
|
|
3977
|
-
function expectDefined8(value) {
|
|
3978
|
-
if (value === null || value === void 0) {
|
|
3979
|
-
throw new Error("Expected value to be defined");
|
|
3980
|
-
}
|
|
3981
|
-
return value;
|
|
3982
|
-
}
|
|
3983
4717
|
var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
|
|
3984
4718
|
var CloudSync = class {
|
|
3985
4719
|
constructor(paths, getConfig, setConfig) {
|
|
@@ -4022,8 +4756,8 @@ var CloudSync = class {
|
|
|
4022
4756
|
const cfg = this.getConfig();
|
|
4023
4757
|
if (!cfg?.enabled) return { ok: false, action: "push", categories: [], message: "Not enabled." };
|
|
4024
4758
|
const parts = cfg.repo.split("/");
|
|
4025
|
-
const owner =
|
|
4026
|
-
const repoName =
|
|
4759
|
+
const owner = expectDefined(parts[0]);
|
|
4760
|
+
const repoName = expectDefined(parts[1]);
|
|
4027
4761
|
const branch = "main";
|
|
4028
4762
|
const baseTreeSha = this.state?.sha;
|
|
4029
4763
|
const { treeEntries, rev } = await this.buildLocalTree(cfg.categories);
|
|
@@ -4075,8 +4809,8 @@ var CloudSync = class {
|
|
|
4075
4809
|
const cfg = this.getConfig();
|
|
4076
4810
|
if (!cfg?.enabled) return { ok: false, action: "pull", categories: [], message: "Not enabled." };
|
|
4077
4811
|
const pullParts = cfg.repo.split("/");
|
|
4078
|
-
const owner =
|
|
4079
|
-
const repoName =
|
|
4812
|
+
const owner = expectDefined(pullParts[0]);
|
|
4813
|
+
const repoName = expectDefined(pullParts[1]);
|
|
4080
4814
|
const branchData = await this.getRef(token, owner, repoName, "main");
|
|
4081
4815
|
const currentSha = branchData.object.sha;
|
|
4082
4816
|
const commitData = await this.getCommit(token, owner, repoName, currentSha);
|
|
@@ -4393,6 +5127,6 @@ function resolveSessionLoggingConfig(cfg) {
|
|
|
4393
5127
|
};
|
|
4394
5128
|
}
|
|
4395
5129
|
|
|
4396
|
-
export { ALL_SYNC_CATEGORIES, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, MAX_JOURNAL_ENTRIES, QueueStore, RecoveryLock, ReplayLogStore, STANDARD_AUDIT_EVENTS, SessionAnalyzer, SessionRecovery, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, createSessionEventBridge, deriveTodosFromPlanItem, emptyGoal, emptyPlan, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, goalFilePath, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTodosCheckpoint, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, saveGoal, savePlan, saveTodosCheckpoint, setPlanItemStatus, summarizeUsage };
|
|
5130
|
+
export { ALL_SYNC_CATEGORIES, AnnotationsStore, CORE_RECONSTRUCT_EVENTS, CloudSync, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultPromptStore, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DirectorStateCheckpoint, FileMemoryBackend, GraphMemoryBackend, MAX_JOURNAL_ENTRIES, QueueStore, RecoveryLock, ReplayLogStore, STANDARD_AUDIT_EVENTS, SessionAnalyzer, SessionMemoryConsolidator, SessionRecovery, ToolAuditLog, addPlanItem, appendJournal, attachPlanCheckpoint, attachTodosCheckpoint, clearPlan, createSessionEventBridge, deriveTodosFromPlanItem, emptyGoal, emptyPlan, formatGoal, formatPlan, formatPlanTemplates, getPlanTemplate, goalFilePath, listPlanTemplates, loadDirectorState, loadGoal, loadPlan, loadTodosCheckpoint, parseEntries, removePlanItem, resolveAuditLevel, resolveSessionLoggingConfig, runConfigMigrations, saveGoal, savePlan, saveTodosCheckpoint, setPlanItemStatus, summarizeUsage };
|
|
4397
5131
|
//# sourceMappingURL=index.js.map
|
|
4398
5132
|
//# sourceMappingURL=index.js.map
|