@psiclawops/hypermem 0.9.2 → 0.9.4

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/INSTALL.md +73 -70
  3. package/README.md +33 -51
  4. package/assets/default-config.json +47 -0
  5. package/bin/hypermem-doctor.mjs +76 -2
  6. package/bin/hypermem-status.mjs +255 -7
  7. package/dist/adaptive-lifecycle.d.ts +39 -0
  8. package/dist/adaptive-lifecycle.d.ts.map +1 -1
  9. package/dist/adaptive-lifecycle.js +87 -9
  10. package/dist/background-indexer.d.ts.map +1 -1
  11. package/dist/background-indexer.js +7 -5
  12. package/dist/compositor.d.ts.map +1 -1
  13. package/dist/compositor.js +239 -20
  14. package/dist/hybrid-retrieval.d.ts +8 -0
  15. package/dist/hybrid-retrieval.d.ts.map +1 -1
  16. package/dist/hybrid-retrieval.js +112 -10
  17. package/dist/index.d.ts +15 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +17 -0
  20. package/dist/message-store.d.ts +62 -1
  21. package/dist/message-store.d.ts.map +1 -1
  22. package/dist/message-store.js +355 -2
  23. package/dist/open-domain.d.ts.map +1 -1
  24. package/dist/open-domain.js +3 -2
  25. package/dist/proactive-pass.d.ts +42 -2
  26. package/dist/proactive-pass.d.ts.map +1 -1
  27. package/dist/proactive-pass.js +294 -39
  28. package/dist/topic-synthesizer.d.ts.map +1 -1
  29. package/dist/topic-synthesizer.js +9 -3
  30. package/dist/types.d.ts +99 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/vector-store.d.ts +10 -1
  33. package/dist/vector-store.d.ts.map +1 -1
  34. package/dist/vector-store.js +45 -9
  35. package/docs/DIAGNOSTICS.md +87 -0
  36. package/docs/INTEGRATION_VALIDATION.md +40 -1
  37. package/docs/ROADMAP.md +25 -12
  38. package/docs/TUNING.md +45 -4
  39. package/install.sh +5 -60
  40. package/memory-plugin/dist/index.d.ts +24 -0
  41. package/memory-plugin/dist/index.js +570 -0
  42. package/memory-plugin/openclaw.plugin.json +199 -2
  43. package/memory-plugin/package.json +3 -3
  44. package/package.json +24 -10
  45. package/plugin/dist/index.d.ts +210 -0
  46. package/plugin/dist/index.d.ts.map +1 -0
  47. package/plugin/dist/index.js +3641 -0
  48. package/plugin/dist/index.js.map +1 -0
  49. package/plugin/openclaw.plugin.json +199 -2
  50. package/plugin/package.json +4 -4
  51. package/scripts/install-packed-runtime.mjs +99 -0
  52. package/scripts/install-runtime.mjs +164 -4
@@ -123,6 +123,10 @@ function setCommand(pathKey, value, strictJson = false) {
123
123
  return `openclaw config set ${pathKey} ${shellQuote(rendered)}${strictJson ? ' --strict-json' : ''}`;
124
124
  }
125
125
 
126
+ function unsetCommand(pathKey) {
127
+ return `openclaw config unset ${pathKey}`;
128
+ }
129
+
126
130
  function shellQuote(value) {
127
131
  if (/^[A-Za-z0-9_./:@=-]+$/.test(value)) return value;
128
132
  return `'${String(value).replaceAll("'", "'\\''")}'`;
@@ -165,7 +169,7 @@ function checkConfigReadable() {
165
169
  } else if (hypermemRead.exists) {
166
170
  required('ok', 'hypermem-config-json', `HyperMem config readable: ${flags.hypermemConfig}`);
167
171
  } else {
168
- recommended('warn', 'hypermem-config-present', `No legacy HyperMem config found at ${flags.hypermemConfig}; ok if config lives in openclaw.json`);
172
+ recommended('warn', 'hypermem-config-present', `HyperMem config missing at ${flags.hypermemConfig}; run hypermem-install to create the 0.9.4 default config, or declare equivalent plugin config in openclaw.json`);
169
173
  }
170
174
  }
171
175
 
@@ -212,6 +216,15 @@ function checkRuntimePlugins() {
212
216
  recommended('ok', 'runtime-plugin-list', 'Runtime plugin load check skipped');
213
217
  return;
214
218
  }
219
+ const registryRefresh = spawnSync('openclaw', ['plugins', 'registry', '--refresh'], { encoding: 'utf8', timeout: 15000 });
220
+ if (registryRefresh.error) {
221
+ recommended('warn', 'plugin-registry-refresh', `Could not refresh OpenClaw plugin registry: ${registryRefresh.error.message}`);
222
+ } else if (registryRefresh.status === 0) {
223
+ recommended('ok', 'plugin-registry-refresh', 'OpenClaw plugin registry refresh command completed');
224
+ } else {
225
+ recommended('warn', 'plugin-registry-refresh', `OpenClaw plugin registry refresh exited ${registryRefresh.status}; run openclaw plugins registry --refresh after staging HyperMem`);
226
+ }
227
+
215
228
  const result = spawnSync('openclaw', ['plugins', 'list'], { encoding: 'utf8', timeout: 8000 });
