@inetafrica/open-claudia 2.6.22 → 2.6.24

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/bin/cli.js CHANGED
@@ -335,7 +335,7 @@ Memory tools:
335
335
  (alias: ts; --all for every project; --help for options)
336
336
  open-claudia transcript-window <pattern> Search project transcript, show hits with context
337
337
  (alias: tw; --help for options)
338
- open-claudia pack list|show|match|migrate Context packs: living topic docs (skills + memory)
338
+ open-claudia pack list|show|match|archive|restore|archived Context packs: living topic docs (skills + memory)
339
339
  open-claudia entity list|show|match|note Entity notes: people/places/projects memory
340
340
  open-claudia dream [--dry-run] Run the memory consolidation pass now
341
341
 
package/bin/pack.js CHANGED
@@ -19,9 +19,36 @@ function run(args) {
19
19
  const all = packs.listPacks();
20
20
  if (all.length === 0) return console.log(`No packs yet (${packs.PACKS_DIR}).`);
21
21
  for (const p of all) {
22
- const used = p.last_used ? ` last-used ${p.last_used.slice(0, 10)}` : "";
22
+ const used = p.last_used ? ` last-used ${p.last_used.slice(0, 10)} (${p.usage_count || 0}×)` : "";
23
23
  console.log(`${p.dir} — ${p.name}${p.tags.length ? ` [${p.tags.join(", ")}]` : ""}${used}\n ${p.description}`);
24
24
  }
25
+ const archived = packs.listArchived();
26
+ if (archived.length) console.log(`\n(${archived.length} archived — open-claudia pack archived)`);
27
+ break;
28
+ }
29
+
30
+ case "archive": {
31
+ const p = packs.archivePack(rest[0]);
32
+ if (!p) { console.error(`No pack: ${rest[0]}`); process.exitCode = 1; return; }
33
+ console.log(`Archived pack ${p.dir} → ${path.join(packs.PACKS_DIR, ".archived", p.dir)}\nRestore with: open-claudia pack restore ${p.dir}`);
34
+ break;
35
+ }
36
+
37
+ case "restore": {
38
+ let p;
39
+ try { p = packs.restorePack(rest[0]); }
40
+ catch (e) { console.error(e.message); process.exitCode = 1; return; }
41
+ if (!p) { console.error(`No archived pack: ${rest[0]}`); process.exitCode = 1; return; }
42
+ console.log(`Restored pack ${p.dir}.`);
43
+ break;
44
+ }
45
+
46
+ case "archived": {
47
+ const all = packs.listArchived();
48
+ if (all.length === 0) return console.log("No archived packs.");
49
+ for (const p of all) {
50
+ console.log(`${p.dir} — ${p.name} (archived ${(p.archived || "").slice(0, 10)}, last-used ${(p.last_used || "never").slice(0, 10)}, ${p.usage_count || 0}×)`);
51
+ }
25
52
  break;
26
53
  }
27
54
 
@@ -93,7 +120,7 @@ function run(args) {
93
120
  }
94
121
 
95
122
  default:
96
- console.log("Usage: open-claudia pack [list|show <dir>|match \"<text>\"|migrate|remove <dir>|reindex]");
123
+ console.log("Usage: open-claudia pack [list|show <dir>|match \"<text>\"|migrate|remove <dir>|archive <dir>|restore <dir>|archived|reindex]");
97
124
  }
98
125
  }
99
126
 
package/core/dream.js CHANGED
@@ -14,6 +14,7 @@ const path = require("path");
14
14
  const cron = require("node-cron");
15
15
 
16
16
  const CONFIG_DIR = require("../config-dir");
17
+ const { config } = require("./config");
17
18
  const packs = require("./packs");
18
19
  const entities = require("./entities");
19
20
  const persona = require("./persona");
@@ -23,7 +24,13 @@ const DREAM_MODEL = process.env.DREAM_MODEL || "sonnet";
23
24
  const DREAM_CRON = process.env.DREAM_CRON || "0 4 * * *";
24
25
  const MAX_PACK_CHARS = 2500;
