@inetafrica/open-claudia 3.0.44 → 3.1.0
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/CHANGELOG.md +37 -0
- package/README.md +9 -5
- package/bin/tool.js +15 -5
- package/bot.js +78 -3
- package/core/boot-health.js +84 -0
- package/core/doctor-memory.js +213 -0
- package/core/doctor.js +5 -0
- package/core/dream.js +11 -9
- package/core/entities.js +47 -3
- package/core/handlers.js +118 -1
- package/core/keyring.js +24 -4
- package/core/memory-mutation-queue.js +42 -11
- package/core/pack-review.js +60 -4
- package/core/packs.js +47 -3
- package/core/recall/discoverer.js +22 -6
- package/core/recall/graph.js +84 -52
- package/core/recall/tuning.js +1 -2
- package/core/router.js +18 -0
- package/core/runner.js +21 -0
- package/core/safe-mode.js +227 -0
- package/core/state.js +2 -2
- package/core/system-prompt.js +43 -3
- package/core/tool-graph.js +99 -71
- package/core/tools.js +3 -2
- package/core/turn-observer.js +23 -1
- package/package.json +3 -3
- package/test-extractive-injection.js +21 -0
- package/test-memory-mutation-queue.js +33 -3
- package/test-provider-compaction.js +5 -0
- package/test-provider-core-prompt.js +3 -0
- package/test-provider-language.js +48 -1
- package/test-provider-recall.js +33 -0
- package/test-recall-discoverer.js +8 -0
- package/test-recall-graph.js +66 -11
- package/test-tool-graph.js +67 -16
- package/test-tool-manifest.js +32 -0
- package/test-tools.js +31 -0
package/core/tool-graph.js
CHANGED
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
// and traversed directionally. Each edge carries a Hebbian `weight` bumped on
|
|
24
24
|
// co-use and decayed over time; unlike pack edges there is no structural floor,
|
|
25
25
|
// so a chain that stops being used eventually decays away and is pruned.
|
|
26
|
+
//
|
|
27
|
+
// Decay is computed ON READ (same doctrine as core/recall/graph.js): the stored
|
|
28
|
+
// `weight` is the value AS OF `last_reinforced`; reads map it through
|
|
29
|
+
// effectiveWeight() and reinforcement settles accrued decay before bumping.
|
|
30
|
+
// Nothing rewrites weights nightly — tend() only physically deletes edges whose
|
|
31
|
+
// effective weight has faded under `pruneBelow`. The old nightly decay()
|
|
32
|
+
// rewrite compounded (full age reapplied to an already-decayed weight each
|
|
33
|
+
// night), emptying chains in days; that path no longer exists.
|
|
26
34
|
|
|
27
35
|
const fs = require("fs");
|
|
28
36
|
const path = require("path");
|
|
@@ -37,8 +45,8 @@ const GRAPH_DB = process.env.TOOL_GRAPH_DB
|
|
|
37
45
|
|
|
38
46
|
// Tunables (overridable via dream knobs later).
|
|
39
47
|
const DEFAULTS = {
|
|
40
|
-
halfLifeDays: 45, // Hebbian weight half-life for decay
|
|
41
|
-
pruneBelow: 0.15, // edges
|
|
48
|
+
halfLifeDays: 45, // Hebbian weight half-life for read-time decay
|
|
49
|
+
pruneBelow: 0.15, // edges whose effective weight falls under this are dead (no structural floor)
|
|
42
50
|
};
|
|
43
51
|
|
|
44
52
|
let _db = null;
|
|
@@ -82,6 +90,18 @@ function openDb() {
|
|
|
82
90
|
)`);
|
|
83
91
|
db.exec("CREATE INDEX IF NOT EXISTS pack_tool_edges_pack ON pack_tool_edges (pack)");
|
|
84
92
|
db.exec("CREATE INDEX IF NOT EXISTS pack_tool_edges_tool ON pack_tool_edges (tool)");
|
|
93
|
+
// v1 migration: legacy DBs carry weights the old nightly decay() rewrite
|
|
94
|
+
// had already over-decayed (compounding). Treat stored values as current
|
|
95
|
+
// and restart the clock — decay is read-time from here on.
|
|
96
|
+
try {
|
|
97
|
+
const uv = db.prepare("PRAGMA user_version").get() || {};
|
|
98
|
+
if (Number(uv.user_version || 0) < 1) {
|
|
99
|
+
const stamp = now();
|
|
100
|
+
db.prepare("UPDATE tool_edges SET last_reinforced=? WHERE last_reinforced IS NOT NULL").run(stamp);
|
|
101
|
+
db.prepare("UPDATE pack_tool_edges SET last_reinforced=? WHERE last_reinforced IS NOT NULL").run(stamp);
|
|
102
|
+
db.exec("PRAGMA user_version = 1");
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {}
|
|
85
105
|
_db = db;
|
|
86
106
|
return db;
|
|
87
107
|
} catch (e) {
|
|
@@ -91,23 +111,49 @@ function openDb() {
|
|
|
91
111
|
|
|
92
112
|
function now() { return new Date().toISOString(); }
|
|
93
113
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
114
|
+
// Read-time weight of an edge row: exponential decay of the stored weight since
|
|
115
|
+
// last_reinforced. Pure — storage is never touched. No structural floor here;
|
|
116
|
+
// an edge that was never reinforced (last_reinforced null) does not decay.
|
|
117
|
+
function effectiveWeight(e, nowMs = Date.now(), halfLifeDays = DEFAULTS.halfLifeDays) {
|
|
118
|
+
if (!e || !Number.isFinite(e.weight)) return 0;
|
|
119
|
+
if (!e.last_reinforced) return e.weight;
|
|
120
|
+
const ageDays = (nowMs - Date.parse(e.last_reinforced)) / 86400000;
|
|
121
|
+
if (!(ageDays > 0)) return e.weight;
|
|
122
|
+
return e.weight * Math.exp(-Math.log(2) * ageDays / Math.max(1, halfLifeDays));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Rank rows by read-time weight, dropping ones already faded under pruneBelow
|
|
126
|
+
// (dead advice — physically deleted on the next tend()). Weights are rounded
|
|
127
|
+
// to 4dp so a seconds-old reinforcement still reads as its whole value.
|
|
128
|
+
function rankLive(rows, limit) {
|
|
129
|
+
const nowMs = Date.now();
|
|
130
|
+
return rows
|
|
131
|
+
.map((r) => ({ ...r, weight: Math.round(effectiveWeight(r, nowMs) * 1e4) / 1e4 }))
|
|
132
|
+
.filter((r) => r.weight >= DEFAULTS.pruneBelow)
|
|
133
|
+
.sort((a, b) => b.weight - a.weight
|
|
134
|
+
|| String(b.last_reinforced || "").localeCompare(String(a.last_reinforced || "")))
|
|
135
|
+
.slice(0, Math.max(0, limit));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Directed upsert of a follows-edge src→dst. `bump` settles the decay accrued
|
|
139
|
+
// since last_reinforced, adds to the effective weight (Hebbian) and restamps;
|
|
140
|
+
// with no bump it just ensures the edge exists at `weight` (default 1).
|
|
97
141
|
function addFollow(src, dst, opts = {}) {
|
|
98
142
|
const db = openDb();
|
|
99
143
|
if (!db || !src || !dst || src === dst) return false;
|
|
100
144
|
const bump = Number.isFinite(opts.bump) ? opts.bump : 0;
|
|
101
145
|
const weight = Number.isFinite(opts.weight) ? opts.weight : 1;
|
|
102
146
|
try {
|
|
103
|
-
const existing = db.prepare("SELECT weight FROM tool_edges WHERE src=? AND dst=?").get(src, dst);
|
|
147
|
+
const existing = db.prepare("SELECT weight, last_reinforced FROM tool_edges WHERE src=? AND dst=?").get(src, dst);
|
|
104
148
|
if (existing) {
|
|
105
|
-
const next = bump ? existing.weight + bump : Math.max(existing.weight, weight);
|
|
106
149
|
if (bump) {
|
|
107
150
|
db.prepare("UPDATE tool_edges SET weight=?, last_reinforced=? WHERE src=? AND dst=?")
|
|
108
|
-
.run(
|
|
151
|
+
.run(effectiveWeight(existing) + bump, now(), src, dst);
|
|
109
152
|
} else {
|
|
110
|
-
|
|
153
|
+
const next = Math.max(existing.weight, weight);
|
|
154
|
+
if (next !== existing.weight) {
|
|
155
|
+
db.prepare("UPDATE tool_edges SET weight=? WHERE src=? AND dst=?").run(next, src, dst);
|
|
156
|
+
}
|
|
111
157
|
}
|
|
112
158
|
} else {
|
|
113
159
|
// No structural floor for tools: a brand-new edge is worth exactly its
|
|
@@ -148,14 +194,14 @@ function allEdges() {
|
|
|
148
194
|
catch (e) { return []; }
|
|
149
195
|
}
|
|
150
196
|
|
|
151
|
-
// Tools that tend to follow `name`, strongest first. This
|
|
152
|
-
// signal: "you ran X — you usually run these next".
|
|
197
|
+
// Tools that tend to follow `name`, strongest first (read-time weights). This
|
|
198
|
+
// is the surfaced signal: "you ran X — you usually run these next".
|
|
153
199
|
function followers(name, limit = 5) {
|
|
154
200
|
const db = openDb();
|
|
155
201
|
if (!db || !name) return [];
|
|
156
202
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
203
|
+
const rows = db.prepare("SELECT dst AS name, weight, last_reinforced FROM tool_edges WHERE src=?").all(name);
|
|
204
|
+
return rankLive(rows, limit).map((r) => ({ name: r.name, weight: r.weight }));
|
|
159
205
|
} catch (e) { return []; }
|
|
160
206
|
}
|
|
161
207
|
|
|
@@ -164,8 +210,8 @@ function predecessors(name, limit = 5) {
|
|
|
164
210
|
const db = openDb();
|
|
165
211
|
if (!db || !name) return [];
|
|
166
212
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
213
|
+
const rows = db.prepare("SELECT src AS name, weight, last_reinforced FROM tool_edges WHERE dst=?").all(name);
|
|
214
|
+
return rankLive(rows, limit).map((r) => ({ name: r.name, weight: r.weight }));
|
|
169
215
|
} catch (e) { return []; }
|
|
170
216
|
}
|
|
171
217
|
|
|
@@ -206,7 +252,13 @@ function parseToolRuns(command) {
|
|
|
206
252
|
const out = [];
|
|
207
253
|
const cmd = String(command || "");
|
|
208
254
|
if (!/tool\s+(?:run|exec)\s+/i.test(cmd)) return out;
|
|
209
|
-
|
|
255
|
+
// B17: split on ALL shell composition operators — &&, ||, |, &, ;, newline.
|
|
256
|
+
// The single pipe was missed, so `tool run a … | tool run b …` parsed as one
|
|
257
|
+
// segment and only the first tool earned a follows-edge. Separators inside
|
|
258
|
+
// quoted spans are neutralized first so a pipe in an argument doesn't split
|
|
259
|
+
// (quoted content never reaches the shape anyway — shapeCommand masks it).
|
|
260
|
+
const masked = cmd.replace(/(["'])(?:\\.|(?!\1).)*\1/g, (q) => q.replace(/[;\n|&]/g, " "));
|
|
261
|
+
for (const seg of masked.split(/[;\n|&]+/)) {
|
|
210
262
|
const m = seg.match(/\btool\s+(?:run|exec)\s+["']?([a-z0-9][\w.-]*)["']?\s*(.*)$/i);
|
|
211
263
|
if (!m) continue;
|
|
212
264
|
out.push({ name: m[1], shape: shapeCommand(`${m[1]} ${m[2] || ""}`) });
|
|
@@ -227,7 +279,8 @@ function recordPackTool(pack, tool, opts = {}) {
|
|
|
227
279
|
try {
|
|
228
280
|
const row = db.prepare("SELECT weight, intent, last_reinforced FROM pack_tool_edges WHERE pack=? AND tool=? AND command=?").get(pack, tool, command);
|
|
229
281
|
if (row) {
|
|
230
|
-
|
|
282
|
+
// Bump settles accrued decay first (same invariant as addFollow).
|
|
283
|
+
const next = bump ? effectiveWeight(row) + bump : Math.max(row.weight, weight);
|
|
231
284
|
const intent = (opts.intent != null && opts.intent !== "") ? String(opts.intent).slice(0, 200) : row.intent;
|
|
232
285
|
db.prepare("UPDATE pack_tool_edges SET weight=?, intent=?, last_reinforced=? WHERE pack=? AND tool=? AND command=?")
|
|
233
286
|
.run(next, intent, bump ? now() : row.last_reinforced, pack, tool, command);
|
|
@@ -243,15 +296,16 @@ function recordPackTool(pack, tool, opts = {}) {
|
|
|
243
296
|
}
|
|
244
297
|
|
|
245
298
|
// Strongest contextual tool usages recorded under a pack, for surfacing in that
|
|
246
|
-
// pack's injected context. Ordered by
|
|
299
|
+
// pack's injected context. Ordered by read-time weight, newest first as a
|
|
247
300
|
// tiebreak. One row per distinct command shape.
|
|
248
301
|
function packTools(pack, limit = 2) {
|
|
249
302
|
const db = openDb();
|
|
250
303
|
if (!db || !pack) return [];
|
|
251
304
|
try {
|
|
252
|
-
|
|
253
|
-
"SELECT tool, command, intent, weight FROM pack_tool_edges WHERE pack=?
|
|
254
|
-
).all(pack
|
|
305
|
+
const rows = db.prepare(
|
|
306
|
+
"SELECT tool, command, intent, weight, last_reinforced FROM pack_tool_edges WHERE pack=?"
|
|
307
|
+
).all(pack);
|
|
308
|
+
return rankLive(rows, limit).map((r) => ({ tool: r.tool, command: r.command, intent: r.intent, weight: r.weight }));
|
|
255
309
|
} catch (e) { return []; }
|
|
256
310
|
}
|
|
257
311
|
|
|
@@ -260,9 +314,10 @@ function toolPacks(tool, limit = 5) {
|
|
|
260
314
|
const db = openDb();
|
|
261
315
|
if (!db || !tool) return [];
|
|
262
316
|
try {
|
|
263
|
-
|
|
264
|
-
"SELECT pack, command, intent, weight FROM pack_tool_edges WHERE tool=?
|
|
265
|
-
).all(tool
|
|
317
|
+
const rows = db.prepare(
|
|
318
|
+
"SELECT pack, command, intent, weight, last_reinforced FROM pack_tool_edges WHERE tool=?"
|
|
319
|
+
).all(tool);
|
|
320
|
+
return rankLive(rows, limit).map((r) => ({ pack: r.pack, command: r.command, intent: r.intent, weight: r.weight }));
|
|
266
321
|
} catch (e) { return []; }
|
|
267
322
|
}
|
|
268
323
|
|
|
@@ -280,53 +335,25 @@ function allPackToolEdges() {
|
|
|
280
335
|
catch (e) { return []; }
|
|
281
336
|
}
|
|
282
337
|
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// follows-edges and the pack↔tool
|
|
287
|
-
|
|
338
|
+
// Physically delete edges whose EFFECTIVE weight has faded under `pruneBelow` —
|
|
339
|
+
// a chain that fell out of use disappears instead of lingering as stale advice.
|
|
340
|
+
// Weights themselves are never rewritten (decay-on-read); this is pure garbage
|
|
341
|
+
// collection. Applies to both the tool→tool follows-edges and the pack↔tool
|
|
342
|
+
// contextual edges (same physics). Never-reinforced ensure-edges don't fade.
|
|
343
|
+
function prune({ halfLifeDays = DEFAULTS.halfLifeDays, pruneBelow = DEFAULTS.pruneBelow } = {}) {
|
|
288
344
|
const db = openDb();
|
|
289
|
-
if (!db) return {
|
|
345
|
+
if (!db) return { pruned: 0 };
|
|
290
346
|
const nowMs = Date.now();
|
|
291
|
-
|
|
292
|
-
let decayed = 0, pruned = 0;
|
|
293
|
-
const decayFactor = (lastReinforced) => {
|
|
294
|
-
if (!lastReinforced) return null;
|
|
295
|
-
const ageDays = (nowMs - Date.parse(lastReinforced)) / 86400000;
|
|
296
|
-
if (!(ageDays > 0)) return null;
|
|
297
|
-
return Math.exp(-ln2 * ageDays / Math.max(1, halfLifeDays));
|
|
298
|
-
};
|
|
347
|
+
let pruned = 0;
|
|
299
348
|
for (const e of allEdges()) {
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
const next = e.weight * factor;
|
|
303
|
-
if (next < pruneBelow) {
|
|
304
|
-
if (removeEdge(e.src, e.dst)) pruned++;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (Math.abs(next - e.weight) > 1e-6) {
|
|
308
|
-
try {
|
|
309
|
-
db.prepare("UPDATE tool_edges SET weight=? WHERE src=? AND dst=?").run(next, e.src, e.dst);
|
|
310
|
-
decayed++;
|
|
311
|
-
} catch (e2) {}
|
|
312
|
-
}
|
|
349
|
+
if (!e.last_reinforced) continue;
|
|
350
|
+
if (effectiveWeight(e, nowMs, halfLifeDays) < pruneBelow && removeEdge(e.src, e.dst)) pruned++;
|
|
313
351
|
}
|
|
314
352
|
for (const e of allPackToolEdges()) {
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
const next = e.weight * factor;
|
|
318
|
-
if (next < pruneBelow) {
|
|
319
|
-
if (removePackTool(e.pack, e.tool, e.command)) pruned++;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (Math.abs(next - e.weight) > 1e-6) {
|
|
323
|
-
try {
|
|
324
|
-
db.prepare("UPDATE pack_tool_edges SET weight=? WHERE pack=? AND tool=? AND command=?").run(next, e.pack, e.tool, e.command);
|
|
325
|
-
decayed++;
|
|
326
|
-
} catch (e2) {}
|
|
327
|
-
}
|
|
353
|
+
if (!e.last_reinforced) continue;
|
|
354
|
+
if (effectiveWeight(e, nowMs, halfLifeDays) < pruneBelow && removePackTool(e.pack, e.tool, e.command)) pruned++;
|
|
328
355
|
}
|
|
329
|
-
return {
|
|
356
|
+
return { pruned };
|
|
330
357
|
}
|
|
331
358
|
|
|
332
359
|
// Drop edges whose endpoints are no longer registered tools (a tool was removed
|
|
@@ -361,13 +388,14 @@ function pruneOrphans(toolsLib, packsLib) {
|
|
|
361
388
|
return removed;
|
|
362
389
|
}
|
|
363
390
|
|
|
364
|
-
// Nightly maintenance (called by the dream):
|
|
365
|
-
//
|
|
366
|
-
// { packsLib } to also prune pack↔tool edges whose pack was
|
|
391
|
+
// Nightly maintenance (called by the dream): drop faded and orphaned edges.
|
|
392
|
+
// No weight pass — decay is applied on read. Deterministic + safe — no model
|
|
393
|
+
// needed. Pass { packsLib } to also prune pack↔tool edges whose pack was
|
|
394
|
+
// removed.
|
|
367
395
|
function tend(toolsLib, opts = {}) {
|
|
368
|
-
const {
|
|
396
|
+
const { pruned } = prune(opts);
|
|
369
397
|
const orphaned = toolsLib ? pruneOrphans(toolsLib, opts.packsLib) : 0;
|
|
370
|
-
return {
|
|
398
|
+
return { pruned: pruned + orphaned, ...stats() };
|
|
371
399
|
}
|
|
372
400
|
|
|
373
401
|
function stats() {
|
|
@@ -384,7 +412,7 @@ module.exports = {
|
|
|
384
412
|
DEFAULTS, GRAPH_DB,
|
|
385
413
|
available, openDb,
|
|
386
414
|
addFollow, reinforceSequence, removeEdge, allEdges,
|
|
387
|
-
followers, predecessors,
|
|
415
|
+
followers, predecessors, effectiveWeight, prune, pruneOrphans, tend, stats,
|
|
388
416
|
shapeCommand, parseToolRuns, recordPackTool, packTools, toolPacks,
|
|
389
417
|
removePackTool, allPackToolEdges,
|
|
390
418
|
_resetForTest,
|
package/core/tools.js
CHANGED
|
@@ -30,6 +30,7 @@ const fs = require("fs");
|
|
|
30
30
|
const path = require("path");
|
|
31
31
|
const crypto = require("crypto");
|
|
32
32
|
const CONFIG_DIR = require("../config-dir");
|
|
33
|
+
const { atomicWriteFileSync } = require("./fsutil");
|
|
33
34
|
|
|
34
35
|
const TOOLS_DIR = process.env.TOOLS_DIR ? path.resolve(process.env.TOOLS_DIR) : path.join(CONFIG_DIR, "tools");
|
|
35
36
|
|
|
@@ -90,7 +91,7 @@ function readUsage() {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function writeUsage(u) {
|
|
93
|
-
try { ensureDir();
|
|
94
|
+
try { ensureDir(); atomicWriteFileSync(USAGE_FILE, JSON.stringify(u), { mode: 0o600 }); }
|
|
94
95
|
catch (e) { /* best-effort */ }
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -106,7 +107,7 @@ function writeState(name, s) {
|
|
|
106
107
|
const file = p.state || path.join(TOOLS_DIR, sanitizeName(name), "state.json");
|
|
107
108
|
try {
|
|
108
109
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
109
|
-
|
|
110
|
+
atomicWriteFileSync(file, JSON.stringify(s, null, 1), { mode: 0o600 });
|
|
110
111
|
} catch (e) { /* best-effort */ }
|
|
111
112
|
}
|
|
112
113
|
|
package/core/turn-observer.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
const { chatContext, currentAdapter } = require("./context");
|
|
14
14
|
const { send } = require("./io");
|
|
15
15
|
|
|
16
|
+
// Consecutive recall failures on one channel before the owner is alerted (B5).
|
|
17
|
+
const RECALL_FAILURE_ALERT_AFTER = 3;
|
|
18
|
+
|
|
16
19
|
function esc(s) {
|
|
17
20
|
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
18
21
|
}
|
|
@@ -71,8 +74,27 @@ function createTurnObserver({ state, store }) {
|
|
|
71
74
|
// 🧠 /recall + 🔧 /tooltrace banners, fired once the prompt bundle (and its
|
|
72
75
|
// recall metadata) exists — before the provider spawns, like v2.15.
|
|
73
76
|
const onRecallSurfaced = (metadata) => {
|
|
74
|
-
if (guarded) return;
|
|
75
77
|
const r = metadata;
|
|
78
|
+
// B5: recall fails OPEN (the turn proceeds without memory), so a persistent
|
|
79
|
+
// breakage is invisible unless someone counts. Track consecutive failures
|
|
80
|
+
// on channel state and tell the owner once per streak — a one-off transient
|
|
81
|
+
// stays quiet, an outage does not.
|
|
82
|
+
if (r && r.reason === "optional_recall_unavailable") {
|
|
83
|
+
state.recallFailureStreak = (state.recallFailureStreak || 0) + 1;
|
|
84
|
+
if (settings.showRecall) {
|
|
85
|
+
notify("banner:recall", "🧠 Recall FAILED this turn — I'm answering without pack/entity/tool memory. See bot log: [recall] builder failed.");
|
|
86
|
+
}
|
|
87
|
+
if (state.recallFailureStreak === RECALL_FAILURE_ALERT_AFTER) {
|
|
88
|
+
notify("alert:recall-failure", [
|
|
89
|
+
`⚠️ Memory recall has failed ${state.recallFailureStreak} turns in a row — I'm running without pack/entity/tool context.`,
|
|
90
|
+
r.detail ? `Last error: ${r.detail}` : null,
|
|
91
|
+
"Check the bot log for \"[recall] builder failed\". I'll stay quiet until it recovers or breaks again.",
|
|
92
|
+
].filter(Boolean).join("\n"));
|
|
93
|
+
}
|
|
94
|
+
} else if (r && !r.omitted) {
|
|
95
|
+
state.recallFailureStreak = 0;
|
|
96
|
+
}
|
|
97
|
+
if (guarded) return;
|
|
76
98
|
if (!r || r.omitted) return;
|
|
77
99
|
const fmt = (arr, icon) => (arr || []).map((x) => (x.why ? `${icon} <b>${esc(x.name)}</b> — ${esc(x.why)}` : `${icon} <b>${esc(x.name)}</b>`));
|
|
78
100
|
if (settings.showRecall) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js",
|
|
12
|
+
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js",
|
|
13
13
|
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"license": "MIT",
|
|
133
133
|
"type": "commonjs",
|
|
134
134
|
"engines": {
|
|
135
|
-
"node": ">=
|
|
135
|
+
"node": ">=24.0.0"
|
|
136
136
|
},
|
|
137
137
|
"dependencies": {
|
|
138
138
|
"node-cron": "^4.2.1",
|
|
@@ -67,4 +67,25 @@ assert.ok(/#### Stance/.test(outHeadline), "headline still shows Stance");
|
|
|
67
67
|
assert.ok(!outHeadline.includes("The ArgoCD sync for payments"), "headline does NOT inline State (unchanged legacy behavior)");
|
|
68
68
|
assert.ok(/not shown/.test(outHeadline), "headline still points to hidden sections");
|
|
69
69
|
|
|
70
|
+
// ── LOCKED hoist: a Stance line carrying LOCKED (uppercase) is pulled under
|
|
71
|
+
// a binding-constraints header in every mode; prose "locked" stays put ──
|
|
72
|
+
const lockedPack = {
|
|
73
|
+
...pack,
|
|
74
|
+
dir: "coders-africa",
|
|
75
|
+
sections: {
|
|
76
|
+
...pack.sections,
|
|
77
|
+
Stance: "Audience-first design.\nLOCKED: heading font is Marcellus, body font is Nunito.\nThe staging door is locked after 18:00.",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const outLockedX = formatPackForContext(lockedPack, packsLib, { extractive: true, matchedLines: [] });
|
|
81
|
+
assert.ok(/BINDING CONSTRAINTS \(LOCKED/.test(outLockedX), "extractive mode hoists LOCKED lines under the binding header");
|
|
82
|
+
assert.ok(/- LOCKED: heading font is Marcellus, body font is Nunito\./.test(outLockedX), "the LOCKED line is bulleted verbatim");
|
|
83
|
+
const bindingIdx = outLockedX.indexOf("BINDING CONSTRAINTS");
|
|
84
|
+
assert.ok(bindingIdx !== -1 && bindingIdx < outLockedX.indexOf("Audience-first design."), "binding block leads the Stance section");
|
|
85
|
+
assert.ok(outLockedX.includes("The staging door is locked after 18:00."), "lowercase 'locked' prose is not hoisted");
|
|
86
|
+
assert.strictEqual((outLockedX.match(/heading font is Marcellus/g) || []).length, 1, "hoisted line is moved, not duplicated");
|
|
87
|
+
assert.ok(/BINDING CONSTRAINTS \(LOCKED/.test(formatPackForContext(lockedPack, packsLib, { headline: true })), "headline mode hoists too");
|
|
88
|
+
assert.ok(/BINDING CONSTRAINTS \(LOCKED/.test(formatPackForContext(lockedPack, packsLib, {})), "full mode hoists too");
|
|
89
|
+
assert.ok(!/BINDING CONSTRAINTS/.test(out), "packs without LOCKED lines are untouched");
|
|
90
|
+
|
|
70
91
|
console.log("extractive injection OK");
|
|
@@ -15,6 +15,7 @@ fs.mkdirSync(process.env.OPEN_CLAUDIA_CONFIG_DIR, { recursive: true });
|
|
|
15
15
|
const {
|
|
16
16
|
MemoryMutationQueue,
|
|
17
17
|
MutationQueueError,
|
|
18
|
+
inspectLock,
|
|
18
19
|
} = require("./core/memory-mutation-queue");
|
|
19
20
|
const { atomicWriteFileSync } = require("./core/fsutil");
|
|
20
21
|
|
|
@@ -62,14 +63,12 @@ async function main() {
|
|
|
62
63
|
journalFile,
|
|
63
64
|
maxPending: 2,
|
|
64
65
|
lockPollMs: 5,
|
|
65
|
-
staleLockMs: 10,
|
|
66
66
|
});
|
|
67
67
|
const liveContender = new MemoryMutationQueue({
|
|
68
68
|
lockDir: liveLockDir,
|
|
69
69
|
journalFile,
|
|
70
70
|
maxPending: 2,
|
|
71
71
|
lockPollMs: 5,
|
|
72
|
-
staleLockMs: 10,
|
|
73
72
|
lockTimeoutMs: 50,
|
|
74
73
|
});
|
|
75
74
|
const liveMutation = liveOwner.enqueue("long-dream", async () => liveGate.promise);
|
|
@@ -92,11 +91,42 @@ async function main() {
|
|
|
92
91
|
lockDir: deadLockDir,
|
|
93
92
|
journalFile,
|
|
94
93
|
lockPollMs: 5,
|
|
95
|
-
staleLockMs: 10,
|
|
96
94
|
lockTimeoutMs: 200,
|
|
97
95
|
});
|
|
98
96
|
assert.strictEqual(await deadRecovery.enqueue("recover-dead-lock", async () => "recovered"), "recovered");
|
|
99
97
|
|
|
98
|
+
// Container pid-1 reuse: a lock left by a CRASHED incarnation of this same
|
|
99
|
+
// pid (different bootId) must be broken immediately — pid-liveness alone
|
|
100
|
+
// reads it as held forever, which starved reviewers on restarting pods.
|
|
101
|
+
const rebootLockDir = path.join(root, "reboot-stale.lock");
|
|
102
|
+
fs.mkdirSync(rebootLockDir);
|
|
103
|
+
fs.writeFileSync(path.join(rebootLockDir, "owner.json"),
|
|
104
|
+
JSON.stringify({ token: "prev", pid: process.pid, bootId: "previous-incarnation" }));
|
|
105
|
+
const rebootRecovery = new MemoryMutationQueue({
|
|
106
|
+
lockDir: rebootLockDir,
|
|
107
|
+
journalFile,
|
|
108
|
+
lockPollMs: 5,
|
|
109
|
+
lockTimeoutMs: 200,
|
|
110
|
+
});
|
|
111
|
+
assert.strictEqual(await rebootRecovery.enqueue("recover-reboot-lock", async () => "recovered"), "recovered",
|
|
112
|
+
"a lock from a previous incarnation sharing our pid is broken immediately");
|
|
113
|
+
assert.strictEqual(
|
|
114
|
+
inspectLock(rebootLockDir).held, false,
|
|
115
|
+
"inspectLock reports a released lock as not held");
|
|
116
|
+
|
|
117
|
+
// Orphan: mkdir succeeded but owner.json never landed (crash mid-acquire).
|
|
118
|
+
const orphanLockDir = path.join(root, "orphan.lock");
|
|
119
|
+
fs.mkdirSync(orphanLockDir);
|
|
120
|
+
fs.utimesSync(orphanLockDir, old, old);
|
|
121
|
+
assert.strictEqual(inspectLock(orphanLockDir).stale, true, "an aged ownerless lock dir reads as stale");
|
|
122
|
+
const orphanRecovery = new MemoryMutationQueue({
|
|
123
|
+
lockDir: orphanLockDir,
|
|
124
|
+
journalFile,
|
|
125
|
+
lockPollMs: 5,
|
|
126
|
+
lockTimeoutMs: 200,
|
|
127
|
+
});
|
|
128
|
+
assert.strictEqual(await orphanRecovery.enqueue("recover-orphan-lock", async () => "recovered"), "recovered");
|
|
129
|
+
|
|
100
130
|
const failureQueue = new MemoryMutationQueue({
|
|
101
131
|
lockDir: path.join(root, "failure.lock"),
|
|
102
132
|
journalFile,
|
|
@@ -122,6 +122,11 @@ async function immutableSuccessProbe() {
|
|
|
122
122
|
assert.strictEqual(contexts[1].permissionMode, "plan");
|
|
123
123
|
assert.strictEqual(contexts[1].maxTurns, 1);
|
|
124
124
|
assert.strictEqual(contexts[1].timeoutMs, contexts[0].timeoutMs);
|
|
125
|
+
assert.strictEqual(contexts[0].purpose, "compact-summary");
|
|
126
|
+
assert.strictEqual(contexts[0].recallQuery, null, "summary stage carries no recall query");
|
|
127
|
+
assert.strictEqual(contexts[1].purpose, "compact-seed");
|
|
128
|
+
assert.ok(String(contexts[1].recallQuery || "").includes("A sufficiently detailed condensed continuation"),
|
|
129
|
+
"seed recall query carries the condensed brief, not seed-prompt boilerplate");
|
|
125
130
|
const restrictedEnv = runner.decorateForegroundInvocation(
|
|
126
131
|
{ env: { FIXTURE_SAFE: "1" } },
|
|
127
132
|
contexts[0],
|
|
@@ -164,6 +164,9 @@ async function probe() {
|
|
|
164
164
|
assert.strictEqual(omitted.userPrompt, "RECALL FAILURE USER SENTINEL");
|
|
165
165
|
assert.strictEqual(omitted.recallMetadata.status, "omitted");
|
|
166
166
|
assert.strictEqual(omitted.recallMetadata.omitted, true);
|
|
167
|
+
assert.strictEqual(omitted.recallMetadata.reason, "optional_recall_unavailable");
|
|
168
|
+
assert.strictEqual(omitted.recallMetadata.detail, "fixture optional recall failure",
|
|
169
|
+
"the swallowed recall error surfaces in metadata for the owner alert");
|
|
167
170
|
assert.ok(omitted.coreInstructions.includes("## Delivery"));
|
|
168
171
|
assert.ok(omitted.dynamicContext.includes("## Runtime state"));
|
|
169
172
|
|
|
@@ -99,7 +99,54 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
99
99
|
// ids all forced fresh) and the refused prompt is auto-retried exactly ONCE in
|
|
100
100
|
// a fresh session behind a ♻️ notice. A refusal on already-fresh context gets
|
|
101
101
|
// no retry (the new message itself is the problem); capture runs never retry.
|
|
102
|
-
|
|
102
|
+
// 3.0.45 approved by Sumeet 2026-07-20 ("Ok so you ready to implement?"): first
|
|
103
|
+
// fix batch from the 2026-07-20 blind code review. Recall-graph decay moves to
|
|
104
|
+
// decay-on-read (the nightly rewrite compounded — a 60d half-life emptied in
|
|
105
|
+
// ~10 nights; one-time user_version 0→1 restamp, dream-tunable half-life);
|
|
106
|
+
// keyring/tool-state/tuning writes become crash-safe (atomic temp+fsync+rename,
|
|
107
|
+
// keyring .bak snapshot with read-through repair); recall failures alert after
|
|
108
|
+
// 3 consecutive memoryless turns instead of degrading silently; the recall
|
|
109
|
+
// pre-gate strips pleasantry prefixes so "ok deploy … now" recalls; tool-run
|
|
110
|
+
// creds are preflighted BEFORE an approval token is consumed (a missing key no
|
|
111
|
+
// longer burns the human's Approve); tool-graph parsing splits on | and &;
|
|
112
|
+
// raw NUL bytes in discoverer.js/state.js become \u0000 escapes; README
|
|
113
|
+
// corrected — discoverer is the shipped default engine, classic the opt-out.
|
|
114
|
+
// 3.0.45 second wave folded in per Sumeet 2026-07-20 ("i think just wok
|
|
115
|
+
// through all these features and fold them into .45?"): the memory-substrate
|
|
116
|
+
// revival. Root cause: pods on node:20 have no node:sqlite, so pack FTS, the
|
|
117
|
+
// recall graph and episodes silently never initialized — rahilcoder ran 16
|
|
118
|
+
// days with seedsTotal=0 while repeating corrected branding mistakes, and its
|
|
119
|
+
// reviewer starved 87% of runs on a stale mutation lock (container pid-1
|
|
120
|
+
// reuse made a crashed incarnation's lock look alive forever). Ships:
|
|
121
|
+
// Dockerfile.base FROM node:24-slim (+ CI build image node:24); a /doctor
|
|
122
|
+
// memory-health section (substrate hard-fail, graph stats, 24h recall
|
|
123
|
+
// pipeline, reviewer health, mutation lock, deferred reviews, corpus caps,
|
|
124
|
+
// disk) with a deduped boot-time substrate alert and a post-upgrade doctor
|
|
125
|
+
// gate on all three /upgrade paths; a pure-JS lexical seed floor under
|
|
126
|
+
// matchPacks/matchEntities (recall degrades to keyword matching, never to
|
|
127
|
+
// OFF — parity-tested with and without the sim-no-sqlite shim); the
|
|
128
|
+
// mutation-queue lock gains a boot id + ownerless-lock grace so pid-1 reuse
|
|
129
|
+
// can't wedge it, plus a reviewer retry+defer seatbelt; LOCKED Stance lines
|
|
130
|
+
// hoist under a BINDING CONSTRAINTS header and the reviewer must carry them
|
|
131
|
+
// verbatim and journal violations; compaction recall is fixed (summary
|
|
132
|
+
// meta-turn skips recall; the fresh seed matches on condensed brief + open
|
|
133
|
+
// task titles, not prompt boilerplate).
|
|
134
|
+
// 3.1.0 approved by Sumeet 2026-07-20 ("yes that's fine but if in hard boot
|
|
135
|
+
// loop it shoyuld give the user a way to get back online and diagnose or fall
|
|
136
|
+
// back into a safe mode or something to have it fixed or set up or downgrade,
|
|
137
|
+
// actually i think that's improtant as well to have a downgrade, i think add
|
|
138
|
+
// this all in and let's bump the minor to 3.1.0"): the substrate-revival
|
|
139
|
+
// fold graduates to a minor because boot semantics change. node:sqlite is now
|
|
140
|
+
// REQUIRED — a missing substrate boots into SAFE MODE (adapters + diagnostic
|
|
141
|
+
// slash commands only; no agent turns, crons, or memory writes) instead of
|
|
142
|
+
// silently degrading; MEMORY_ALLOW_DEGRADED=1 or a one-shot /safemode
|
|
143
|
+
// continue is the explicit keyword-only escape hatch. A boot crash loop (3
|
|
144
|
+
// consecutive boots dying before 5min stable uptime) also lands in safe mode
|
|
145
|
+
// so the operator keeps a live chat to rescue from instead of a dead pod.
|
|
146
|
+
// New /downgrade rolls back to the previously-running version from boot
|
|
147
|
+
// history (strict semver-validated target); /upgrade//downgrade clear the
|
|
148
|
+
// crash counter as deliberate fix attempts. package.json engines >=24.
|
|
149
|
+
assert.strictEqual(pkg.version, "3.1.0", "release version must remain unchanged without explicit approval");
|
|
103
150
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
104
151
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
105
152
|
}
|
package/test-provider-recall.js
CHANGED
|
@@ -253,6 +253,39 @@ async function main() {
|
|
|
253
253
|
recall.getEngine = originalGetEngine;
|
|
254
254
|
assert.strictEqual(admittedProvider, "codex");
|
|
255
255
|
|
|
256
|
+
// ── compaction recall policy: the summary meta-turn skips recall entirely;
|
|
257
|
+
// the seed turn matches on the runner-supplied recallQuery (condensed
|
|
258
|
+
// brief + task titles), never the seed prompt's boilerplate ──
|
|
259
|
+
const summarySkip = await systemPrompt.buildOptionalRecallContext(
|
|
260
|
+
"Summarize this conversation for a fresh compacted continuation.",
|
|
261
|
+
{
|
|
262
|
+
state: mutableState,
|
|
263
|
+
runContext: { provider: "claude", purpose: "compact-summary", project: { name: "project-a", dir: root }, sessionId: null, runId: "compact-sum" },
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
assert.strictEqual(summarySkip.metadata.status, "omitted", "summary turn skips recall");
|
|
267
|
+
assert.strictEqual(summarySkip.metadata.reason, "compaction_meta_turn");
|
|
268
|
+
assert.strictEqual(summarySkip.dynamicContext, "");
|
|
269
|
+
|
|
270
|
+
let seedQueryText = null;
|
|
271
|
+
recall.getEngine = () => ({
|
|
272
|
+
name: "fixture",
|
|
273
|
+
async run(ctx) {
|
|
274
|
+
seedQueryText = ctx.userText;
|
|
275
|
+
return { packBlock: "", entityBlock: "", toolBlock: "", episodeBlock: "", packMatches: [], entityMatches: [] };
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
await systemPrompt.buildOptionalRecallContext(
|
|
279
|
+
"This is a compacted continuation of a previous Open Claudia session. Treat the following summary as the prior conversation context.",
|
|
280
|
+
{
|
|
281
|
+
state: mutableState,
|
|
282
|
+
runContext: { provider: "claude", purpose: "compact-seed", recallQuery: "coders africa branding fonts\nFinalize v3.0.45", project: { name: "project-a", dir: root }, sessionId: null, runId: "compact-seed" },
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
recall.getEngine = originalGetEngine;
|
|
286
|
+
assert.strictEqual(seedQueryText, "coders africa branding fonts\nFinalize v3.0.45",
|
|
287
|
+
"seed recall matches on recallQuery, not the seed prompt");
|
|
288
|
+
|
|
256
289
|
for (const file of ["core/recall-filter.js", "core/recall/discoverer.js", "core/recall/warm-walker.js"]) {
|
|
257
290
|
const source = fs.readFileSync(path.join(__dirname, file), "utf8");
|
|
258
291
|
assert.ok(!source.includes("spawnSubagent"), `${file} no longer uses the compatibility sub-agent path`);
|
|
@@ -53,6 +53,14 @@ assert.strictEqual(disc.recallTier("restart bot", 3), "seeds", "short command
|
|
|
53
53
|
assert.strictEqual(disc.recallTier("show fup logs", 2), "seeds", "≤4 words → seeds-only tier");
|
|
54
54
|
assert.strictEqual(disc.recallTier("yes we need to see the 100", 2), "full", "substantive yes-prefixed turn → full");
|
|
55
55
|
assert.strictEqual(disc.recallTier("how do I deploy the mobile app", 4), "full", "substantive → full");
|
|
56
|
+
// B2: a pleasantry prefix is stripped BEFORE tiering, so a short real request
|
|
57
|
+
// wearing an ack is no longer swallowed by the ≤4-word pleasantry skip.
|
|
58
|
+
assert.strictEqual(disc.recallTier("ok deploy kazee now", 3), "seeds", "ack-prefixed 4-word command → seeds, not skip");
|
|
59
|
+
assert.strictEqual(disc.recallTier("yes deploy it", 4), "seeds", "approval + command → seeds, not skip");
|
|
60
|
+
assert.strictEqual(disc.recallTier("thanks! restart euvy bot", 3), "seeds", "thanks-prefixed command → seeds, not skip");
|
|
61
|
+
assert.strictEqual(disc.needsRecall("ok deploy kazee now", 0), true, "prefixed command needs recall");
|
|
62
|
+
assert.strictEqual(disc.recallTier("ok thanks sounds good perfect", 5), "skip", "stacked pure pleasantries skip at any length");
|
|
63
|
+
assert.strictEqual(disc.recallTier("morning check the kazee deploys", 2), "seeds", "greeting stripped → remainder tiers as a short command");
|
|
56
64
|
|
|
57
65
|
// --- shared stub corpus + helpers ---
|
|
58
66
|
const packs = {
|