@inetafrica/open-claudia 2.6.35 → 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/.env.example CHANGED
@@ -27,6 +27,10 @@ USAGE_ALERT_BASELINE_TURNS=20
27
27
  USAGE_ALERT_MIN_BASELINE_TURNS=6
28
28
  USAGE_ALERT_COOLDOWN_MS=1800000
29
29
  MEMORY_RECALL_MAX_CHARS=9000
30
+ # Default recall engine when a chat hasn't picked one with /engine: classic | discoverer
31
+ RECALL_ENGINE=classic
32
+ # Dream model tier: low (haiku) | medium (sonnet) | high (opus, default). DREAM_MODEL overrides.
33
+ DREAM_TIER=high
30
34
  PROJECT_TRANSCRIPTS=true
31
35
  TRANSCRIPT_MAX_ENTRY_CHARS=12000
32
36
  TRANSCRIPTS_DIR=
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
6
+ ## v2.6.36
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.
8
+
3
9
  ## v2.6.35
4
10
  - **Fix `/status` silently dying + show the active recall engine.** The status handler referenced an undefined `activeCrons`, throwing a ReferenceError that `router.js` swallows by design — so `/status` did nothing. It now counts this channel's crons via `jobs.listForChannel(...)` and adds a `Recall engine:` line so you can confirm which engine (`classic`/`discoverer`) the chat is on. Note: `/engine` already worked when typed; it just may not appear in Telegram's slash-command autocomplete until the client refreshes the cached `setMyCommands` menu.
5
11
 
package/README.md CHANGED
@@ -15,7 +15,8 @@ Send text, voice notes, screenshots, and files from your phone. Your chosen AI a
15
15
  ### Memory & long-term context
16
16
  - **Context packs** — living per-topic documents (one per project, system, or recurring task) holding Stance, Procedure, State, and Journal. Packs matching your message are auto-injected into the agent's context, and a background reviewer updates them after every substantial turn — the assistant keeps its train of thought across sessions and projects
17
17
  - **Entity memory** — short notes on the people, places, projects, orgs, and systems you mention, extracted automatically and injected when they come up again
18
- - **Dream consolidation** — a nightly pass on a stronger model that merges duplicate packs, builds umbrella/parent pack trees, tightens descriptions, dedupes entities, and reports what it tidied with everything backed up first
18
+ - **Pluggable recall** — switch per chat with `/engine`: the stable **classic** keyword engine (default) or the opt-in **discoverer**, which walks a typed-edge graph over the same packs/entities and surfaces hits with one-line why-bullets
19
+ - **Dream consolidation** — a nightly pass on a stronger model that merges duplicate packs, builds umbrella/parent pack trees, tightens descriptions, dedupes entities, tends the recall graph, and reports what it tidied — with everything backed up first
19
20
  - **Personality** — a persona file gives the assistant a consistent voice on top of your soul file, and the dream pass evolves it gently as you work together
20
21
  - **Transcript search** — redacted project transcripts indexed in SQLite FTS5; `open-claudia transcript-search` gives the agent ~50ms ranked recall over months of history
21
22
  - **Smart compaction** — long conversations are summarized proactively before they get slow; full briefs are archived to disk so nothing is truly lost (`/compact`, `/compactwindow`)
@@ -177,7 +178,7 @@ When you select a project, the last conversation is automatically resumed. Tap "
177
178
  | `/ask` | Toggle ask mode — read-only Q&A, no edits (Cursor Agent only) |
178
179
  | `/worktree` | Toggle isolated git branch |
179
180
  | `/mode` | Switch between direct and agent bot modes |
180
- | `/status` | Show current session, backend, and settings |
181
+ | `/status` | Show current session, backend, recall engine, and settings |
181
182
  | `/usage` | Token usage and cost for this session |
182
183
  | `/doctor` / `/requirements` | Check Node, CLI binaries/versions/auth, voice stack, and writable paths |
183
184
 
@@ -187,6 +188,8 @@ When you select a project, the last conversation is automatically resumed. Tap "
187
188
  |---------|-------------|
188
189
  | `/learn [<hint>]` | Capture the last piece of work into the matching context pack |
189
190
  | `/skills [show\|remove <name>]` | List, show, or remove legacy learned skills |
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) |
190
193
  | `/soul` | View/edit assistant identity and personality |