25
26
  const MAX_ENTITY_CHARS = 900;
26
- const LIMITS = { merges: 3, umbrellas: 2, parents: 8, retag: 8, entity_merges: 3, entity_notes: 4 };
27
+ const LIMITS = { merges: 3, umbrellas: 2, parents: 8, retag: 8, entity_merges: 3, entity_notes: 4, archive: 5 };
28
+
29
+ // Retirement guard: even if the model proposes archiving, only act when a pack
30
+ // is genuinely cold — old enough, idle long enough, and rarely used. Protects
31
+ // freshly-created packs the user just made.
32
+ const ARCHIVE_IDLE_DAYS = Number(process.env.DREAM_ARCHIVE_IDLE_DAYS || 30);
33
+ const ARCHIVE_MAX_USAGE = Number(process.env.DREAM_ARCHIVE_MAX_USAGE || 3);
27
34
 
28
35
  let _dreaming = false;
29
36
 
@@ -31,6 +38,15 @@ function enabled() {
31
38
  return (process.env.DREAM || "on").toLowerCase() !== "off";
32
39
  }
33
40
 
41
+ // Whether to post the post-dream summary into the owner's chat. On by
42
+ // default; toggled with /dreamsummary (persisted to .env, read back from
43
+ // config on restart, with the live process.env taking precedence so the
44
+ // toggle takes effect without a restart).
45
+ function summaryEnabled() {
46
+ const v = process.env.DREAM_SUMMARY ?? config.DREAM_SUMMARY ?? "on";
47
+ return String(v).toLowerCase() !== "off";
48
+ }
49
+
34
50
  function clip(s, n) {
35
51
  const t = String(s || "");
36
52
  return t.length > n ? t.slice(0, n) + "\n…[truncated]" : t;
@@ -45,7 +61,7 @@ function buildDreamPrompt() {
45
61
  `name: ${p.name}`,
46
62
  `description: ${p.description}`,
47
63
  `tags: ${p.tags.join(", ")}`,
48
- `last_used: ${p.last_used || "never"}`,
64
+ `last_used: ${p.last_used || "never"} (used ${p.usage_count || 0}×)`,
49
65
  `Stance: ${p.sections.Stance}`,
50
66
  `Procedure: ${p.sections.Procedure}`,
51
67
  `State: ${p.sections.State}`,
@@ -83,13 +99,14 @@ Your job — decide what consolidation, if any, is warranted:
83
99
  4. retag: tighten descriptions and tags. The router FTS-matches incoming messages against name/description/tags, so generic words there cause false matches. Descriptions should be one specific line; tags specific nouns.
84
100
  5. entity_merges: the same real-world entity recorded twice gets merged (the better slug wins).
85
101
  6. entity_notes: rewrite an entity's Notes to be current and cross-linked — mention related packs as [[pack-dir]] and related entities by name.
86
- 7. persona: evolve the persona GENTLYkeep its structure and length (under 2200 chars), adjust only what recent work justifies (a new habit, a sharpened quirk). Most dreams should return null here.
87
- 8. report: a short chat message to the owner, written AS Open Claudia in first person warm, a little playful, a few emojis, mobile-friendly (2-6 short lines). Say what you tidied and why it helps. If you changed nothing, say the memory is in good shape, charmingly.
102
+ 7. archive: retire packs that have gone cold long unused AND rarely used over their life (consult last_used and the usage count). Archiving moves a pack out of the live index (reversibly, backed up) so it stops adding recall noise. ONLY propose packs idle ≥${ARCHIVE_IDLE_DAYS} days and used ≤${ARCHIVE_MAX_USAGE}× total; never an umbrella/parent pack that still has children; when in doubt, leave it. Give a one-line reason.
103
+ 8. persona: evolve the persona GENTLY keep its structure and length (under 2200 chars), adjust only what recent work justifies (a new habit, a sharpened quirk). Most dreams should return null here.
104
+ 9. report: a short chat message to the owner, written AS Open Claudia in first person — warm, a little playful, a few emojis, mobile-friendly (2-6 short lines). Say what you tidied and why it helps. If you changed nothing, say the memory is in good shape, charmingly.
88
105
 
89
106
  Hard rules:
90
107
  - Never invent packs/entities not listed above; reference them by exact dir/slug.
91
108
  - Never store secrets, tokens, passwords, or credentials anywhere.
92
- - Limits: ≤${LIMITS.merges} merges, ≤${LIMITS.umbrellas} umbrellas, ≤${LIMITS.parents} parents, ≤${LIMITS.retag} retags, ≤${LIMITS.entity_merges} entity merges, ≤${LIMITS.entity_notes} entity note rewrites.
109
+ - Limits: ≤${LIMITS.merges} merges, ≤${LIMITS.umbrellas} umbrellas, ≤${LIMITS.parents} parents, ≤${LIMITS.retag} retags, ≤${LIMITS.entity_merges} entity merges, ≤${LIMITS.entity_notes} entity note rewrites, ≤${LIMITS.archive} archives.
93
110
  - An empty decision is a perfectly good decision.
94
111
 
95
112
  Reply with ONLY a JSON object, no prose, no code fences:
@@ -99,6 +116,7 @@ Reply with ONLY a JSON object, no prose, no code fences:
99
116
  "retag": [{"pack": "<dir>", "description": "<one specific line>", "tags": ["..."]}],
100
117
  "entity_merges": [{"into": "<slug>", "from": ["<slug>"], "notes": "<merged Notes or null>"}],
101
118
  "entity_notes": [{"entity": "<slug>", "notes": "<rewritten Notes>"}],
119
+ "archive": [{"pack": "<dir>", "reason": "<one line: why it's cold>"}],
102
120
  "persona": null,
103
121
  "report": "<chat message>"}`;
104
122
  }
@@ -118,6 +136,7 @@ function parseDream(text) {
118
136
  retag: arr("retag"),
119
137
  entity_merges: arr("entity_merges"),
120
138
  entity_notes: arr("entity_notes"),
139
+ archive: arr("archive"),
121
140
  persona: typeof obj.persona === "string" ? obj.persona : null,
122
141
  report: typeof obj.report === "string" ? obj.report.trim() : "",
123
142
  };
@@ -259,6 +278,27 @@ function applyDream(decision, backupRoot) {
259
278
  } catch (e) { console.warn(`[dream] entity notes failed: ${e.message}`); }
260
279
  }
261
280
 
281
+ if (decision.archive && decision.archive.length) {
282
+ const allPacks = packs.listPacks();
283
+ for (const a of decision.archive) {
284
+ try {
285
+ const dir = a?.pack;
286
+ if (!dir || gone.has(dir)) continue;
287
+ const pack = packs.readPack(dir);
288
+ if (!pack) continue;
289
+ if (allPacks.some((p) => p.parent === dir)) continue; // never orphan children
290
+ const ageDays = pack.created ? (Date.now() - Date.parse(pack.created)) / 86400000 : Infinity;
291
+ const idleDays = pack.last_used ? (Date.now() - Date.parse(pack.last_used)) / 86400000 : ageDays;
292
+ const uses = Number(pack.usage_count) || 0;
293
+ if (ageDays < ARCHIVE_IDLE_DAYS || idleDays < ARCHIVE_IDLE_DAYS || uses > ARCHIVE_MAX_USAGE) continue;
294
+ backupPack(dir, backupRoot);
295
+ packs.archivePack(dir);
296
+ gone.add(dir);
297
+ lines.push(`🗃 Archived ${dir} — ${a.reason || `idle ${Math.round(idleDays)}d, used ${uses}×`}`);
298
+ } catch (e) { console.warn(`[dream] archive failed: ${e.message}`); }
299
+ }
300
+ }
301
+
262
302
  if (decision.persona) {
263
303
  try {
264
304
  if (persona.personaExists()) {
@@ -305,8 +345,9 @@ async function runDream({ trigger = "manual" } = {}) {
305
345
  }
306
346
 
307
347
  // In-bot scheduler: one quiet-hours cron, reporting to the owner's
308
- // Telegram chat. Silent (console only) when the dream changed nothing
309
- // a nightly "all tidy" ping would be noise.
348
+ // Telegram chat. When the dream summary is on (default) it posts a short
349
+ // note after every run — including the charming "all tidy" line when
350
+ // nothing changed. /dreamsummary off mutes it to console only.
310
351
  function initDream(adapters) {
311
352
  if (!enabled()) return;
312
353
  if (!cron.validate(DREAM_CRON)) {
@@ -318,7 +359,8 @@ function initDream(adapters) {
318
359
  const res = await runDream({ trigger: "cron" });
319
360
  if (res.skipped) { console.log(`dream: skipped — ${res.skipped}`); return; }
320
361
  console.log(`dream: applied ${res.applied.length} change(s)`);
321
- if (res.applied.length === 0) return;
362
+ if (!summaryEnabled()) { console.log("dream: summary muted (DREAM_SUMMARY=off)"); return; }
363
+ if (!res.message) return;
322
364
  const { CHAT_ID } = require("./config");
323
365
  const tg = (adapters || []).find((a) => a.type === "telegram");
324
366
  if (tg && CHAT_ID) await tg.send(CHAT_ID, res.message);
@@ -329,4 +371,4 @@ function initDream(adapters) {
329
371
  console.log(`dream: scheduled (${DREAM_CRON}, model ${DREAM_MODEL})`);
330
372
  }
331
373
 
332
- module.exports = { runDream, initDream, buildDreamPrompt, parseDream, applyDream, enabled, DREAM_CRON, DREAM_MODEL };
374
+ module.exports = { runDream, initDream, buildDreamPrompt, parseDream, applyDream, enabled, summaryEnabled, DREAM_CRON, DREAM_MODEL };
package/core/handlers.js CHANGED
@@ -125,7 +125,7 @@ register({
125
125
  "Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode",
126
126
  "Identity: /whoami /link",
127
127
  "Team: /people /intros /auth (owner)",
128
- "Automation: /cron /vault /soul",
128
+ "Automation: /cron /vault /soul /dreamsummary",
129
129
  "Claude auth: /auth_status /login /setup_token /use_oauth_token /clear_oauth_token",
130
130
  "Codex auth: /codex_auth_status /codex_login /codex_setup_token",
131
131
  "System: /doctor /requirements /restart /upgrade",
@@ -947,6 +947,24 @@ register({
947
947
  },
948
948
  });
949
949
 
950
+ register({
951
+ name: "dreamsummary", description: "Toggle the post-dream memory summary in chat", args: "[on|off]",
952
+ handler: async (env, { tail }) => {
953
+ if (!authorized(env)) return;
954
+ const dream = require("./dream");
955
+ const arg = (tail || "").trim().toLowerCase();
956
+ if (arg !== "on" && arg !== "off") {
957
+ const on = dream.summaryEnabled();
958
+ return send(`Dream summary is ${on ? "on" : "off"}.\n\nAfter each nightly memory consolidation I ${on ? "post a short summary here" : "tidy up silently"}.\n\nToggle: /dreamsummary on · /dreamsummary off`);
959
+ }
960
+ saveEnvKey("DREAM_SUMMARY", arg);
961
+ process.env.DREAM_SUMMARY = arg;
962
+ return send(arg === "on"
963
+ ? "Dream summary on — I'll drop a short note here after each nightly memory tidy-up. 💤"
964
+ : "Dream summary off — I'll consolidate memory silently. Backups are still kept, so nothing is lost.");
965
+ },
966
+ });
967
+
950
968
  // ── Learned skills (now backed by context packs) ────────────────────
951
969
  // Skills migrated into context packs (~/.open-claudia/packs). /skills is a
952
970
  // thin window onto that store; any stragglers still in the legacy
@@ -984,6 +1002,10 @@ register({
984
1002
  }
985
1003
  const lines = packs.map((p) => `• ${p.dir} — ${p.description || p.name || "(no description)"}`);
986
1004
  let msg = `Skills / context packs (${packs.length}):\n\n${lines.join("\n")}`;
1005
+ const archived = packsLib.listArchived();
1006
+ if (archived.length) {
1007
+ msg += `\n\n${archived.length} archived (cold packs retired by the nightly dream) — open-claudia pack archived to view, pack restore <dir> to bring one back.`;
1008
+ }
987
1009
  if (legacy.length) {
988
1010
  msg += `\n\n${legacy.length} legacy skill(s) still in ~/.claude/skills — run open-claudia pack migrate to fold them in.`;
989
1011
  }
package/core/packs.js CHANGED
@@ -70,8 +70,10 @@ function serialize(pack) {
70
70
  `created: ${pack.created || new Date().toISOString()}`,
71
71
  `updated: ${pack.updated || new Date().toISOString()}`,
72
72
  `last_used: ${pack.last_used || ""}`,
73
- "---",
73
+ `usage_count: ${pack.usage_count || 0}`,
74
74
  );
75
+ if (pack.archived) fmLines.push(`archived: ${pack.archived}`);
76
+ fmLines.push("---");
75
77
  const sections = SECTIONS
76
78
  .map((s) => `## ${s}\n\n${(pack.sections?.[s] || "").trim()}`)
77
79
  .join("\n\n");
@@ -99,6 +101,8 @@ function readPack(dir) {
99
101
  created: fm.created || "",
100
102
  updated: fm.updated || (stat ? stat.mtime.toISOString() : ""),
101
103
  last_used: fm.last_used || "",
104
+ usage_count: Number(fm.usage_count || 0) || 0,
105
+ archived: fm.archived || "",
102
106
  sections,
103
107
  };
104
108
  }
