@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 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]`. Switch back any time with `/engine classic`.
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 { packBlock, entityBlock, packMatches: finalPacks, entityMatches: finalEnts };
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 { require("./system-prompt").consumeLastInjected(); } catch (e) { /* best-effort */ }
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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,
@@ -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 { packBlock, entityBlock } = await engine.run({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.36",
3
+ "version": "2.6.37",
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": {
@@ -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