216
229
  if (result.error) {
217
230
  recommended('warn', 'runtime-plugin-list', `Could not run openclaw plugins list: ${result.error.message}`);
@@ -224,7 +237,51 @@ function checkRuntimePlugins() {
224
237
  'runtime-plugin-list',
225
238
  hasComposer && hasMemory
226
239
  ? 'Runtime plugin list mentions hypercompositor and hypermem'
227
- : 'Runtime plugin list did not clearly show both hypercompositor and hypermem; restart gateway after config changes');
240
+ : 'Runtime plugin list did not clearly show both hypercompositor and hypermem; run openclaw plugins registry --refresh, openclaw doctor --fix --yes, then restart gateway after config changes');
241
+ }
242
+
243
+
244
+ function effectiveHyperMemConfig() {
245
+ return {
246
+ ...hypermem,
247
+ ...pluginConfig,
248
+ compositor: { ...(hypermem.compositor ?? {}), ...(pluginConfig.compositor ?? {}) },
249
+ embedding: { ...(hypermem.embedding ?? {}), ...(pluginConfig.embedding ?? {}) },
250
+ };
251
+ }
252
+
253
+ function checkRecallSurfaceRecommendations() {
254
+ const config = effectiveHyperMemConfig();
255
+ const compositor = config.compositor ?? {};
256
+ const expected = [
257
+ ['compositor.turnBudget.budgetFraction', 0.6],
258
+ ['compositor.turnBudget.minContextFraction', 0.18],
259
+ ['compositor.warming.protectedFloorEnabled', true],
260
+ ['compositor.warming.shapedWarmupDecay', true],
261
+ ['compositor.adjacency.enabled', true],
262
+ ['compositor.adjacency.boostMultiplier', 1.3],
263
+ ['compositor.adjacency.maxLookback', 5],
264
+ ['compositor.adjacency.maxClockDeltaMin', 10],
265
+ ['compositor.adjacency.evictionGuardMessages', 3],
266
+ ['compositor.adjacency.evictionGuardTokenCap', 4000],
267
+ ];
268
+
269
+ for (const [pathKey, value] of expected) {
270
+ const actual = get({ compositor }, pathKey);
271
+ const ok = actual === value;
272
+ recommended(ok ? 'ok' : 'warn', pathKey,
273
+ ok
274
+ ? `${pathKey} is recommended 0.9.4 value ${JSON.stringify(value)}`
275
+ : `${pathKey} is ${JSON.stringify(actual)}; recommended ${JSON.stringify(value)} for 0.9.4 recall-surface protection`);
276
+ }
277
+
278
+ const dims = config.embedding?.dims;
279
+ const dimensions = config.embedding?.dimensions;
280
+ if (dims != null && dimensions != null && dims !== dimensions) {
281
+ recommended('warn', 'embedding.dims-consistency', `embedding.dims (${dims}) and embedding.dimensions (${dimensions}) differ; keep them aligned while both aliases are supported`);
282
+ } else {
283
+ recommended('ok', 'embedding.dims-consistency', 'Embedding dimension aliases are absent or aligned');
284
+ }
228
285
  }
229
286
 