@@ -193,10 +197,68 @@ function touchUsed(dirs) {
193
197
  const pack = readPack(dir);
194
198
  if (!pack) continue;
195
199
  pack.last_used = now;
200
+ pack.usage_count = (Number(pack.usage_count) || 0) + 1;
196
201
  try { writePack(pack); } catch (e) {}
197
202
  }
198
203
  }
199
204
 
205
+ // ---------------------------------------------------------------------------
206
+ // Lifecycle: archive stale packs to PACKS_DIR/.archived/<dir>. listPacks and
207
+ // the FTS index skip dot-dirs, so an archived pack stops matching but the file
208
+ // is kept and fully restorable. This is the brake on an index that only grows.
209
+
210
+ const ARCHIVE_DIRNAME = ".archived";
211
+ function archiveRoot() { return path.join(PACKS_DIR, ARCHIVE_DIRNAME); }
212
+
213
+ function archivePack(nameOrDir) {
214
+ const pack = findPack(nameOrDir);
215
+ if (!pack) return null;
216
+ ensureDir();
217
+ fs.mkdirSync(archiveRoot(), { recursive: true, mode: 0o700 });
218
+ pack.archived = new Date().toISOString();
219
+ try { fs.writeFileSync(packFile(pack.dir), serialize(pack), { mode: 0o600 }); } catch (e) {}
220
+ const dest = path.join(archiveRoot(), pack.dir);
221
+ fs.rmSync(dest, { recursive: true, force: true });
222
+ fs.renameSync(path.join(PACKS_DIR, pack.dir), dest);
223
+ markIndexDirty();
224
+ return pack;
225
+ }
226
+
227
+ function listArchived() {
228
+ let entries;
229
+ try { entries = fs.readdirSync(archiveRoot()); } catch (e) { return []; }
230
+ const out = [];
231
+ for (const name of entries) {
232
+ if (name.startsWith(".")) continue;
233
+ let content;
234
+ try { content = fs.readFileSync(path.join(archiveRoot(), name, "PACK.md"), "utf-8"); }
235
+ catch (e) { continue; }
236
+ const { fm } = parseFrontmatter(content);
237
+ out.push({
238
+ dir: name,
239
+ name: fm.name || name,
240
+ description: fm.description || "",
241
+ last_used: fm.last_used || "",
242
+ usage_count: Number(fm.usage_count || 0) || 0,
243
+ archived: fm.archived || "",
244
+ });
245
+ }
246
+ return out.sort((a, b) => a.dir.localeCompare(b.dir));
247
+ }
248
+
249
+ function restorePack(nameOrDir) {
250
+ const needle = String(nameOrDir || "").trim().toLowerCase();
251
+ if (!needle) return null;
252
+ const found = listArchived().find((p) => p.dir.toLowerCase() === needle || p.name.toLowerCase() === needle);
253
+ if (!found) return null;
254
+ if (readPack(found.dir)) throw new Error(`an active pack ${found.dir} already exists`);
255
+ fs.renameSync(path.join(archiveRoot(), found.dir), path.join(PACKS_DIR, found.dir));
256
+ const pack = readPack(found.dir);
257
+ if (pack) { pack.archived = ""; pack.updated = new Date().toISOString(); try { writePack(pack); } catch (e) {} }
258
+ markIndexDirty();
259
+ return pack;
260
+ }
261
+
200
262
  // Recognise a Write/Edit aimed at a pack file (for chat announcements).