191
194
  | `/dreamsummary [on\|off]` | Toggle the post-dream memory summary in chat |
192
195
 
@@ -262,7 +265,9 @@ Open Claudia layers three memory systems on top of the backend's native sessions
262
265
 
263
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.
264
267
 
265
- **Dream consolidation** — while the per-turn reviewer takes quick notes, *dream* is the slow overnight pass (default 4am, on a stronger 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, and cross-links notes. 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`.
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`.
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`.
266
271
 
267
272
  **Personality** — your `soul.md` holds identity and hard rules; `~/.open-claudia/persona.md` holds the voice on top — tone, quirks, emoji habits. It feeds into the system prompt and the dream pass may evolve it gently (bounded, backed up, announced). Edit it directly any time.
268
273
 
@@ -272,6 +277,8 @@ Open Claudia layers three memory systems on top of the backend's native sessions
272
277
  open-claudia pack list|show <dir>|match "<text>"|migrate|remove <dir>|reindex
273
278
  open-claudia entity list|show <slug>|match "<text>"|note <name> "<text>"|remove <slug>|reindex
274
279
  open-claudia dream [--dry-run] # run the consolidation pass now
280
+ open-claudia recall-stats # discoverer-engine metrics summary
281
+ open-claudia recall graph [--sync] # recall-graph node/edge stats; --sync rebuilds structural edges
275
282
  open-claudia transcript-search "<query>" # alias: ts; --all for every project
276
283
  open-claudia transcript-window "<pattern>" # alias: tw; hits with surrounding turns
277
284
  ```
@@ -427,6 +434,8 @@ All stored in `~/.open-claudia/`:
427
434
  | `USAGE_ALERT_RATE_MULTIPLIER` | No | Alert when the latest context-token rate exceeds the recent baseline by this multiple (default `1.75`, `off` disables) |
428
435
  | `USAGE_ALERT_BASELINE_TURNS` / `USAGE_ALERT_MIN_BASELINE_TURNS` / `USAGE_ALERT_COOLDOWN_MS` | No | Tune token-rate baseline size, minimum sample size, and alert cooldown |
429
436
  | `MEMORY_RECALL_MAX_CHARS` | No | Hard cap for auto-injected pack/entity memory per turn (default `9000`, `off` disables auto recall injection) |
437
+ | `RECALL_ENGINE` | No | Default recall engine when a chat hasn't set one via `/engine` (`classic` or `discoverer`, default `classic`) |
438
+ | `RECALL_GRAPH_DB` / `RECALL_METRICS` | No | Override the discoverer graph DB path; `off` on metrics disables per-turn recall logging |
430
439
  | `PROJECT_TRANSCRIPTS` | No | Enable redacted project transcripts (default `true`) |
431
440
  | `TRANSCRIPT_MAX_ENTRY_CHARS` | No | Max chars per transcript entry (default `12000`) |
432
441
  | `TRANSCRIPTS_DIR` / `PACKS_DIR` / `ENTITIES_DIR` | No | Override storage directories |
@@ -435,7 +444,8 @@ All stored in `~/.open-claudia/`:
435
444
  | `PACK_MATCH_THRESHOLD` / `ENTITY_MATCH_THRESHOLD` | No | Router match score thresholds (default `2`) |
436
445
  | `DREAM` | No | `off` disables the nightly memory consolidation pass (default on) |
437
446
  | `DREAM_CRON` | No | Schedule for the dream pass (default `0 4 * * *`) |
438
- | `DREAM_MODEL` | No | Model for the dream pass (default `sonnet`) |
447
+ | `DREAM_MODEL` | No | Explicit model override for the dream pass (otherwise picked from `DREAM_TIER`) |
448
+ | `DREAM_TIER` | No | Model tier for the dream pass: `low` (haiku) / `medium` (sonnet) / `high` (opus, default) |
439
449
  | `PERSONA_FILE` | No | Override the persona file location |
440
450
  | `WEB_UI` / `WEB_PORT` / `WEB_PASSWORD` | No | Web UI toggle, port, and password |
441
451
  | `WHISPER_CLI` / `WHISPER_MODEL` | No | whisper.cpp binary and model for voice notes |
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.35",
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