230
287
  function checkOpenClawRecommendations() {
@@ -254,6 +311,22 @@ function checkOpenClawRecommendations() {
254
311
  else recommended(ok ? 'ok' : 'warn', pathKey, message, details);
255
312
  }
256
313
 
314
+ const legacyMemorySearch = get(openclaw, 'agents.defaults.memorySearch');
315
+ recommended(legacyMemorySearch == null ? 'ok' : 'warn',
316
+ 'agents.defaults.memorySearch',
317
+ legacyMemorySearch == null
318
+ ? 'Legacy agents.defaults.memorySearch is unset'
319
+ : `Legacy agents.defaults.memorySearch is ${JSON.stringify(legacyMemorySearch)}; HyperMem supersedes this path and it should be unset to avoid stale operator assumptions`,
320
+ legacyMemorySearch == null ? {} : { command: unsetCommand('agents.defaults.memorySearch') });
321
+
322
+ const maxActiveTranscriptBytes = get(openclaw, 'agents.defaults.compaction.maxActiveTranscriptBytes');
323
+ recommended(maxActiveTranscriptBytes == null ? 'ok' : 'warn',
324
+ 'agents.defaults.compaction.maxActiveTranscriptBytes',
325
+ maxActiveTranscriptBytes == null
326
+ ? 'agents.defaults.compaction.maxActiveTranscriptBytes is unset, as recommended for HyperMem-managed compaction'
327
+ : `agents.defaults.compaction.maxActiveTranscriptBytes is ${JSON.stringify(maxActiveTranscriptBytes)}; recommended unset so OpenClaw transcript rotation does not fight HyperMem fences`,
328
+ maxActiveTranscriptBytes == null ? {} : { command: unsetCommand('agents.defaults.compaction.maxActiveTranscriptBytes') });
329
+
257
330
  const injection = get(openclaw, 'agents.defaults.contextInjection');
258
331
  if (injection == null || injection === 'always' || injection === 'continuation-skip') {
259
332
  recommended('ok', 'agents.defaults.contextInjection', `Context injection mode is ${JSON.stringify(injection ?? 'default')}`);
@@ -367,6 +440,7 @@ if (openclawRead.value) {
367
440
  checkOpenClawRecommendations();
368
441
  checkModels();
369
442
  }
443
+ checkRecallSurfaceRecommendations();
370
444
  checkDataDir();
371
445
  checkRuntimePlugins();
372
446
 
@@ -39,9 +39,28 @@ const flags = {
39
39
  health: args.includes('--health'),
40
40
  master: args.includes('--master') || args.includes('--planks'),
41
41
  help: args.includes('--help') || args.includes('-h'),
42
+ repairReferencedNoise: args.includes('--repair-referenced-noise'),
43
+ fleetRepair: args.includes('--fleet-repair'),
42
44
  agent: null,
43
45
  };
44
46
 
47
+ function numericArg(name, fallback) {
48
+ const idx = args.indexOf(name);
49
+ if (idx === -1 || !args[idx + 1]) return fallback;
50
+ const value = Number(args[idx + 1]);
51
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
52
+ }
53
+
54
+ const MAX_REPAIR_LIMIT = 500;
55
+ const repairLimit = Math.min(numericArg('--repair-limit', 100), MAX_REPAIR_LIMIT);
56
+ const DEFAULT_FLEET_AGENT_LIMIT = 20;
57
+ const DEFAULT_FLEET_CANDIDATES_PER_CONVERSATION = 500;
58
+ const DEFAULT_TOP_AGENTS = 10;
59
+ const fleetAgentLimit = numericArg('--fleet-agent-limit', DEFAULT_FLEET_AGENT_LIMIT);
60
+ const maxAgents = numericArg('--max-agents', fleetAgentLimit);
61
+ const maxCandidatesPerConversation = numericArg('--max-candidates-per-conversation', DEFAULT_FLEET_CANDIDATES_PER_CONVERSATION);
62
+ const topAgentsLimit = Math.max(1, numericArg('--top-agents', DEFAULT_TOP_AGENTS));
63
+
45
64
  const agentIdx = args.indexOf('--agent');
46
65
  if (agentIdx !== -1 && args[agentIdx + 1]) {
47
66
  flags.agent = args[agentIdx + 1];
@@ -59,11 +78,25 @@ Options:
59
78
  --agent <id> Scope metrics to a specific agent
60
79
  --json Output raw JSON instead of formatted summary
61
80
  --health Health checks only (exits 1 if any check fails)
81
+ --repair-referenced-noise Run conservative tree-safe referenced-noise compaction
82
+ --repair-limit <n> Max referenced-noise mutations per run (default: 100, capped at 500)
83
+ --fleet-repair Allow referenced-noise repair without --agent (off by default)
84
+ --fleet-agent-limit <n> Max agent DBs scanned in fleet maintenance summary (default: ${DEFAULT_FLEET_AGENT_LIMIT})
85
+ --max-agents <n> Alias for --fleet-agent-limit
86
+ --top-agents <n> Max agents shown in maintenance top list (default: ${DEFAULT_TOP_AGENTS})
87
+ --max-candidates-per-conversation <n>
88
+ Max noise candidates inspected per conversation in fleet mode (default: ${DEFAULT_FLEET_CANDIDATES_PER_CONVERSATION})
62
89
  -h, --help Show this help
63
90
  `);
64
91
  process.exit(0);
65
92
  }
66
93
 
94
+
95
+ if (flags.repairReferencedNoise && !flags.agent && !flags.fleetRepair) {
96
+ console.error('Error: --repair-referenced-noise requires --agent <id>. Use --fleet-repair only for explicit fleet-wide repair.');
97
+ process.exit(2);
98
+ }
99
+
67
100
  // ── Resolve config and data directory ────────────────────────────
68
101
  function readJsonIfExists(filePath) {
69
102
  if (!existsSync(filePath)) return null;
@@ -141,6 +174,38 @@ function resolveRuntimeConfig() {
141
174
  const runtime = resolveRuntimeConfig();
142
175
  const dataDir = runtime.dataDir;
143
176
 
177
+ function nestedValue(obj, dotted) {
178
+ return dotted.split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), obj);
179
+ }
180
+
181
+ function collectRecallSurfaceConfig(config) {
182
+ const checks = [
183
+ ['turnBudget.budgetFraction', 0.6],
184
+ ['turnBudget.minContextFraction', 0.18],
185
+ ['warming.protectedFloorEnabled', true],
186
+ ['warming.shapedWarmupDecay', true],
187
+ ['adjacency.enabled', true],
188
+ ['adjacency.boostMultiplier', 1.3],
189
+ ['adjacency.maxLookback', 5],
190
+ ['adjacency.maxClockDeltaMin', 10],
191
+ ['adjacency.evictionGuardMessages', 3],
192
+ ['adjacency.evictionGuardTokenCap', 4000],
193
+ ];
194
+ const compositor = config.compositor ?? {};
195
+ const items = checks.map(([pathKey, expected]) => {
196
+ const actual = nestedValue(compositor, pathKey);
197
+ return { path: `compositor.${pathKey}`, expected, actual: actual ?? null, ok: actual === expected };
198
+ });
199
+ const missing = items.filter(item => !item.ok);
200
+ return {
201
+ status: missing.length === 0 ? 'ok' : 'attention',
202
+ ok: items.length - missing.length,
203
+ total: items.length,
204
+ missing,
205
+ items,
206
+ };
207
+ }
208
+
144
209
  if (!existsSync(dataDir)) {
145
210
  console.error(`Error: data directory not found: ${dataDir}`);
146
211
  console.error('Is HyperMem installed? Set HYPERMEM_DATA_DIR if using a custom path.');
@@ -216,12 +281,121 @@ function findMessageDb(agentId) {
216
281
  return null;
217
282
  }
218
283
 
219
- function collectMessageStats(agentId) {
284
+
285
+ function listMessageDbs(agentId, limit = Infinity) {
220
286
  const agentsDir = join(dataDir, 'agents');
221
- if (!existsSync(agentsDir)) return { agentsWithMessages: 0, totalMessages: 0, newestMessageAt: null };
287
+ if (!existsSync(agentsDir)) return { items: [], totalAvailable: 0, skipped: 0, truncated: false };
222
288
  const agents = agentId ? [agentId] : readdirSync(agentsDir, { withFileTypes: true })
289
+ .filter(d => d.isDirectory())
290
+ .map(d => d.name)
291
+ .sort();
292
+ const existing = agents
293
+ .map(agent => ({ agent, path: join(agentsDir, agent, 'messages.db') }))
294
+ .filter(item => existsSync(item.path));
295
+ const boundedLimit = Number.isFinite(limit) ? Math.max(0, limit) : Infinity;
296
+ const items = existing.slice(0, boundedLimit);
297
+ return {
298
+ items,
299
+ totalAvailable: existing.length,
300
+ skipped: Math.max(0, existing.length - items.length),
301
+ truncated: existing.length > items.length,
302
+ };
303
+ }
304
+
305
+ function emptyReferencedNoiseHealth() {
306
+ return {
307
+ status: 'ok',
308
+ agentsScanned: 0,
309
+ conversationsScanned: 0,
310
+ noiseCandidates: 0,
311
+ referencedNoise: 0,
312
+ parentReferencedNoise: 0,
313
+ contextReferencedNoise: 0,
314
+ snapshotReferencedNoise: 0,
315
+ otherReferencedNoise: 0,
316
+ topAgents: [],
317
+ sampleRefs: [],
318
+ repair: null,
319
+ truncated: false,
320
+ agentsAvailable: 0,
321
+ agentsSkipped: 0,
322
+ caps: null,
323
+ };
324
+ }
325
+
326
+ async function collectReferencedNoiseHealth(agentId, repair = false) {
327
+ const health = emptyReferencedNoiseHealth();
328
+ const fleetMode = !agentId;
329
+ const agentLimit = fleetMode ? maxAgents : Infinity;
330
+ const candidateLimit = fleetMode ? maxCandidatesPerConversation : Infinity;
331
+ health.caps = fleetMode
332
+ ? { agents: agentLimit, candidatesPerConversation: candidateLimit, topAgents: topAgentsLimit }
333
+ : null;
334
+ const distPath = join(root, 'dist', 'proactive-pass.js');
335
+ if (!existsSync(distPath)) {
336
+ health.status = 'unknown';
337
+ health.error = 'dist/proactive-pass.js missing; run npm run build';
338
+ return health;
339
+ }
340
+ const { collectReferencedNoiseDebt, runTreeSafeNoiseCompaction } = await import(distPath);
341
+ const perAgent = [];
342
+ const repairs = [];
343
+
344
+ const messageDbs = listMessageDbs(agentId, agentLimit);
345
+ health.agentsAvailable = messageDbs.totalAvailable;
346
+ health.agentsSkipped = messageDbs.skipped;
347
+ health.truncated = messageDbs.truncated;
348
+
349
+ for (const item of messageDbs.items) {
350
+ const db = openDb(item.path, `${item.agent} messages.db`, false);
351
+ if (!db) continue;
352
+ try {
353
+ if (repair) {
354
+ const repaired = runTreeSafeNoiseCompaction(db, undefined, 20, repairLimit);
355
+ repairs.push({ agent: item.agent, ...repaired });
356
+ }
357
+ const debt = collectReferencedNoiseDebt(db, undefined, 20, candidateLimit);
358
+ health.agentsScanned += 1;
359
+ health.conversationsScanned += debt.conversationsScanned;
360
+ health.noiseCandidates += debt.noiseCandidates;
361
+ health.referencedNoise += debt.referencedNoise;
362
+ health.parentReferencedNoise += debt.parentReferencedNoise;
363
+ health.contextReferencedNoise += debt.contextReferencedNoise;
364
+ health.snapshotReferencedNoise += debt.snapshotReferencedNoise;
365
+ health.otherReferencedNoise += debt.otherReferencedNoise;
366
+ for (const ref of debt.sampleRefs) if (health.sampleRefs.length < 12) health.sampleRefs.push(`${item.agent}:${ref}`);
367
+ if (debt.referencedNoise > 0) perAgent.push({ agent: item.agent, referencedNoise: debt.referencedNoise, parentReferencedNoise: debt.parentReferencedNoise });
368
+ } finally {
369
+ try { db.close(); } catch {}
370
+ }
371
+ }
372
+
373
+ health.topAgents = perAgent
374
+ .sort((a, b) => b.referencedNoise - a.referencedNoise)
375
+ .slice(0, topAgentsLimit);
376
+ health.status = health.referencedNoise > 0 ? 'attention' : 'ok';
377
+ if (repair) {
378
+ health.repair = {
379
+ limit: repairLimit,
380
+ agentsAttempted: repairs.length,
381
+ deleted: repairs.reduce((sum, item) => sum + (item.deleted || 0), 0),
382
+ reparented: repairs.reduce((sum, item) => sum + (item.reparented || 0), 0),
383
+ skippedBlocked: repairs.reduce((sum, item) => sum + (item.skippedBlocked || 0), 0),
384
+ skippedRoot: repairs.reduce((sum, item) => sum + (item.skippedRoot || 0), 0),
385
+ failures: repairs.filter(item => item.fkCheck && item.fkCheck !== 'none'),
386
+ };
387
+ }
388
+ return health;
389
+ }
390
+
391
+ function collectMessageStats(agentId) {
392
+ const agentsDir = join(dataDir, 'agents');
393
+ if (!existsSync(agentsDir)) return { agentsWithMessages: 0, totalMessages: 0, newestMessageAt: null, agentsAvailable: 0, agentsSkipped: 0, truncated: false };
394
+ const allAgents = agentId ? [agentId] : readdirSync(agentsDir, { withFileTypes: true })
223
395
  .filter(d => d.isDirectory())
224
396
  .map(d => d.name);
397
+ const limit = agentId ? Infinity : maxAgents;
398
+ const agents = allAgents.slice(0, Number.isFinite(limit) ? Math.max(0, limit) : undefined);
225
399
  let agentsWithMessages = 0;
226
400
  let totalMessages = 0;
227
401
  let newestMessageAt = null;
@@ -237,7 +411,14 @@ function collectMessageStats(agentId) {
237
411
  if (newest && (!newestMessageAt || newest > newestMessageAt)) newestMessageAt = newest;
238
412
  try { db.close(); } catch {}
239
413
  }
240
- return { agentsWithMessages, totalMessages, newestMessageAt };
414
+ return {
415
+ agentsWithMessages,
416
+ totalMessages,
417
+ newestMessageAt,
418
+ agentsAvailable: allAgents.length,
419
+ agentsSkipped: Math.max(0, allAgents.length - agents.length),
420
+ truncated: allAgents.length > agents.length,
421
+ };
241
422
  }
242
423
 
243
424
  function getVectorDimensions(vectorDb, tableName) {
@@ -252,7 +433,35 @@ function getQuickCheck(db) {
252
433
  return row ? Object.values(row)[0] : 'unknown';
253
434
  }
254
435
 
255
- function collectMasterHealth(libraryDb, vectorDb, mainDb) {
436
+ function fileContains(filePath, text) {
437
+ try {
438
+ return existsSync(filePath) && readFileSync(filePath, 'utf8').includes(text);
439
+ } catch {
440
+ return false;
441
+ }
442
+ }
443
+
444
+ function collectHistoryQueryHealth(mainDb) {
445
+ const coreDts = join(root, 'dist', 'index.d.ts');
446
+ const memoryPluginJs = join(root, 'memory-plugin', 'dist', 'index.js');
447
+ const coreApi = fileContains(coreDts, 'queryHistory(query: HistoryQuery): HistoryQueryResult');
448
+ const pluginTool = fileContains(memoryPluginJs, 'history_query') && fileContains(memoryPluginJs, 'history.query');
449
+ const telemetry = fileContains(memoryPluginJs, 'history-query') && fileContains(memoryPluginJs, 'HYPERMEM_TELEMETRY_PATH');
450
+ const messagesTable = safeGet(mainDb, `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'messages'`)?.sql ?? '';
451
+ const fencesTable = safeGet(mainDb, `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'compaction_fences'`)?.sql ?? '';
452
+ const schemaReady = ['tool_calls', 'tool_results', 'context_id', 'topic_id'].every(col => messagesTable.includes(col))
453
+ && fencesTable.includes('fence_message_id');
454
+
455
+ return {
456
+ status: coreApi && pluginTool && telemetry && schemaReady ? 'ok' : 'fail',
457
+ coreApi,
458
+ pluginTool,
459
+ telemetry,
460
+ schemaReady,
461
+ };
462
+ }
463
+
464
+ async function collectMasterHealth(libraryDb, vectorDb, mainDb) {
256
465
  const agentClause = flags.agent ? 'AND agent_id = ?' : '';
257
466
  const params = flags.agent ? [flags.agent] : [];
258
467
 
@@ -374,11 +583,15 @@ function collectMasterHealth(libraryDb, vectorDb, mainDb) {
374
583
  const dbStatus = libraryQuickCheck === 'ok' && (vectorQuickCheck === 'ok' || vectorQuickCheck === 'missing') && (mainQuickCheck === 'ok' || mainQuickCheck === 'missing') ? 'ok' : 'fail';
375
584
 
376
585
  const messageStats = collectMessageStats(flags.agent);
586
+ const historyQueryHealth = collectHistoryQueryHealth(mainDb);
587
+ const recallSurfaceConfig = collectRecallSurfaceConfig(runtime.config);
377
588
  const totalTurns = safeGet(libraryDb, `SELECT COUNT(*) AS count FROM output_metrics WHERE 1=1 ${agentClause}`, params)?.count ?? 0;
378
589
  const avgLatency = safeGet(libraryDb, `SELECT AVG(latency_ms) AS value FROM output_metrics WHERE latency_ms IS NOT NULL ${agentClause}`, params)?.value ?? null;
379
590
  const avgInput = safeGet(libraryDb, `SELECT AVG(input_tokens) AS value FROM output_metrics WHERE input_tokens IS NOT NULL ${agentClause}`, params)?.value ?? null;
380
591
  const avgOutput = safeGet(libraryDb, `SELECT AVG(output_tokens) AS value FROM output_metrics WHERE output_tokens IS NOT NULL ${agentClause}`, params)?.value ?? null;
381
592
 
593
+ const referencedNoiseHealth = await collectReferencedNoiseHealth(flags.agent, flags.repairReferencedNoise);
594
+
382
595
  const issues = [];
383
596
  if (dbStatus === 'fail') issues.push('database quick_check failed');
384
597
  if (dimensionMatches === 'fail') issues.push('vector table dimensions do not match configured embedding dimensions');
@@ -386,8 +599,12 @@ function collectMasterHealth(libraryDb, vectorDb, mainDb) {
386
599
  if (episodeStatus === 'warn') issues.push(`episode vector coverage low (${episodeCoverage}%)`);
387
600
  if (knowledgeStatus === 'warn') issues.push(`knowledge vector coverage low (${knowledgeCoverage}%)`);
388
601
  if (!vectorDb) issues.push('shared vectors.db missing');
602
+ if (historyQueryHealth.status !== 'ok') issues.push('history.query surface incomplete');
603
+ if (recallSurfaceConfig.status !== 'ok') issues.push(`0.9.4 recall-surface config incomplete (${recallSurfaceConfig.ok}/${recallSurfaceConfig.total} recommended knobs)`);
604
+ if (referencedNoiseHealth.status === 'attention') issues.push(`referenced-noise compaction debt (${referencedNoiseHealth.referencedNoise} messages${referencedNoiseHealth.truncated ? ', bounded fleet sample' : ''})`);
605
+ if (referencedNoiseHealth.status === 'unknown') issues.push(`referenced-noise health unknown: ${referencedNoiseHealth.error}`);
389
606
 
390
- const overall = issues.length === 0 ? 'healthy' : (dbStatus === 'fail' || dimensionMatches === 'fail' ? 'degraded' : 'attention');
607
+ const overall = issues.length === 0 ? 'healthy' : (dbStatus === 'fail' || dimensionMatches === 'fail' || historyQueryHealth.status === 'fail' ? 'degraded' : 'attention');
391
608
 
392
609
  return {
393
610
  snapshotAt: new Date().toISOString(),
@@ -405,6 +622,7 @@ function collectMasterHealth(libraryDb, vectorDb, mainDb) {
405
622
  baseUrl: runtime.embedding.openaiBaseUrl ?? runtime.embedding.ollamaUrl ?? runtime.embedding.geminiBaseUrl ?? null,
406
623
  },
407
624
  reranker: runtime.config.reranker ? stripSecretFields(runtime.config.reranker) : null,
625
+ recallSurface: recallSurfaceConfig,
408
626
  },
409
627
  databases: {
410
628
  library: { path: libraryDbPath, ...fileInfo(libraryDbPath), quickCheck: libraryQuickCheck },
@@ -437,6 +655,12 @@ function collectMasterHealth(libraryDb, vectorDb, mainDb) {
437
655
  avgOutputTokens: avgOutput == null ? null : Math.round(avgOutput),
438
656
  semanticDiagnosticsPersisted: false,
439
657
  },
658
+ maintenance: {
659
+ referencedNoise: referencedNoiseHealth,
660
+ },
661
+ querySurfaces: {
662
+ historyQuery: historyQueryHealth,
663
+ },
440
664
  };
441
665
  }
442
666
 
@@ -461,6 +685,11 @@ function formatMasterHealth(h) {
461
685
  lines.push(` embed: ${h.config.embedding.provider ?? 'unknown'} / ${h.config.embedding.model ?? 'unknown'}${h.config.embedding.dimensions ? ` (${h.config.embedding.dimensions}d)` : ''}${h.config.embedding.batchSize ? ` batch=${h.config.embedding.batchSize}` : ''}`);
462
686
  if (h.config.embedding.baseUrl) lines.push(` base: ${h.config.embedding.baseUrl}`);
463
687
  lines.push(` rerank: ${h.config.reranker?.provider ?? 'none'}`);
688
+ const rs = h.config.recallSurface;
689
+ lines.push(` recall: ${icon(rs.status === 'ok' ? 'ok' : 'warn')} 0.9.4 surface ${rs.ok}/${rs.total} recommended knobs`);
690
+ for (const item of rs.missing.slice(0, 5)) {
691
+ lines.push(` missing ${item.path}=${JSON.stringify(item.expected)}`);
692
+ }
464
693
 
465
694
  lines.push('');
466
695
  lines.push('## Databases');
@@ -475,7 +704,7 @@ function formatMasterHealth(h) {
475
704
  lines.push(` episodes: ${h.memory.episodes.eligible.toLocaleString()} significant / ${h.memory.episodes.total.toLocaleString()} total`);
476
705
  lines.push(` knowledge:${String(h.memory.knowledge.active.toLocaleString()).padStart(7)} active`);
477
706
  lines.push(` chunks: ${h.memory.docChunks.total.toLocaleString()}`);
478
- lines.push(` messages: ${h.memory.messages.totalMessages.toLocaleString()} across ${h.memory.messages.agentsWithMessages} agent DBs${h.memory.messages.newestMessageAt ? `, newest ${h.memory.messages.newestMessageAt}` : ''}`);
707
+ lines.push(` messages: ${h.memory.messages.totalMessages.toLocaleString()} across ${h.memory.messages.agentsWithMessages} agent DBs${h.memory.messages.truncated ? `/${h.memory.messages.agentsAvailable} bounded` : ''}${h.memory.messages.newestMessageAt ? `, newest ${h.memory.messages.newestMessageAt}` : ''}`);
479
708
 
480
709
  lines.push('');
481
710
  lines.push('## Vector coverage');
@@ -503,6 +732,25 @@ function formatMasterHealth(h) {
503
732
  }
504
733
  lines.push(' semantic recall counts: runtime log only, not persisted yet');
505
734
 
735
+ lines.push('');
736
+ lines.push('## Maintenance');
737
+ const rn = h.maintenance.referencedNoise;
738
+ lines.push(` ${icon(rn.status === 'ok' ? 'ok' : rn.status === 'attention' ? 'warn' : 'fail')} referenced-noise debt=${rn.referencedNoise.toLocaleString()} parent=${rn.parentReferencedNoise.toLocaleString()} context=${rn.contextReferencedNoise.toLocaleString()} snapshot=${rn.snapshotReferencedNoise.toLocaleString()} agents=${rn.agentsScanned}${rn.truncated ? `/${rn.agentsAvailable} bounded` : ''}`);
739
+ if (rn.truncated) {
740
+ lines.push(` fleet scan bounded: skipped ${rn.agentsSkipped} agent DBs; rerun with --fleet-agent-limit ${rn.agentsAvailable} for a full scan`);
741
+ }
742
+ if (rn.topAgents.length > 0) {
743
+ lines.push(` top agents: ${rn.topAgents.map(item => `${item.agent}=${Number(item.referencedNoise).toLocaleString()}`).join(' ')}`);
744
+ }
745
+ if (rn.repair) {
746
+ lines.push(` repair: limit=${rn.repair.limit} deleted=${rn.repair.deleted} reparented=${rn.repair.reparented} skippedBlocked=${rn.repair.skippedBlocked} skippedRoot=${rn.repair.skippedRoot}`);
747
+ }
748
+
749
+ lines.push('');
750
+ lines.push('## Query surfaces');
751
+ const hq = h.querySurfaces.historyQuery;
752
+ lines.push(` ${icon(hq.status)} history.query coreApi=${hq.coreApi ? 'yes' : 'no'} pluginTool=${hq.pluginTool ? 'yes' : 'no'} telemetry=${hq.telemetry ? 'yes' : 'no'} schema=${hq.schemaReady ? 'yes' : 'no'}`);
753
+
506
754
  return lines.join('\n');
507
755
  }
508
756
 
@@ -538,7 +786,7 @@ if (vectorDb) {
538
786
 
539
787
  try {
540
788
  if (flags.master) {
541
- const master = collectMasterHealth(libraryDb, vectorDb, mainDb);
789
+ const master = await collectMasterHealth(libraryDb, vectorDb, mainDb);
542
790
  if (flags.json) console.log(JSON.stringify(master, null, 2));
543
791
  else console.log(formatMasterHealth(master));
544
792
  process.exit(master.overall === 'degraded' ? 1 : 0);
@@ -16,6 +16,8 @@ export interface AdaptiveLifecycleInput {
16
16
  pressureFraction?: number;
17
17
  /** Number of user turns observed in the session. */
18
18
  userTurnCount?: number;
19
+ /** Number of topic-bearing (substantive) user turns observed in the session. */
20
+ topicBearingTurnCount?: number;
19
21
  /** True when the incoming user turn explicitly starts with /new. */
20
22
  explicitNewSession?: boolean;
21
23
  /** Topic-shift confidence from the detector, 0..1. */
@@ -27,6 +29,33 @@ export interface AdaptiveLifecycleInput {
27
29
  /** Parent-session user turns observed when the fork was prepared. */
28
30
  forkedParentUserTurnCount?: number;
29
31
  }
32
+ /**
33
+ * Minimal message shape for the lightweight topic-bearing classifier.
34
+ * Used by compose paths to count substantive user turns from existing
35
+ * message data without content telemetry or schema migration.
36
+ */
37
+ export interface TopicBearingMessageLike {
38
+ role: string;
39
+ textContent: string | null;
40
+ isHeartbeat?: boolean;
41
+ }
42
+ /**
43
+ * Determine whether a user turn is "topic-bearing" (substantive).
44
+ *
45
+ * Heartbeat, empty, and small-talk turns are NOT topic-bearing and do not
46
+ * extend the warmup window. This is intentionally a lightweight heuristic:
47
+ * no topic-detector architecture change, no model calls.
48
+ *
49
+ * Mirrors the plugin afterTurn gradient semantics so compose-path band
50
+ * decisions stay consistent with afterTurn-path band decisions.
51
+ */
52
+ export declare function isTopicBearingTurn(msg: TopicBearingMessageLike): boolean;
53
+ /**
54
+ * Count topic-bearing turns in a message array.
55
+ *
56
+ * Pure helper: no side effects, no store access. Returns 0 for empty arrays.
57
+ */
58
+ export declare function countTopicBearingTurns(messages: TopicBearingMessageLike[]): number;
30
59
  /**
31
60
  * Eviction-pipeline step labels. The order in `AdaptiveEvictionPlan.steps`
32
61
  * is the order the compose-window cluster-drop path should attempt them.
@@ -47,6 +76,14 @@ export interface AdaptiveEvictionPlan {
47
76
  /** Ballast-reduction steps run before any cluster drop. Always true today. */
48
77
  preferBallastFirst: boolean;
49
78
  }
79
+ interface ProtectedWarmingMetadata {
80
+ /** Whether warming is currently protected by a hard floor. True for high/critical. */
81
+ isProtected: boolean;
82
+ /** The floor value warming cannot drop below. 0 when not protected. */
83
+ floor: number;
84
+ /** Human-readable explanation. */
85
+ reason: string;
86
+ }
50
87
  export interface AdaptiveLifecyclePolicy {
51
88
  band: AdaptiveLifecycleBand;
52
89
  pressureFraction: number;
@@ -59,6 +96,7 @@ export interface AdaptiveLifecyclePolicy {
59
96
  enableTopicCentroidEviction: boolean;
60
97
  triggerProactiveCompaction: boolean;
61
98
  evictionPlan: AdaptiveEvictionPlan;
99
+ protectedWarmingMetadata: ProtectedWarmingMetadata;
62
100
  reasons: string[];
63
101
  }
64
102
  /**
@@ -78,4 +116,5 @@ export declare function resolveAdaptiveEvictionPlan(band: AdaptiveLifecycleBand)
78
116
  * Resolve the adaptive lifecycle posture for one compose/afterTurn cycle.
79
117
  */
80
118
  export declare function resolveAdaptiveLifecyclePolicy(input?: AdaptiveLifecycleInput): AdaptiveLifecyclePolicy;
119
+ export {};
81
120
  //# sourceMappingURL=adaptive-lifecycle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adaptive-lifecycle.d.ts","sourceRoot":"","sources":["../src/adaptive-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,qBAAqB,GAC7B,WAAW,GACX,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,UAAU,CAAC;AAEf,MAAM,WAAW,sBAAsB;IACrC,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,sDAAsD;IACtD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0EAA0E;IAC1E,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,qEAAqE;IACrE,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,qBAAqB,GACrB,kBAAkB,GAClB,0BAA0B,GAC1B,qBAAqB,CAAC;AAE1B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACvC,wEAAwE;IACxE,oBAAoB,EAAE,OAAO,CAAC;IAC9B,8EAA8E;IAC9E,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,yBAAyB,EAAE,MAAM,CAAC;IAClC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,wBAAwB,EAAE,MAAM,CAAC;IACjC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2BAA2B,EAAE,OAAO,CAAC;IACrC,0BAA0B,EAAE,OAAO,CAAC;IACpC,YAAY,EAAE,oBAAoB,CAAC;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CAsB7F;AAyGD;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,KAAK,GAAE,sBAA2B,GACjC,uBAAuB,CAoBzB"}
1
+ {"version":3,"file":"adaptive-lifecycle.d.ts","sourceRoot":"","sources":["../src/adaptive-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,qBAAqB,GAC7B,WAAW,GACX,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,UAAU,CAAC;AAEf,MAAM,WAAW,sBAAsB;IACrC,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,sDAAsD;IACtD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0EAA0E;IAC1E,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,qEAAqE;IACrE,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,uBAAuB,GAAG,OAAO,CAmBxE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,uBAAuB,EAAE,GAAG,MAAM,CAElF;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,qBAAqB,GACrB,kBAAkB,GAClB,0BAA0B,GAC1B,qBAAqB,CAAC;AAE1B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACvC,wEAAwE;IACxE,oBAAoB,EAAE,OAAO,CAAC;IAC9B,8EAA8E;IAC9E,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,UAAU,wBAAwB;IAChC,sFAAsF;IACtF,WAAW,EAAE,OAAO,CAAC;IACrB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,yBAAyB,EAAE,MAAM,CAAC;IAClC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,wBAAwB,EAAE,MAAM,CAAC;IACjC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2BAA2B,EAAE,OAAO,CAAC;IACrC,0BAA0B,EAAE,OAAO,CAAC;IACpC,YAAY,EAAE,oBAAoB,CAAC;IACnC,wBAAwB,EAAE,wBAAwB,CAAC;IACnD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CAsB7F;AA4ID;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,KAAK,GAAE,sBAA2B,GACjC,uBAAuB,CAqBzB"}