201
263
  function packNameFromPath(filePath) {
202
264
  if (!filePath) return null;
@@ -333,4 +395,5 @@ module.exports = {
333
395
  PACKS_DIR, SECTIONS, slugify,
334
396
  listPacks, findPack, readPack, writePack, createPack, updatePack, removePack,
335
397
  touchUsed, packNameFromPath, matchPacks, reindex, markIndexDirty,
398
+ archivePack, restorePack, listArchived,
336
399
  };
@@ -291,6 +291,19 @@ const packsInjectedFor = new Map(); // `${adapterId}:${channelId}:${dir}` -> `${
291
291
  const PACK_INJECT_MAX_CHARS = 4000;
292
292
  const DEFAULT_MEMORY_RECALL_MAX_CHARS = 9000;
293
293
 
294
+ // Progressive disclosure: when on, a matched pack is injected as a cheap
295
+ // teaser (Stance + State + a pointer) rather than its full body. Heavy
296
+ // reference sections (Procedure, Journal) are fetched on demand via
297
+ // `open-claudia pack show`. Because each teaser is small we can widen the
298
+ // match count — surface more relevant packs for the same budget and let the
299
+ // agent decide which to read further.
300
+ function packProgressive() {
301
+ return String(config.PACK_PROGRESSIVE ?? process.env.PACK_PROGRESSIVE ?? "on").toLowerCase() === "on";
302
+ }
303
+ function packMatchLimit() {
304
+ return intSetting("PACK_MATCH_LIMIT", packProgressive() ? 6 : 3);
305
+ }
306
+
294
307
  function intSetting(key, fallback) {
295
308
  const raw = config[key] ?? process.env[key];
296
309
  if (raw === undefined || raw === null || raw === "") return fallback;
@@ -332,7 +345,8 @@ function consumeLastInjected() {
332
345
  return out;
333
346
  }
334
347
 
335
- function formatPackForContext(pack, packsLib) {
348
+ function formatPackForContext(pack, packsLib, opts = {}) {
349
+ const progressive = !!opts.progressive;
336
350
  const clip = (s, n) => (s.length > n ? s.slice(0, n) + "\n…[truncated — read the full pack file]" : s);
337
351
  const parts = [`### Pack: ${pack.name} (${pack.dir})`];
338
352
  if (pack.description) parts.push(pack.description);
@@ -345,12 +359,21 @@ function formatPackForContext(pack, packsLib) {
345
359
  const childLines = children.map((c) => `- ${c.dir}: ${c.description || c.name}`).join("\n");
346
360
  parts.push(`Sub-packs (dig deeper with \`open-claudia pack show <dir>\`):\n${childLines}`);
347
361
  }
348
- for (const section of ["Stance", "Procedure", "State"]) {
362
+ // Progressive mode injects only the live sections (Stance = how to think,
363
+ // State = where we are) plus a pointer; full mode also inlines Procedure
364
+ // and the recent Journal.
365
+ const sections = progressive ? ["Stance", "State"] : ["Stance", "Procedure", "State"];
366
+ for (const section of sections) {
349
367
  const body = (pack.sections[section] || "").trim();
350
368
  if (body) parts.push(`#### ${section}\n${body}`);
351
369
  }
352
- const journal = (pack.sections.Journal || "").trim().split("\n").filter(Boolean).slice(-10).join("\n");
353
- if (journal) parts.push(`#### Journal (recent)\n${journal}`);
370
+ if (progressive) {
371
+ const hasMore = (pack.sections.Procedure || "").trim() || (pack.sections.Journal || "").trim();
372
+ if (hasMore) parts.push(`#### More\nProcedure + Journal not shown — run \`open-claudia pack show ${pack.dir}\` to read them.`);
373
+ } else {
374
+ const journal = (pack.sections.Journal || "").trim().split("\n").filter(Boolean).slice(-10).join("\n");
375
+ if (journal) parts.push(`#### Journal (recent)\n${journal}`);
376
+ }
354
377
  return clip(parts.join("\n\n"), PACK_INJECT_MAX_CHARS);
355
378
  }
356
379
 
@@ -362,6 +385,7 @@ function buildPackBlock(matches, budget) {
362
385
  const adapter = currentAdapter();
363
386
  const channelId = currentChannelId();
364
387
  const sess = state.lastSessionId || "new";
388
+ const progressive = packProgressive();
365
389
  const blocks = [];
366
390
  const used = [];
367
391
  for (const m of matches) {
@@ -370,13 +394,13 @@ function buildPackBlock(matches, budget) {
370
394
  used.push(m.dir);
371
395
  const key = `${adapter?.id || "?"}:${channelId || "?"}:${m.dir}`;
372
396
  const stamp = `${sess}:${pack.updated}`;
373
- // Inject a pack's full body once per (channel, session, version). A
397
+ // Inject a pack's body once per (channel, session, version). A
374
398
  // compaction mints a new session id, which changes the stamp and
375
- // forces a fresh full re-injection on the next turn (same mechanism
399
+ // forces a fresh re-injection on the next turn (same mechanism
376
400
  // as the task tree), so the pack survives compaction without paying
377
401
  // to re-stamp an anchor on every intervening turn.
378
402
  if (packsInjectedFor.get(key) === stamp) continue;
379
- const block = formatPackForContext(pack, packsLib);
403
+ const block = formatPackForContext(pack, packsLib, { progressive });
380
404
  if (!tryUseRecallBudget(budget, block)) continue;
381
405
  packsInjectedFor.set(key, stamp);
382
406
  lastInjected.packs.push(pack.name || m.dir);
@@ -536,10 +560,11 @@ async function promptWithDynamicContext(prompt, opts = {}) {
536
560
  const entitiesLib = require("./entities");
537
561
  let packMatches = [];
538
562
  let entityMatches = [];
563
+ const packLimit = packMatchLimit();
539
564
  try {
540
565
  packMatches = mergeMatches(
541
- packsLib.matchPacks(userText, { limit: 3 }),
542
- fullContext ? packsLib.matchPacks(fullContext, { limit: 3 }) : [],
566
+ packsLib.matchPacks(userText, { limit: packLimit }),
567
+ fullContext ? packsLib.matchPacks(fullContext, { limit: packLimit }) : [],
543
568
  (m) => m.dir,
544
569
  );
545
570
  } catch (e) {}
@@ -553,7 +578,7 @@ async function promptWithDynamicContext(prompt, opts = {}) {
553
578
  const candPacks = packMatches;
554
579
  const candEntities = entityMatches;
555
580
  ({ packMatches, entityMatches } = await filterMatches(userText, fullContext, packMatches, entityMatches));
556
- packMatches = packMatches.slice(0, 3);
581
+ packMatches = packMatches.slice(0, packLimit);
557
582
  entityMatches = entityMatches.slice(0, 4);
558
583
  logRecall(userText, candPacks, candEntities, packMatches, entityMatches);
559
584
  const budget = memoryRecallBudget();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.22",
3
+ "version": "2.6.24",
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": {