@inetafrica/open-claudia 2.6.40 → 2.6.42
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 +3 -0
- package/core/dream.js +41 -16
- package/core/recall/read-signal.js +32 -0
- package/core/runner.js +19 -1
- package/package.json +4 -3
- package/test-pack-nesting.js +15 -0
- package/test-read-signal.js +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.42
|
|
4
|
+
- **Read-time `📖 Recalled my notes on …` now fires for direct file reads, not just the CLI.** The read-side recall banner — and the Hebbian co-use signal that reinforces the recall graph — was wired only to shell commands, watching for `open-claudia pack show <dir>` / `entity show <slug>`. When the agent opened one of its own notes by reading the raw `…/packs/<dir>/PACK.md` or `…/entities/<slug>.md` straight through the Read tool, the detector never saw it: no banner, and the open never counted as co-use. A new `core/recall/read-signal.js` resolves a file path to its memory node (mirroring the existing Write/Edit path-detection), and `runner.js` calls it on Read tool-uses across the Claude and Cursor backends — deduped against the CLI path via the shared notify key, so reading the same node both ways still announces once. Adds `test-read-signal.js`.
|
|
5
|
+
|
|
3
6
|
## v2.6.37
|
|
4
7
|
- **`/recall [on|off]` — watch recall work.** A per-chat debug toggle that, when on, posts a short `🧠 Recall this turn` line just before each reply listing the packs/entities that surfaced — and on the **discoverer** engine, the one-line why-bullet for each. On a gated turn (pre-gate skipped recall) it says so; on a quiet turn with no matches it stays silent. The engines now return a `why` map + `gated` flag, `promptWithDynamicContext` captures a recall summary into the per-turn `consumeLastInjected()` buffer, and `runner.js` renders it when `settings.showRecall` is set. Off by default; flip with `/recall` (buttons) or `/recall on`.
|
|
5
8
|
|
package/core/dream.js
CHANGED
|
@@ -41,6 +41,16 @@ const DREAM_MODEL = pickDreamModel();
|
|
|
41
41
|
// reasoning effort by default. Overridable via DREAM_EFFORT (low|medium|high|xhigh|max).
|
|
42
42
|
const DREAM_EFFORT = process.env.DREAM_EFFORT || "max";
|
|
43
43
|
const DREAM_CRON = process.env.DREAM_CRON || "0 4 * * *";
|
|
44
|
+
// Consolidation model timeout. The model weighs the WHOLE corpus in one pass,
|
|
45
|
+
// so a fixed budget that was fine at 30 packs starves at 130 (the timeout that
|
|
46
|
+
// motivated this). Floor 20m, +6s per pack, capped at 40m; DREAM_TIMEOUT_MS
|
|
47
|
+
// (ms) overrides entirely. Even if it still times out, the deterministic phases
|
|
48
|
+
// (prefix nesting, graph tend) run regardless — the model pass is best-effort.
|
|
49
|
+
function dreamTimeoutMs(packCount = 0) {
|
|
50
|
+
const override = Number(process.env.DREAM_TIMEOUT_MS);
|
|
51
|
+
if (override > 0) return override;
|
|
52
|
+
return Math.min(20 * 60 * 1000 + packCount * 6000, 40 * 60 * 1000);
|
|
53
|
+
}
|
|
44
54
|
const MAX_PACK_CHARS = 2500;
|
|
45
55
|
const MAX_ENTITY_CHARS = 900;
|
|
46
56
|
const LIMITS = {
|
|
@@ -277,7 +287,7 @@ function applyDream(decision, backupRoot) {
|
|
|
277
287
|
const lines = [];
|
|
278
288
|
const gone = new Set(); // dirs/slugs removed this run
|
|
279
289
|
|
|
280
|
-
for (const m of decision.merges) {
|
|
290
|
+
for (const m of decision.merges || []) {
|
|
281
291
|
try {
|
|
282
292
|
const into = m?.into && packs.readPack(m.into);
|
|
283
293
|
const from = [].concat(m?.from || []).filter((d) => d && d !== m.into && !gone.has(d) && packs.readPack(d));
|
|
@@ -312,7 +322,7 @@ function applyDream(decision, backupRoot) {
|
|
|
312
322
|
} catch (e) { console.warn(`[dream] merge failed: ${e.message}`); }
|
|
313
323
|
}
|
|
314
324
|
|
|
315
|
-
for (const u of decision.umbrellas) {
|
|
325
|
+
for (const u of decision.umbrellas || []) {
|
|
316
326
|
try {
|
|
317
327
|
const dir = packs.slugify(u?.dir || u?.name);
|
|
318
328
|
if (!dir || gone.has(dir)) continue;
|
|
@@ -345,7 +355,7 @@ function applyDream(decision, backupRoot) {
|
|
|
345
355
|
} catch (e) { console.warn(`[dream] umbrella failed: ${e.message}`); }
|
|
346
356
|
}
|
|
347
357
|
|
|
348
|
-
for (const p of decision.parents) {
|
|
358
|
+
for (const p of decision.parents || []) {
|
|
349
359
|
try {
|
|
350
360
|
const child = p?.pack && !gone.has(p.pack) && packs.readPack(p.pack);
|
|
351
361
|
if (!child || !p.parent || gone.has(p.parent) || !packs.readPack(p.parent)) continue;
|
|
@@ -358,7 +368,7 @@ function applyDream(decision, backupRoot) {
|
|
|
358
368
|
} catch (e) { console.warn(`[dream] parent failed: ${e.message}`); }
|
|
359
369
|
}
|
|
360
370
|
|
|
361
|
-
for (const r of decision.retag) {
|
|
371
|
+
for (const r of decision.retag || []) {
|
|
362
372
|
try {
|
|
363
373
|
if (!r?.pack || gone.has(r.pack) || !packs.readPack(r.pack)) continue;
|
|
364
374
|
const desc = typeof r.description === "string" ? r.description.trim() : "";
|
|
@@ -374,7 +384,7 @@ function applyDream(decision, backupRoot) {
|
|
|
374
384
|
} catch (e) { console.warn(`[dream] retag failed: ${e.message}`); }
|
|
375
385
|
}
|
|
376
386
|
|
|
377
|
-
for (const em of decision.entity_merges) {
|
|
387
|
+
for (const em of decision.entity_merges || []) {
|
|
378
388
|
try {
|
|
379
389
|
const into = em?.into && entities.readEntity(em.into);
|
|
380
390
|
const from = [].concat(em?.from || []).filter((s) => s && s !== em.into && !gone.has(s) && entities.readEntity(s));
|
|
@@ -397,7 +407,7 @@ function applyDream(decision, backupRoot) {
|
|
|
397
407
|
} catch (e) { console.warn(`[dream] entity merge failed: ${e.message}`); }
|
|
398
408
|
}
|
|
399
409
|
|
|
400
|
-
for (const en of decision.entity_notes) {
|
|
410
|
+
for (const en of decision.entity_notes || []) {
|
|
401
411
|
try {
|
|
402
412
|
const ent = en?.entity && !gone.has(en.entity) && entities.readEntity(en.entity);
|
|
403
413
|
if (!ent || typeof en.notes !== "string" || !en.notes.trim()) continue;
|
|
@@ -736,14 +746,26 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
736
746
|
|
|
737
747
|
_dreaming = true;
|
|
738
748
|
try {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
749
|
+
// Best-effort consolidation: on a large corpus this call can time out or
|
|
750
|
+
// return unreadable JSON. That must NOT abort the dream — the deterministic
|
|
751
|
+
// phases below (prefix nesting, graph tend) need no model and are the
|
|
752
|
+
// load-bearing cleanup. Catch any model failure, fall back to an empty
|
|
753
|
+
// decision (applyDream no-ops), note it for the report, and carry on.
|
|
754
|
+
let decision = null;
|
|
755
|
+
let modelFailNote = "";
|
|
756
|
+
try {
|
|
757
|
+
const { text } = await spawnSubagent(buildDreamPrompt(), {
|
|
758
|
+
model: DREAM_MODEL,
|
|
759
|
+
effort: DREAM_EFFORT,
|
|
760
|
+
timeoutMs: dreamTimeoutMs(packCount),
|
|
761
|
+
systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
|
|
762
|
+
});
|
|
763
|
+
decision = parseDream(text);
|
|
764
|
+
if (!decision) modelFailNote = "the AI consolidation step returned unreadable output";
|
|
765
|
+
} catch (e) {
|
|
766
|
+
modelFailNote = `the AI consolidation step didn't finish (${e.message})`;
|
|
767
|
+
}
|
|
768
|
+
if (!decision) decision = { report: "" };
|
|
747
769
|
|
|
748
770
|
const backupRoot = makeBackupRoot();
|
|
749
771
|
const applied = applyDream(decision, backupRoot);
|
|
@@ -754,7 +776,10 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
754
776
|
try { nestLines = nestPrefixFamilies(backupRoot).lines; }
|
|
755
777
|
catch (e) { console.warn(`[dream] nesting pass failed: ${e.message}`); }
|
|
756
778
|
const consolidationLines = applied.concat(nestLines);
|
|
757
|
-
|
|
779
|
+
let report = decision.report || (consolidationLines.length > 0 ? "Tidied up my memory overnight." : "");
|
|
780
|
+
if (modelFailNote) {
|
|
781
|
+
report = `Heads up — ${modelFailNote}, so I skipped the AI-led merges this round but still ran the automatic filing + graph upkeep.${report ? " " + report : ""}`;
|
|
782
|
+
}
|
|
758
783
|
|
|
759
784
|
// Phase 2: self-improvement introspection. Reviews the day's seeds, reads
|
|
760
785
|
// its own code/memory READ-ONLY (allowedTools whitelist), promotes lessons
|
|
@@ -860,7 +885,7 @@ function initDream(adapters) {
|
|
|
860
885
|
}
|
|
861
886
|
|
|
862
887
|
module.exports = {
|
|
863
|
-
runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers, nestPrefixFamilies,
|
|
888
|
+
runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers, nestPrefixFamilies, dreamTimeoutMs,
|
|
864
889
|
buildIntrospectionPrompt, parseIntrospection, applyIntrospection, writeDreamReport,
|
|
865
890
|
enabled, summaryEnabled, introspectEnabled, selfApplyEnabled,
|
|
866
891
|
DREAM_CRON, DREAM_MODEL, DREAM_EFFORT, PROMOTE_MIN_PROJECTS,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Read-time recall signal: resolve a file path to the memory node it
|
|
2
|
+
// represents — a context pack (…/packs/<dir>/PACK.md) or an entity note
|
|
3
|
+
// (…/entities/<slug>.md) — or null if it's neither.
|
|
4
|
+
//
|
|
5
|
+
// The runner fires "📖 Recalled my notes on …" and reinforces the recall
|
|
6
|
+
// co-use graph when the agent OPENS one of its own notes. That detection used
|
|
7
|
+
// to watch only shell commands (`open-claudia pack show <dir>` / `entity show
|
|
8
|
+
// <slug>`), so reading the raw .md straight through the Read tool was invisible
|
|
9
|
+
// to both the banner and the graph. This mirrors the Write/Edit path-detection
|
|
10
|
+
// so a direct read counts the same as a CLI read. Best-effort: it returns a
|
|
11
|
+
// display name when the doc is on disk and falls back to the id otherwise.
|
|
12
|
+
const packsLib = require("../packs");
|
|
13
|
+
const entitiesLib = require("../entities");
|
|
14
|
+
|
|
15
|
+
function recallNodeFromPath(filePath) {
|
|
16
|
+
if (!filePath) return null;
|
|
17
|
+
try {
|
|
18
|
+
const packDir = packsLib.packNameFromPath(filePath);
|
|
19
|
+
if (packDir) {
|
|
20
|
+
const pack = packsLib.readPack(packDir);
|
|
21
|
+
return { kind: "pack", id: packDir, name: (pack && (pack.name || pack.dir)) || packDir };
|
|
22
|
+
}
|
|
23
|
+
const entSlug = entitiesLib.entityNameFromPath(filePath);
|
|
24
|
+
if (entSlug) {
|
|
25
|
+
const ent = entitiesLib.readEntity(entSlug);
|
|
26
|
+
return { kind: "entity", id: entSlug, name: (ent && (ent.name || ent.slug)) || entSlug };
|
|
27
|
+
}
|
|
28
|
+
} catch (e) { /* best-effort: announcements never block a turn */ }
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { recallNodeFromPath };
|
package/core/runner.js
CHANGED
|
@@ -28,6 +28,7 @@ const skillsLib = require("./skills");
|
|
|
28
28
|
const packsLib = require("./packs");
|
|
29
29
|
const entitiesLib = require("./entities");
|
|
30
30
|
const packReview = require("./pack-review");
|
|
31
|
+
const { recallNodeFromPath } = require("./recall/read-signal");
|
|
31
32
|
const {
|
|
32
33
|
appendUsageRecord,
|
|
33
34
|
loadUsageHistory,
|
|
@@ -897,6 +898,20 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
897
898
|
} catch (e) { /* announcements are best-effort */ }
|
|
898
899
|
};
|
|
899
900
|
|
|
901
|
+
// The same 📖 signal when the agent opens a note with the Read tool instead
|
|
902
|
+
// of the CLI: noteRecallFromShell only sees shell commands, so a raw-file
|
|
903
|
+
// read of a pack/entity would otherwise be invisible to both the banner and
|
|
904
|
+
// the co-use graph. Deduped against the CLI path via the shared notifySkill
|
|
905
|
+
// key, so reading the same node both ways still announces only once.
|
|
906
|
+
const noteRecallFromReadPath = (filePath) => {
|
|
907
|
+
try {
|
|
908
|
+
const node = recallNodeFromPath(filePath);
|
|
909
|
+
if (!node) return;
|
|
910
|
+
openedThisTurn.add(`${node.kind}:${node.id}`);
|
|
911
|
+
notifySkill(`recall:${node.kind}:${node.id}`, `📖 Recalled my notes on: ${node.name}`);
|
|
912
|
+
} catch (e) { /* announcements are best-effort */ }
|
|
913
|
+
};
|
|
914
|
+
|
|
900
915
|
let args;
|
|
901
916
|
try {
|
|
902
917
|
args = await buildClaudeArgs(prompt, opts);
|
|
@@ -1009,6 +1024,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1009
1024
|
else currentToolDetail = "";
|
|
1010
1025
|
noteSkillToolUse(block.name, input);
|
|
1011
1026
|
if (block.name === "Bash" && input.command) noteRecallFromShell(input.command);
|
|
1027
|
+
else if (block.name === "Read") noteRecallFromReadPath(input.file_path || input.filePath);
|
|
1012
1028
|
}
|
|
1013
1029
|
}
|
|
1014
1030
|
}
|
|
@@ -1021,7 +1037,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1021
1037
|
if (a.command) noteRecallFromShell(a.command);
|
|
1022
1038
|
} else if (tc.readToolCall) {
|
|
1023
1039
|
currentTool = "Read"; toolUses.push("Read");
|
|
1024
|
-
|
|
1040
|
+
const readPath = tc.readToolCall.args?.path;
|
|
1041
|
+
currentToolDetail = (readPath || "").split("/").slice(-2).join("/");
|
|
1042
|
+
noteRecallFromReadPath(readPath);
|
|
1025
1043
|
} else if (tc.editToolCall) {
|
|
1026
1044
|
currentTool = "Edit"; toolUses.push("Edit");
|
|
1027
1045
|
currentToolDetail = (tc.editToolCall.args?.filePath || "").split("/").slice(-2).join("/");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.42",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee 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
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js"
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"test-ability-tiers.js",
|
|
43
43
|
"test-ability-merge-guard.js",
|
|
44
44
|
"test-learning-e2e.js",
|
|
45
|
-
"test-pack-nesting.js"
|
|
45
|
+
"test-pack-nesting.js",
|
|
46
|
+
"test-read-signal.js"
|
|
46
47
|
],
|
|
47
48
|
"keywords": [
|
|
48
49
|
"claude",
|
package/test-pack-nesting.js
CHANGED
|
@@ -77,6 +77,21 @@ assert.ok(res.lines.some((l) => l.includes("billing")), "nest pass announces the
|
|
|
77
77
|
const again = dream.nestPrefixFamilies(null);
|
|
78
78
|
assert.strictEqual(again.filed, 0, "second pass files nothing new");
|
|
79
79
|
|
|
80
|
+
// ── dream resilience: the deterministic cleanup must not depend on the model ──
|
|
81
|
+
// applyDream tolerates an empty decision (the fallback used when the AI
|
|
82
|
+
// consolidation step times out / returns garbage) instead of throwing.
|
|
83
|
+
assert.deepStrictEqual(dream.applyDream({}, null), [], "empty decision applies nothing, throws nothing");
|
|
84
|
+
assert.deepStrictEqual(dream.applyDream({ report: "" }, null), [], "report-only fallback decision is safe");
|
|
85
|
+
|
|
86
|
+
// timeout scales with corpus: floor 20m, grows per pack, capped at 40m, env override.
|
|
87
|
+
assert.strictEqual(dream.dreamTimeoutMs(0), 20 * 60 * 1000, "floor is 20 minutes");
|
|
88
|
+
assert.ok(dream.dreamTimeoutMs(130) > dream.dreamTimeoutMs(30), "more packs → longer budget");
|
|
89
|
+
assert.strictEqual(dream.dreamTimeoutMs(100000), 40 * 60 * 1000, "capped at 40 minutes");
|
|
90
|
+
const prev = process.env.DREAM_TIMEOUT_MS;
|
|
91
|
+
process.env.DREAM_TIMEOUT_MS = "777000";
|
|
92
|
+
assert.strictEqual(dream.dreamTimeoutMs(50), 777000, "DREAM_TIMEOUT_MS overrides entirely");
|
|
93
|
+
if (prev === undefined) delete process.env.DREAM_TIMEOUT_MS; else process.env.DREAM_TIMEOUT_MS = prev;
|
|
94
|
+
|
|
80
95
|
// ── versioned-only fold (FTS-gated): a versioned proposal folds into its pack,
|
|
81
96
|
// a non-versioned near-duplicate does NOT. Skipped when node:sqlite is absent. ──
|
|
82
97
|
if (packs.reindex()) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Read-time recall signal: a Read of a pack/entity .md must resolve to the
|
|
2
|
+
// right memory node so the runner can fire "📖 Recalled my notes on …" (and
|
|
3
|
+
// reinforce the co-use graph) even when the agent reads the raw file directly
|
|
4
|
+
// instead of going through `open-claudia pack show` / `entity show`.
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "read-signal-"));
|
|
11
|
+
process.env.PACKS_DIR = path.join(tmp, "packs");
|
|
12
|
+
process.env.ENTITIES_DIR = path.join(tmp, "entities");
|
|
13
|
+
|
|
14
|
+
const packs = require("./core/packs");
|
|
15
|
+
const entities = require("./core/entities");
|
|
16
|
+
const { recallNodeFromPath } = require("./core/recall/read-signal");
|
|
17
|
+
|
|
18
|
+
// ── a pack PACK.md resolves to its pack node (display name preferred) ──
|
|
19
|
+
packs.createPack({ dir: "billing-invoices", name: "Billing Invoices", description: "invoice generation" });
|
|
20
|
+
const packPath = path.join(process.env.PACKS_DIR, "billing-invoices", "PACK.md");
|
|
21
|
+
assert.deepStrictEqual(
|
|
22
|
+
recallNodeFromPath(packPath),
|
|
23
|
+
{ kind: "pack", id: "billing-invoices", name: "Billing Invoices" },
|
|
24
|
+
"pack path → pack node carrying the display name"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// ── an entity .md resolves to its entity node ──
|
|
28
|
+
entities.upsertEntity({ name: "David Omondi", type: "person", description: "store keeper" });
|
|
29
|
+
const entPath = path.join(process.env.ENTITIES_DIR, "david-omondi.md");
|
|
30
|
+
assert.deepStrictEqual(
|
|
31
|
+
recallNodeFromPath(entPath),
|
|
32
|
+
{ kind: "entity", id: "david-omondi", name: "David Omondi" },
|
|
33
|
+
"entity path → entity node carrying the display name"
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// ── a node that isn't on disk still resolves by id (name falls back to id) ──
|
|
37
|
+
assert.deepStrictEqual(
|
|
38
|
+
recallNodeFromPath(path.join(process.env.PACKS_DIR, "ghost-pack", "PACK.md")),
|
|
39
|
+
{ kind: "pack", id: "ghost-pack", name: "ghost-pack" },
|
|
40
|
+
"missing pack falls back to id as name"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// ── anything that isn't a pack/entity file resolves to nothing ──
|
|
44
|
+
assert.strictEqual(recallNodeFromPath(null), null, "null path → null");
|
|
45
|
+
assert.strictEqual(recallNodeFromPath("/etc/passwd"), null, "outside the memory dirs → null");
|
|
46
|
+
assert.strictEqual(
|
|
47
|
+
recallNodeFromPath(path.join(process.env.PACKS_DIR, "billing-invoices", "notes.md")),
|
|
48
|
+
null,
|
|
49
|
+
"a non-PACK.md file inside a pack dir → null"
|
|
50
|
+
);
|
|
51
|
+
assert.strictEqual(
|
|
52
|
+
recallNodeFromPath(path.join(process.env.ENTITIES_DIR, "sub", "deep.md")),
|
|
53
|
+
null,
|
|
54
|
+
"an entity .md in a subdir → null"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
console.log("read signal OK");
|