@inetafrica/open-claudia 2.6.36 → 2.6.37
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/README.md +2 -1
- package/core/actions.js +6 -0
- package/core/handlers.js +24 -1
- package/core/recall/discoverer.js +5 -2
- package/core/runner.js +11 -1
- package/core/system-prompt.js +12 -4
- package/package.json +1 -1
- package/test-recall-discoverer.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.37
|
|
4
|
+
- **`/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
|
+
|
|
3
6
|
## v2.6.36
|
|
4
7
|
- **Docs: document the dual-engine recall feature.** v2.6.34/v2.6.35 shipped the pluggable recall engine and `/engine` switch but the README and `.env.example` were never updated. This release fills the gap: README gains a "Pluggable recall" Features bullet, the `/engine` command row, a "Recall engines" narrative section, the `recall-stats` / `recall graph` CLI commands, and env-table rows for `RECALL_ENGINE` / `RECALL_GRAPH_DB` / `RECALL_METRICS` / `DREAM_TIER` (plus the corrected `DREAM_MODEL` default). `.env.example` gains `RECALL_ENGINE` and `DREAM_TIER`. Docs-only — no code changes.
|
|
5
8
|
|
package/README.md
CHANGED
|
@@ -189,6 +189,7 @@ When you select a project, the last conversation is automatically resumed. Tap "
|
|
|
189
189
|
| `/learn [<hint>]` | Capture the last piece of work into the matching context pack |
|
|
190
190
|
| `/skills [show\|remove <name>]` | List, show, or remove legacy learned skills |
|
|
191
191
|
| `/engine [classic\|discoverer]` | Switch the per-chat memory recall engine (default `classic`) |
|
|
192
|
+
| `/recall [on\|off]` | Toggle a per-turn "🧠 Recall this turn" debug line showing which packs/entities surfaced (and why, on discoverer) |
|
|
192
193
|
| `/soul` | View/edit assistant identity and personality |
|
|
193
194
|
| `/dreamsummary [on\|off]` | Toggle the post-dream memory summary in chat |
|
|
194
195
|
|
|
@@ -264,7 +265,7 @@ Open Claudia layers three memory systems on top of the backend's native sessions
|
|
|
264
265
|
|
|
265
266
|
**Entity memory** (`~/.open-claudia/entities/<slug>.md`) works the same way for the people, places, projects, orgs, and systems you mention — who they are, current truth, and a dated observation log. Mentioning a name injects its note.
|
|
266
267
|
|
|
267
|
-
**Recall engines** — how packs and entities get matched and surfaced is pluggable per chat via `/engine` (or the `RECALL_ENGINE` env default). **classic** (the default) is keyword FTS plus a relevance judge with headline injection — stable and unchanged. **discoverer** (opt-in) adds a typed-edge graph over the same corpus (`parent`/`governed-by`/`related` edges with weights in `recall-graph.db`) and runs: a pre-gate that skips recall on trivial turns → FTS seeding → spreading activation across the graph (1–2 hops — auto-pulls cross-cutting concerns the query never named) → a walker that reads each candidate and returns the genuinely-relevant set with one-line why-bullets (fail-open to keyword seeds, so it never recalls worse than classic). Edges form structurally from pack `parent` frontmatter and `[[links]]`, and strengthen via Hebbian co-use when the agent opens packs together (📖); weights decay over time. Inspect with `open-claudia recall-stats` and `open-claudia recall graph [--sync]
|
|
268
|
+
**Recall engines** — how packs and entities get matched and surfaced is pluggable per chat via `/engine` (or the `RECALL_ENGINE` env default). **classic** (the default) is keyword FTS plus a relevance judge with headline injection — stable and unchanged. **discoverer** (opt-in) adds a typed-edge graph over the same corpus (`parent`/`governed-by`/`related` edges with weights in `recall-graph.db`) and runs: a pre-gate that skips recall on trivial turns → FTS seeding → spreading activation across the graph (1–2 hops — auto-pulls cross-cutting concerns the query never named) → a walker that reads each candidate and returns the genuinely-relevant set with one-line why-bullets (fail-open to keyword seeds, so it never recalls worse than classic). Edges form structurally from pack `parent` frontmatter and `[[links]]`, and strengthen via Hebbian co-use when the agent opens packs together (📖); weights decay over time. Inspect with `open-claudia recall-stats` and `open-claudia recall graph [--sync]`, or flip on `/recall` to watch — per turn — which packs/entities surfaced and why, right in the chat. Switch back any time with `/engine classic`.
|
|
268
269
|
|
|
269
270
|
**Dream consolidation** — while the per-turn reviewer takes quick notes, *dream* is the slow overnight pass (default 4am, on a high-tier model — opus by default, set `DREAM_TIER` or `DREAM_MODEL`): it merges packs that drifted into the same topic, builds parent/sub pack trees with umbrella summaries, tightens descriptions and tags so the router matches with less noise, dedupes entities, cross-links notes, and tends the recall graph (structural sync, weight decay, orphan prune). Anything merged away is backed up under `~/.open-claudia/backup/dream-<stamp>/` first, and every dream that changes something reports in chat. Configure with `DREAM_CRON` / `DREAM_MODEL`, disable with `DREAM=off`.
|
|
270
271
|
|
package/core/actions.js
CHANGED
|
@@ -282,6 +282,12 @@ async function handleAction(envelope) {
|
|
|
282
282
|
await send(`Recall engine: ${state.settings.recallEngine || "classic (default)"}`);
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
|
+
if (d.startsWith("rcl:")) {
|
|
286
|
+
state.settings.showRecall = d.slice(4) === "on";
|
|
287
|
+
saveState();
|
|
288
|
+
await send(`Recall debug: ${state.settings.showRecall ? "on" : "off"}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
285
291
|
if (d.startsWith("cw:")) {
|
|
286
292
|
const v = d.slice(3);
|
|
287
293
|
if (v === "default") state.settings.compactWindow = null;
|
package/core/handlers.js
CHANGED
|
@@ -122,7 +122,7 @@ register({
|
|
|
122
122
|
if (!authorized(env)) return;
|
|
123
123
|
send([
|
|
124
124
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
125
|
-
"Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode /engine",
|
|
125
|
+
"Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode /engine /recall",
|
|
126
126
|
"Identity: /whoami /link",
|
|
127
127
|
"Team: /people /intros /auth (owner)",
|
|
128
128
|
"Automation: /cron /vault /soul /dreamsummary",
|
|
@@ -719,6 +719,29 @@ register({
|
|
|
719
719
|
},
|
|
720
720
|
});
|
|
721
721
|
|
|
722
|
+
register({
|
|
723
|
+
name: "recall", description: "Show what memory recall surfaced each turn (debug)", args: "[on|off]",
|
|
724
|
+
handler: async (env, { tail }) => {
|
|
725
|
+
if (!authorized(env)) return;
|
|
726
|
+
const { settings } = currentState();
|
|
727
|
+
if (tail) {
|
|
728
|
+
const v = tail.trim().toLowerCase();
|
|
729
|
+
if (v === "on" || v === "true") settings.showRecall = true;
|
|
730
|
+
else if (v === "off" || v === "false") settings.showRecall = false;
|
|
731
|
+
else return send(`Usage: /recall [on|off]. Currently ${settings.showRecall ? "on" : "off"}.`);
|
|
732
|
+
saveState();
|
|
733
|
+
return send(`Recall debug: ${settings.showRecall ? "on" : "off"}`);
|
|
734
|
+
}
|
|
735
|
+
send(
|
|
736
|
+
`Recall debug: ${settings.showRecall ? "on" : "off"}\n\n` +
|
|
737
|
+
"When on, I post a short \"🧠 Recall this turn\" line before each reply, showing which packs/entities surfaced (and, on the discoverer engine, why). Lets you watch recall work.",
|
|
738
|
+
{ keyboard: { inline_keyboard: [
|
|
739
|
+
[{ text: "On", callback_data: "rcl:on" }, { text: "Off", callback_data: "rcl:off" }],
|
|
740
|
+
] } },
|
|
741
|
+
);
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
|
|
722
745
|
register({
|
|
723
746
|
name: "budget", description: "Set max spend for next task", args: "[$N]",
|
|
724
747
|
handler: async (env, { tail }) => {
|
|
@@ -145,7 +145,7 @@ async function run(ctx) {
|
|
|
145
145
|
// 1: pre-gate.
|
|
146
146
|
if (!needsRecall(userText, seedCount)) {
|
|
147
147
|
metrics.logTurn({ engine: "discoverer", query: userText, gated: true, latencyMs: Date.now() - started });
|
|
148
|
-
return { packBlock: "", entityBlock: "", packMatches: [], entityMatches: [] };
|
|
148
|
+
return { packBlock: "", entityBlock: "", packMatches: [], entityMatches: [], why: {}, gated: true };
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// 3: spreading activation from seeds across the graph.
|
|
@@ -225,7 +225,10 @@ async function run(ctx) {
|
|
|
225
225
|
latencyMs: Date.now() - started,
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
packBlock, entityBlock, packMatches: finalPacks, entityMatches: finalEnts,
|
|
230
|
+
why: whyById ? Object.fromEntries(whyById) : {}, gated: false,
|
|
231
|
+
};
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
module.exports = { name: "discoverer", run, needsRecall, walk };
|
package/core/runner.js
CHANGED
|
@@ -881,7 +881,17 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
881
881
|
// `open-claudia pack show <dir>` / `entity show <slug>` — so the banner
|
|
882
882
|
// reflects what was read, not what was pushed. (consumeLastInjected is
|
|
883
883
|
// drained here to keep the per-turn buffer from leaking into the next turn.)
|
|
884
|
-
try {
|
|
884
|
+
try {
|
|
885
|
+
const injected = require("./system-prompt").consumeLastInjected();
|
|
886
|
+
if (settings.showRecall && injected && injected.recall) {
|
|
887
|
+
const r = injected.recall;
|
|
888
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
889
|
+
const fmt = (arr, icon) => arr.map((x) => (x.why ? `${icon} <b>${esc(x.name)}</b> — ${esc(x.why)}` : `${icon} <b>${esc(x.name)}</b>`));
|
|
890
|
+
const lines = [...fmt(r.packs || [], "📦"), ...fmt(r.entities || [], "👤")];
|
|
891
|
+
if (lines.length) send(`🧠 <b>Recall this turn</b> (${esc(r.engine)})\n${lines.join("\n")}`).catch(() => {});
|
|
892
|
+
else if (r.gated) send(`🧠 <b>Recall</b> (${esc(r.engine)}): skipped by pre-gate — trivial turn.`).catch(() => {});
|
|
893
|
+
}
|
|
894
|
+
} catch (e) { /* best-effort */ }
|
|
885
895
|
const binaryPath = getActiveBinary();
|
|
886
896
|
const proc = spawn(binaryPath, args, {
|
|
887
897
|
cwd,
|
package/core/system-prompt.js
CHANGED
|
@@ -349,10 +349,10 @@ function tryUseRecallBudget(budget, text) {
|
|
|
349
349
|
// What the last promptWithDynamicContext call freshly injected (not the
|
|
350
350
|
// deduped repeats) — consumed by the runner to announce recalls in chat,
|
|
351
351
|
// mirroring the write-side announcements.
|
|
352
|
-
let lastInjected = { packs: [], entities: [] };
|
|
352
|
+
let lastInjected = { packs: [], entities: [], recall: null };
|
|
353
353
|
function consumeLastInjected() {
|
|
354
354
|
const out = lastInjected;
|
|
355
|
-
lastInjected = { packs: [], entities: [] };
|
|
355
|
+
lastInjected = { packs: [], entities: [], recall: null };
|
|
356
356
|
return out;
|
|
357
357
|
}
|
|
358
358
|
|
|
@@ -613,7 +613,7 @@ function bumpFtsMissCounter(n) {
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
async function promptWithDynamicContext(prompt, opts = {}) {
|
|
616
|
-
lastInjected = { packs: [], entities: [] };
|
|
616
|
+
lastInjected = { packs: [], entities: [], recall: null };
|
|
617
617
|
try {
|
|
618
618
|
const { userText, contextText } = recallMatchParts(prompt);
|
|
619
619
|
let historyText = "";
|
|
@@ -633,9 +633,17 @@ async function promptWithDynamicContext(prompt, opts = {}) {
|
|
|
633
633
|
packsLib, entitiesLib, mergeMatches, filterMatches, logRecall,
|
|
634
634
|
buildPackBlock, buildEntityBlock,
|
|
635
635
|
};
|
|
636
|
-
const
|
|
636
|
+
const result = await engine.run({
|
|
637
637
|
userText, contextText, fullContext, packLimit, budget, helpers,
|
|
638
638
|
});
|
|
639
|
+
const { packBlock, entityBlock } = result;
|
|
640
|
+
const why = result.why || {};
|
|
641
|
+
lastInjected.recall = {
|
|
642
|
+
engine: engine.name || recall.activeEngineName(settings),
|
|
643
|
+
gated: !!result.gated,
|
|
644
|
+
packs: (result.packMatches || []).map((m) => ({ name: m.name || m.dir, why: why[`pack:${m.dir}`] || "" })),
|
|
645
|
+
entities: (result.entityMatches || []).map((m) => ({ name: m.name || m.slug, why: why[`entity:${m.slug}`] || "" })),
|
|
646
|
+
};
|
|
639
647
|
const budgetNote = budget.omitted > 0
|
|
640
648
|
? `\n\n## Memory budget\n${budget.omitted} matched memory item${budget.omitted === 1 ? " was" : "s were"} omitted to keep this turn under the recall budget (${budget.maxChars} chars). Use \`open-claudia pack show <dir>\`, \`entity show <slug>\`, or transcript search if deeper context is needed.`
|
|
641
649
|
: "";
|
package/package.json
CHANGED
|
@@ -59,6 +59,7 @@ const helpers = { packsLib, entitiesLib, mergeMatches, buildPackBlock, buildEnti
|
|
|
59
59
|
});
|
|
60
60
|
assert.strictEqual(gated.packBlock, "");
|
|
61
61
|
assert.strictEqual(gated.packMatches.length, 0);
|
|
62
|
+
assert.strictEqual(gated.gated, true, "gated turn flags gated:true");
|
|
62
63
|
assert.strictEqual(builtPacks, null, "gated turn never builds blocks");
|
|
63
64
|
|
|
64
65
|
// seeded turn: FTS hits kazee-mobile → fail-open keeps it, block rendered
|
|
@@ -69,6 +70,8 @@ const helpers = { packsLib, entitiesLib, mergeMatches, buildPackBlock, buildEnti
|
|
|
69
70
|
assert.strictEqual(out.packBlock, "PACKBLOCK");
|
|
70
71
|
assert.strictEqual(out.packMatches.length, 1);
|
|
71
72
|
assert.strictEqual(out.packMatches[0].dir, "kazee-mobile");
|
|
73
|
+
assert.strictEqual(out.gated, false, "seeded turn is not gated");
|
|
74
|
+
assert.strictEqual(typeof out.why, "object", "seeded turn returns a why map");
|
|
72
75
|
assert.ok(builtPacks && builtPacks.some((m) => m.dir === "kazee-mobile"), "seed reached the builder");
|
|
73
76
|
|
|
74
77
|
// resilient: a throwing matcher must not blow up the engine
|