@psiclawops/hypermem 0.6.2 → 0.8.0

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 (98) hide show
  1. package/ARCHITECTURE.md +31 -39
  2. package/README.md +20 -14
  3. package/bin/hypermem-status.mjs +1 -1
  4. package/dist/background-indexer.d.ts +14 -3
  5. package/dist/background-indexer.d.ts.map +1 -1
  6. package/dist/background-indexer.js +135 -27
  7. package/dist/budget-policy.d.ts +22 -0
  8. package/dist/budget-policy.d.ts.map +1 -0
  9. package/dist/budget-policy.js +27 -0
  10. package/dist/cache.d.ts +11 -0
  11. package/dist/cache.d.ts.map +1 -1
  12. package/dist/compositor-utils.d.ts +31 -0
  13. package/dist/compositor-utils.d.ts.map +1 -0
  14. package/dist/compositor-utils.js +47 -0
  15. package/dist/compositor.d.ts +163 -1
  16. package/dist/compositor.d.ts.map +1 -1
  17. package/dist/compositor.js +862 -130
  18. package/dist/content-hash.d.ts +43 -0
  19. package/dist/content-hash.d.ts.map +1 -0
  20. package/dist/content-hash.js +75 -0
  21. package/dist/context-store.d.ts +54 -0
  22. package/dist/context-store.d.ts.map +1 -1
  23. package/dist/context-store.js +102 -0
  24. package/dist/contradiction-audit-store.d.ts +54 -0
  25. package/dist/contradiction-audit-store.d.ts.map +1 -0
  26. package/dist/contradiction-audit-store.js +88 -0
  27. package/dist/contradiction-detector.d.ts +78 -0
  28. package/dist/contradiction-detector.d.ts.map +1 -0
  29. package/dist/contradiction-detector.js +362 -0
  30. package/dist/contradiction-resolution-policy.d.ts +21 -0
  31. package/dist/contradiction-resolution-policy.d.ts.map +1 -0
  32. package/dist/contradiction-resolution-policy.js +17 -0
  33. package/dist/cross-agent.d.ts +1 -1
  34. package/dist/cross-agent.js +17 -17
  35. package/dist/degradation.d.ts +102 -0
  36. package/dist/degradation.d.ts.map +1 -0
  37. package/dist/degradation.js +141 -0
  38. package/dist/dreaming-promoter.d.ts +39 -1
  39. package/dist/dreaming-promoter.d.ts.map +1 -1
  40. package/dist/dreaming-promoter.js +70 -4
  41. package/dist/expertise-store.d.ts +129 -0
  42. package/dist/expertise-store.d.ts.map +1 -0
  43. package/dist/expertise-store.js +342 -0
  44. package/dist/fact-store.d.ts +15 -0
  45. package/dist/fact-store.d.ts.map +1 -1
  46. package/dist/fact-store.js +52 -5
  47. package/dist/index.d.ts +74 -8
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +407 -29
  50. package/dist/knowledge-lint.d.ts +2 -0
  51. package/dist/knowledge-lint.d.ts.map +1 -1
  52. package/dist/knowledge-lint.js +40 -1
  53. package/dist/library-schema.d.ts +7 -2
  54. package/dist/library-schema.d.ts.map +1 -1
  55. package/dist/library-schema.js +307 -2
  56. package/dist/message-store.d.ts +64 -1
  57. package/dist/message-store.d.ts.map +1 -1
  58. package/dist/message-store.js +137 -1
  59. package/dist/proactive-pass.d.ts +2 -2
  60. package/dist/proactive-pass.d.ts.map +1 -1
  61. package/dist/proactive-pass.js +66 -12
  62. package/dist/replay-recovery.d.ts +29 -0
  63. package/dist/replay-recovery.d.ts.map +1 -0
  64. package/dist/replay-recovery.js +82 -0
  65. package/dist/reranker.d.ts +95 -0
  66. package/dist/reranker.d.ts.map +1 -0
  67. package/dist/reranker.js +308 -0
  68. package/dist/schema.d.ts +1 -1
  69. package/dist/schema.d.ts.map +1 -1
  70. package/dist/schema.js +46 -1
  71. package/dist/seed.d.ts +1 -1
  72. package/dist/seed.js +1 -1
  73. package/dist/session-flusher.d.ts +4 -4
  74. package/dist/session-flusher.d.ts.map +1 -1
  75. package/dist/session-flusher.js +3 -3
  76. package/dist/spawn-context.d.ts +1 -1
  77. package/dist/spawn-context.js +1 -1
  78. package/dist/temporal-store.d.ts +1 -0
  79. package/dist/temporal-store.d.ts.map +1 -1
  80. package/dist/tool-artifact-store.d.ts +98 -0
  81. package/dist/tool-artifact-store.d.ts.map +1 -0
  82. package/dist/tool-artifact-store.js +244 -0
  83. package/dist/topic-detector.js +2 -2
  84. package/dist/topic-store.d.ts +6 -0
  85. package/dist/topic-store.d.ts.map +1 -1
  86. package/dist/topic-store.js +39 -0
  87. package/dist/topic-synthesizer.js +1 -1
  88. package/dist/trigger-registry.d.ts +1 -1
  89. package/dist/trigger-registry.js +4 -4
  90. package/dist/types.d.ts +239 -3
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/vector-store.d.ts +2 -1
  93. package/dist/vector-store.d.ts.map +1 -1
  94. package/dist/vector-store.js +3 -0
  95. package/dist/version.d.ts +10 -10
  96. package/dist/version.d.ts.map +1 -1
  97. package/dist/version.js +10 -10
  98. package/package.json +6 -4
@@ -5,7 +5,7 @@
5
5
  * All messages are stored in provider-neutral format.
6
6
  * This is the write-through layer: Redis → here.
7
7
  */
8
- import { getOrCreateActiveContext, updateContextHead } from './context-store.js';
8
+ import { getOrCreateActiveContext, updateContextHead, getArchivedContext } from './context-store.js';
9
9
  function nowIso() {
10
10
  return new Date().toISOString();
11
11
  }
@@ -332,6 +332,12 @@ export class MessageStore {
332
332
  *
333
333
  * Falls back to getRecentMessages if the head message has no parent chain
334
334
  * (e.g., legacy data before backfill).
335
+ *
336
+ * @boundary SHARED DAG PRIMITIVE — not for direct call at mining call sites.
337
+ * @policy See specs/DAG_HELPER_POLICY.md for operator-boundary rules.
338
+ * Use mineArchivedContext / mineArchivedContexts for archived context mining,
339
+ * and the active-composition paths for live session history. Direct call sites
340
+ * outside this class should be limited to exceptional diagnostic use.
335
341
  */
336
342
  getHistoryByDAGWalk(headMessageId, limit = 50) {
337
343
  try {
@@ -414,6 +420,136 @@ export class MessageStore {
414
420
  .get(conversationId);
415
421
  return row.count;
416
422
  }
423
+ /**
424
+ * Get the full message chain for an archived or forked context.
425
+ *
426
+ * Throws if the context does not exist or is active (not archived/forked).
427
+ * Returns an empty array if the context has no head message.
428
+ * Delegates to getHistoryByDAGWalk for the actual chain retrieval.
429
+ */
430
+ getArchivedChain(contextId, limit) {
431
+ const context = getArchivedContext(this.db, contextId);
432
+ if (!context) {
433
+ throw new Error('getArchivedChain: context must be archived or forked');
434
+ }
435
+ if (context.headMessageId === null) {
436
+ return [];
437
+ }
438
+ return this.getHistoryByDAGWalk(context.headMessageId, limit ?? 200);
439
+ }
440
+ // ─── Archived Mining (Phase 4 Sprint 2 / Sprint 3) ─────────────
441
+ /**
442
+ * Default maximum number of contextIds accepted by mineArchivedContexts.
443
+ * Callers may supply a lower value but not a higher one.
444
+ */
445
+ static ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX = 20;
446
+ /**
447
+ * Hard ceiling for mineArchivedContexts.
448
+ * Values above this are clamped to this number regardless of caller intent.
449
+ * This prevents unbounded DB fan-out on misconfigured or adversarial inputs.
450
+ */
451
+ static ARCHIVED_MULTI_CONTEXT_HARD_CEILING = 50;
452
+ // ─── Archived Mining (Phase 4 Sprint 2) ───────────────────────
453
+ /**
454
+ * Mine messages from a single archived or forked context.
455
+ *
456
+ * - Rejects active or missing contexts with a clear error.
457
+ * - Hard-caps limit at 200.
458
+ * - Defaults excludeHeartbeats to true.
459
+ * - Optionally filters by ftsQuery (client-side substring match for Sprint 3; SQL FTS is deferred).
460
+ * - Routes through getHistoryByDAGWalk for DAG-native retrieval.
461
+ * - Returns ArchivedMiningResult<StoredMessage[]> with isHistorical: true.
462
+ *
463
+ * This method does NOT widen active composition — it only operates on
464
+ * explicitly non-active (archived/forked) contexts.
465
+ */
466
+ mineArchivedContext(query) {
467
+ const { contextId, limit, excludeHeartbeats = true, ftsQuery } = query;
468
+ const context = getArchivedContext(this.db, contextId);
469
+ if (!context) {
470
+ throw new Error(`mineArchivedContext: context ${contextId} does not exist or is not archived/forked. ` +
471
+ `Only archived or forked contexts may be mined.`);
472
+ }
473
+ // Hard cap at 200
474
+ const effectiveLimit = Math.min(limit ?? 200, 200);
475
+ let messages = [];
476
+ if (context.headMessageId !== null) {
477
+ messages = this.getHistoryByDAGWalk(context.headMessageId, effectiveLimit);
478
+ }
479
+ // Apply heartbeat filter (default: exclude)
480
+ if (excludeHeartbeats) {
481
+ messages = messages.filter(m => !m.isHeartbeat);
482
+ }
483
+ // Client-side ftsQuery filter (substring match for Sprint 2)
484
+ if (ftsQuery && ftsQuery.trim().length > 0) {
485
+ const q = ftsQuery.trim().toLowerCase();
486
+ messages = messages.filter(m => (m.textContent ?? '').toLowerCase().includes(q));
487
+ }
488
+ return {
489
+ isHistorical: true,
490
+ contextId: context.id,
491
+ agentId: context.agentId,
492
+ sessionKey: context.sessionKey,
493
+ contextStatus: context.status,
494
+ contextUpdatedAt: context.updatedAt,
495
+ data: messages,
496
+ };
497
+ }
498
+ /**
499
+ * Mine messages from multiple archived or forked contexts.
500
+ *
501
+ * ## maxContexts gate (Phase 4 Sprint 3, Task 1)
502
+ * Accepts an optional `maxContexts` in opts to control how many contextIds
503
+ * are accepted in a single call:
504
+ * - Default: ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX (20).
505
+ * - Hard ceiling: ARCHIVED_MULTI_CONTEXT_HARD_CEILING (50).
506
+ * - A caller-supplied value above the hard ceiling is clamped to the ceiling
507
+ * (not rejected), so callers need not know the exact constant.
508
+ * - A caller-supplied value at or below the ceiling is used as-is.
509
+ * - If contextIds.length exceeds the effective max, this method THROWS
510
+ * immediately — it does NOT soft-skip or truncate.
511
+ *
512
+ * ## Other behaviors (unchanged from Sprint 2)
513
+ * - Soft-skips active or missing contextIds with a warning (does not throw).
514
+ * - Preserves input order in the result array.
515
+ * - Applies per-context limit and same filters as mineArchivedContext.
516
+ * - Returns one ArchivedMiningResult per valid archived context.
517
+ *
518
+ * This method does NOT widen active composition — it only operates on
519
+ * explicitly non-active (archived/forked) contexts.
520
+ */
521
+ mineArchivedContexts(contextIds, opts) {
522
+ // ── maxContexts gate ──────────────────────────────────────────────────
523
+ const { maxContexts: callerMax, ...perContextOpts } = opts ?? {};
524
+ const effectiveMax = callerMax !== undefined
525
+ ? Math.min(callerMax, MessageStore.ARCHIVED_MULTI_CONTEXT_HARD_CEILING)
526
+ : MessageStore.ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX;
527
+ if (contextIds.length > effectiveMax) {
528
+ throw new Error(`mineArchivedContexts: too many contextIds (${contextIds.length}). ` +
529
+ `Effective limit is ${effectiveMax} ` +
530
+ `(hard ceiling: ${MessageStore.ARCHIVED_MULTI_CONTEXT_HARD_CEILING}, ` +
531
+ `default: ${MessageStore.ARCHIVED_MULTI_CONTEXT_DEFAULT_MAX}). ` +
532
+ `Pass fewer contextIds or supply a higher maxContexts (max: ${MessageStore.ARCHIVED_MULTI_CONTEXT_HARD_CEILING}).`);
533
+ }
534
+ // ── end gate ─────────────────────────────────────────────────────────
535
+ const results = [];
536
+ for (const contextId of contextIds) {
537
+ const context = getArchivedContext(this.db, contextId);
538
+ if (!context) {
539
+ console.warn(`[hypermem:message-store] mineArchivedContexts: skipping contextId ${contextId} ` +
540
+ `— does not exist or is not archived/forked (may be active or missing).`);
541
+ continue;
542
+ }
543
+ try {
544
+ results.push(this.mineArchivedContext({ contextId, ...perContextOpts }));
545
+ }
546
+ catch (err) {
547
+ console.warn(`[hypermem:message-store] mineArchivedContexts: skipping contextId ${contextId} ` +
548
+ `— ${err.message}`);
549
+ }
550
+ }
551
+ return results;
552
+ }
417
553
  // ─── Helpers ─────────────────────────────────────────────────
418
554
  /**
419
555
  * Infer channel type from session key format.
@@ -42,7 +42,7 @@ export interface ToolDecayResult {
42
42
  * Deletions are wrapped in a single transaction. The FTS5 trigger handles
43
43
  * index cleanup automatically (msg_fts_ad fires on DELETE).
44
44
  */
45
- export declare function runNoiseSweep(db: DatabaseSync, conversationId: number, recentWindowSize?: number): NoiseSweepResult;
45
+ export declare function runNoiseSweep(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number): NoiseSweepResult;
46
46
  /**
47
47
  * Truncate oversized tool_results outside the recent window.
48
48
  *
@@ -59,5 +59,5 @@ export declare function runNoiseSweep(db: DatabaseSync, conversationId: number,
59
59
  *
60
60
  * Mutations are committed in a single transaction.
61
61
  */
62
- export declare function runToolDecay(db: DatabaseSync, conversationId: number, recentWindowSize?: number): ToolDecayResult;
62
+ export declare function runToolDecay(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number): ToolDecayResult;
63
63
  //# sourceMappingURL=proactive-pass.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proactive-pass.d.ts","sourceRoot":"","sources":["../src/proactive-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AA6CD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,GAC5B,gBAAgB,CA2ElB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,GAC5B,eAAe,CAiGjB"}
1
+ {"version":3,"file":"proactive-pass.d.ts","sourceRoot":"","sources":["../src/proactive-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAwFD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,EAC7B,aAAa,GAAE,MAAiB,GAC/B,gBAAgB,CAwFlB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,EAC7B,aAAa,GAAE,MAAiB,GAC/B,eAAe,CAgHjB"}
@@ -44,6 +44,44 @@ function getMaxMessageIndex(db, conversationId) {
44
44
  .get(conversationId);
45
45
  return typeof row.max_index === 'number' ? row.max_index : -1;
46
46
  }
47
+ /**
48
+ * Filter candidate message ids down to rows that are safe to delete without
49
+ * violating HyperMem's FK edges.
50
+ *
51
+ * Current blockers:
52
+ * - summary_messages.message_id -> messages.id
53
+ * - messages.parent_id -> messages.id (child rows point at parent rows)
54
+ */
55
+ function getDeletableMessageIds(db, candidateIds) {
56
+ if (candidateIds.length === 0)
57
+ return { deletableIds: [], blockedIds: [] };
58
+ const placeholders = candidateIds.map(() => '?').join(', ');
59
+ const blocked = db
60
+ .prepare(`
61
+ SELECT DISTINCT id
62
+ FROM (
63
+ SELECT sm.message_id AS id
64
+ FROM summary_messages sm
65
+ WHERE sm.message_id IN (${placeholders})
66
+
67
+ UNION
68
+
69
+ SELECT parent.id AS id
70
+ FROM messages child
71
+ JOIN messages parent ON parent.id = child.parent_id
72
+ WHERE child.parent_id IN (${placeholders})
73
+ ) blocked
74
+ `)
75
+ .all(...candidateIds, ...candidateIds);
76
+ if (blocked.length === 0)
77
+ return { deletableIds: candidateIds, blockedIds: [] };
78
+ const blockedIds = blocked.map(row => row.id);
79
+ const blockedSet = new Set(blockedIds);
80
+ return {
81
+ deletableIds: candidateIds.filter(id => !blockedSet.has(id)),
82
+ blockedIds,
83
+ };
84
+ }
47
85
  /**
48
86
  * Decide if a message is noise based on content + is_heartbeat flag.
49
87
  *
@@ -73,7 +111,7 @@ function isNoiseMessage(textContent, isHeartbeat) {
73
111
  * Deletions are wrapped in a single transaction. The FTS5 trigger handles
74
112
  * index cleanup automatically (msg_fts_ad fires on DELETE).
75
113
  */
76
- export function runNoiseSweep(db, conversationId, recentWindowSize = 20) {
114
+ export function runNoiseSweep(db, conversationId, recentWindowSize = 20, maxCandidates = Infinity) {
77
115
  const ZERO = { messagesDeleted: 0, passType: 'noise_sweep' };
78
116
  try {
79
117
  const safeWindow = resolveSafeWindow(recentWindowSize);
@@ -101,11 +139,16 @@ export function runNoiseSweep(db, conversationId, recentWindowSize = 20) {
101
139
  .all(conversationId, cutoff);
102
140
  if (candidates.length === 0)
103
141
  return ZERO;
104
- // Filter to noise messages
105
- const toDelete = candidates.filter(row => isNoiseMessage(row.text_content, row.is_heartbeat));
106
- if (toDelete.length === 0)
142
+ // Filter to noise messages, respecting per-pass candidate cap
143
+ const toDelete = candidates.filter(row => isNoiseMessage(row.text_content, row.is_heartbeat)).slice(0, Number.isFinite(maxCandidates) ? maxCandidates : undefined);
144
+ if (toDelete.length === 0) {
107
145
  return ZERO;
108
- const ids = toDelete.map(r => r.id);
146
+ }
147
+ const candidateIds = toDelete.map(r => r.id);
148
+ const { deletableIds: ids, blockedIds } = getDeletableMessageIds(db, candidateIds);
149
+ if (ids.length === 0) {
150
+ return ZERO;
151
+ }
109
152
  // Delete in a transaction; use chunked IN clauses to avoid
110
153
  // SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (default 999).
111
154
  let totalDeleted = 0;
@@ -126,6 +169,9 @@ export function runNoiseSweep(db, conversationId, recentWindowSize = 20) {
126
169
  db.prepare('ROLLBACK').run();
127
170
  throw innerErr;
128
171
  }
172
+ if (totalDeleted > 0) {
173
+ console.log(`[proactive-pass] Noise sweep conversation=${conversationId} candidates=${candidates.length} noise=${candidateIds.length} deleted=${totalDeleted} skippedReferenced=${blockedIds.length} cutoff=${cutoff}`);
174
+ }
129
175
  return { messagesDeleted: totalDeleted, passType: 'noise_sweep' };
130
176
  }
131
177
  catch (err) {
@@ -150,16 +196,18 @@ export function runNoiseSweep(db, conversationId, recentWindowSize = 20) {
150
196
  *
151
197
  * Mutations are committed in a single transaction.
152
198
  */
153
- export function runToolDecay(db, conversationId, recentWindowSize = 40) {
199
+ export function runToolDecay(db, conversationId, recentWindowSize = 40, maxCandidates = Infinity) {
154
200
  const ZERO = { messagesUpdated: 0, bytesFreed: 0, passType: 'tool_decay' };
155
201
  try {
156
202
  const safeWindow = resolveSafeWindow(recentWindowSize);
157
203
  const maxIndex = getMaxMessageIndex(db, conversationId);
158
- if (maxIndex < 0)
204
+ if (maxIndex < 0) {
159
205
  return ZERO;
206
+ }
160
207
  const cutoff = maxIndex - safeWindow;
161
- if (cutoff <= 0)
208
+ if (cutoff <= 0) {
162
209
  return ZERO;
210
+ }
163
211
  // Fetch messages with large tool_results outside the recent window.
164
212
  const candidates = db
165
213
  .prepare(`
@@ -171,11 +219,13 @@ export function runToolDecay(db, conversationId, recentWindowSize = 40) {
171
219
  AND length(tool_results) > 2000
172
220
  `)
173
221
  .all(conversationId, cutoff);
174
- if (candidates.length === 0)
222
+ if (candidates.length === 0) {
175
223
  return ZERO;
176
- // Build the update list by processing each candidate.
224
+ }
225
+ // Build the update list by processing each candidate, respecting per-pass cap.
226
+ const cappedCandidates = Number.isFinite(maxCandidates) ? candidates.slice(0, maxCandidates) : candidates;
177
227
  const updates = [];
178
- for (const row of candidates) {
228
+ for (const row of cappedCandidates) {
179
229
  let parsed;
180
230
  try {
181
231
  parsed = JSON.parse(row.tool_results);
@@ -207,8 +257,9 @@ export function runToolDecay(db, conversationId, recentWindowSize = 40) {
207
257
  updates.push({ id: row.id, newJson, savedBytes });
208
258
  }
209
259
  }
210
- if (updates.length === 0)
260
+ if (updates.length === 0) {
211
261
  return ZERO;
262
+ }
212
263
  let totalUpdated = 0;
213
264
  let totalBytesFreed = 0;
214
265
  db.prepare('BEGIN').run();
@@ -225,6 +276,9 @@ export function runToolDecay(db, conversationId, recentWindowSize = 40) {
225
276
  db.prepare('ROLLBACK').run();
226
277
  throw innerErr;
227
278
  }
279
+ if (totalUpdated > 0) {
280
+ console.log(`[proactive-pass] Tool decay conversation=${conversationId} candidates=${candidates.length} updated=${totalUpdated} bytesFreed=${totalBytesFreed} cutoff=${cutoff}`);
281
+ }
228
282
  return {
229
283
  messagesUpdated: totalUpdated,
230
284
  bytesFreed: totalBytesFreed,
@@ -0,0 +1,29 @@
1
+ import { type ReplayMarker, type ReplayState } from './degradation.js';
2
+ export interface ReplayRecoveryInputs {
3
+ currentState?: ReplayState | '' | null;
4
+ runtimeTokens: number;
5
+ redisTokens: number;
6
+ effectiveBudget: number;
7
+ }
8
+ export interface ReplayRecoveryDecision {
9
+ active: boolean;
10
+ shouldSkipCacheReplay: boolean;
11
+ trimTargetOverride?: number;
12
+ historyDepthCap?: number;
13
+ emittedMarker?: ReplayMarker;
14
+ emittedText?: string;
15
+ nextState: ReplayState | null;
16
+ }
17
+ export declare const REPLAY_RECOVERY_POLICY: {
18
+ readonly enterPressure: 0.8;
19
+ readonly exitPressure: 0.65;
20
+ readonly redisColdFraction: 0.2;
21
+ readonly enterTrimTarget: 0.2;
22
+ readonly stabilizingTrimTarget: 0.35;
23
+ readonly historyDepthCap: 60;
24
+ readonly redisFloorTokens: 500;
25
+ };
26
+ export declare function isColdRedisReplay(inputs: ReplayRecoveryInputs): boolean;
27
+ export declare function isReplayRecovered(inputs: ReplayRecoveryInputs): boolean;
28
+ export declare function decideReplayRecovery(inputs: ReplayRecoveryInputs): ReplayRecoveryDecision;
29
+ //# sourceMappingURL=replay-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-recovery.d.ts","sourceRoot":"","sources":["../src/replay-recovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,WAAW,GAAG,EAAE,GAAG,IAAI,CAAC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAC;CAC/B;AAED,eAAO,MAAM,sBAAsB;;;;;;;;CAQzB,CAAC;AAgBX,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAKvE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAKvE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,sBAAsB,CA+DzF"}
@@ -0,0 +1,82 @@
1
+ import { formatReplayMarker, } from './degradation.js';
2
+ export const REPLAY_RECOVERY_POLICY = {
3
+ enterPressure: 0.80,
4
+ exitPressure: 0.65,
5
+ redisColdFraction: 0.20,
6
+ enterTrimTarget: 0.20,
7
+ stabilizingTrimTarget: 0.35,
8
+ historyDepthCap: 60,
9
+ redisFloorTokens: 500,
10
+ };
11
+ function coldRedisThreshold(runtimeTokens) {
12
+ return Math.max(REPLAY_RECOVERY_POLICY.redisFloorTokens, Math.floor(runtimeTokens * REPLAY_RECOVERY_POLICY.redisColdFraction));
13
+ }
14
+ function recoveredRedisThreshold(effectiveBudget) {
15
+ return Math.max(REPLAY_RECOVERY_POLICY.redisFloorTokens, Math.floor(effectiveBudget * REPLAY_RECOVERY_POLICY.redisColdFraction));
16
+ }
17
+ export function isColdRedisReplay(inputs) {
18
+ return (inputs.runtimeTokens > inputs.effectiveBudget * REPLAY_RECOVERY_POLICY.enterPressure &&
19
+ inputs.redisTokens < coldRedisThreshold(inputs.runtimeTokens));
20
+ }
21
+ export function isReplayRecovered(inputs) {
22
+ return (inputs.runtimeTokens <= inputs.effectiveBudget * REPLAY_RECOVERY_POLICY.exitPressure &&
23
+ inputs.redisTokens >= recoveredRedisThreshold(inputs.effectiveBudget));
24
+ }
25
+ export function decideReplayRecovery(inputs) {
26
+ const currentState = inputs.currentState ?? null;
27
+ if (!currentState) {
28
+ if (!isColdRedisReplay(inputs)) {
29
+ return {
30
+ active: false,
31
+ shouldSkipCacheReplay: false,
32
+ nextState: null,
33
+ };
34
+ }
35
+ const emittedMarker = {
36
+ state: 'entering',
37
+ status: 'bounded',
38
+ reason: 'replay_cold_redis',
39
+ summary: 'cold restart, keep the window bounded',
40
+ };
41
+ return {
42
+ active: true,
43
+ shouldSkipCacheReplay: true,
44
+ trimTargetOverride: REPLAY_RECOVERY_POLICY.enterTrimTarget,
45
+ historyDepthCap: REPLAY_RECOVERY_POLICY.historyDepthCap,
46
+ emittedMarker,
47
+ emittedText: formatReplayMarker(emittedMarker),
48
+ nextState: 'stabilizing',
49
+ };
50
+ }
51
+ if (isReplayRecovered(inputs)) {
52
+ const emittedMarker = {
53
+ state: 'exited',
54
+ status: 'bounded',
55
+ reason: 'replay_exited',
56
+ summary: 'stable window restored',
57
+ };
58
+ return {
59
+ active: false,
60
+ shouldSkipCacheReplay: false,
61
+ emittedMarker,
62
+ emittedText: formatReplayMarker(emittedMarker),
63
+ nextState: null,
64
+ };
65
+ }
66
+ const emittedMarker = {
67
+ state: 'stabilizing',
68
+ status: 'bounded',
69
+ reason: 'replay_stabilizing',
70
+ summary: 'replay window stabilizing, keep it bounded',
71
+ };
72
+ return {
73
+ active: true,
74
+ shouldSkipCacheReplay: true,
75
+ trimTargetOverride: REPLAY_RECOVERY_POLICY.stabilizingTrimTarget,
76
+ historyDepthCap: REPLAY_RECOVERY_POLICY.historyDepthCap,
77
+ emittedMarker,
78
+ emittedText: formatReplayMarker(emittedMarker),
79
+ nextState: 'stabilizing',
80
+ };
81
+ }
82
+ //# sourceMappingURL=replay-recovery.js.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * hypermem Reranker
3
+ *
4
+ * Pluggable reranking interface with circuit-breaker protection and graceful
5
+ * degradation. Callers that receive null fall back to the original document
6
+ * order without disruption.
7
+ *
8
+ * Providers:
9
+ * - ZeroEntropyReranker — https://api.zeroentropy.dev/v1/rerank (zerank-2)
10
+ * - OpenRouterReranker — https://openrouter.ai/api/v1/rerank (cohere/rerank-4-pro)
11
+ * - OllamaReranker — http://localhost:11434/api/chat (yes/no classification)
12
+ *
13
+ * API key resolution order (per provider):
14
+ * zeroentropy: config.zeroEntropyApiKey → ZEROENTROPY_API_KEY env var
15
+ * openrouter: config.openrouterApiKey → OPENROUTER_API_KEY env var
16
+ */
17
+ export interface RerankResult {
18
+ index: number;
19
+ score: number;
20
+ content: string;
21
+ }
22
+ export interface RerankerProvider {
23
+ readonly name: string;
24
+ /**
25
+ * Reranks `documents` by relevance to `query`.
26
+ * Returns null on any failure — callers MUST fall back to original order.
27
+ */
28
+ rerank(query: string, documents: string[], topK?: number): Promise<RerankResult[] | null>;
29
+ }
30
+ export interface RerankerConfig {
31
+ provider: 'zeroentropy' | 'openrouter' | 'local' | 'none';
32
+ minCandidates: number;
33
+ maxDocuments: number;
34
+ topK: number;
35
+ timeoutMs: number;
36
+ zeroEntropyApiKey?: string;
37
+ zeroEntropyModel?: string;
38
+ openrouterApiKey?: string;
39
+ openrouterModel?: string;
40
+ ollamaUrl?: string;
41
+ ollamaModel?: string;
42
+ }
43
+ export declare class ZeroEntropyReranker implements RerankerProvider {
44
+ readonly name = "zeroentropy";
45
+ private readonly circuit;
46
+ private readonly apiKey;
47
+ private readonly model;
48
+ private readonly timeoutMs;
49
+ constructor(apiKey: string, model?: string, timeoutMs?: number);
50
+ rerank(query: string, documents: string[], topK?: number): Promise<RerankResult[] | null>;
51
+ }
52
+ export declare class OpenRouterReranker implements RerankerProvider {
53
+ readonly name = "openrouter";
54
+ private readonly circuit;
55
+ private readonly apiKey;
56
+ private readonly model;
57
+ private readonly timeoutMs;
58
+ constructor(apiKey: string, model?: string, timeoutMs?: number);
59
+ rerank(query: string, documents: string[], topK?: number): Promise<RerankResult[] | null>;
60
+ }
61
+ export declare class OllamaReranker implements RerankerProvider {
62
+ readonly name = "ollama";
63
+ private readonly circuit;
64
+ private readonly baseUrl;
65
+ private readonly model;
66
+ private readonly timeoutMs;
67
+ constructor(baseUrl?: string, model?: string, timeoutMs?: number);
68
+ /**
69
+ * Scores documents sequentially — one chat call per document.
70
+ * The Qwen3-Reranker-0.6B model responds with "yes" (relevant) or "no".
71
+ * Score: yes → 1.0, anything else → 0.0.
72
+ *
73
+ * Sequential iteration is required because Ollama's /api/chat is stateless
74
+ * per-request and running calls in parallel would overload a local GPU.
75
+ * Returns null on the first failure to preserve circuit breaker semantics.
76
+ */
77
+ rerank(query: string, documents: string[], topK?: number): Promise<RerankResult[] | null>;
78
+ }
79
+ /**
80
+ * Creates a RerankerProvider from the supplied config.
81
+ *
82
+ * API key resolution order:
83
+ * zeroentropy: config.zeroEntropyApiKey → ZEROENTROPY_API_KEY env var
84
+ * openrouter: config.openrouterApiKey → OPENROUTER_API_KEY env var
85
+ *
86
+ * Returns null when:
87
+ * - provider is 'none'
88
+ * - provider is 'zeroentropy' and no key found in config or env
89
+ * - provider is 'openrouter' and no key found in config or env
90
+ *
91
+ * 'local' (Ollama) never returns null from the factory — it has no required
92
+ * API key. The provider itself returns null on runtime failure.
93
+ */
94
+ export declare function createReranker(config: RerankerConfig): RerankerProvider | null;
95
+ //# sourceMappingURL=reranker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reranker.d.ts","sourceRoot":"","sources":["../src/reranker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgFH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,CAAC;CAC3F;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,aAAa,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,qBAAa,mBAAoB,YAAW,gBAAgB;IAC1D,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,MAAM,EAAE,MAAM,EAAE,KAAK,SAAa,EAAE,SAAS,SAAO;IAM1D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC;CAqC5F;AAID,qBAAa,kBAAmB,YAAW,gBAAgB;IACzD,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,MAAM,EAAE,MAAM,EAAE,KAAK,SAAwB,EAAE,SAAS,SAAO;IAMrE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC;CAqC5F;AAQD,qBAAa,cAAe,YAAW,gBAAgB;IACrD,QAAQ,CAAC,IAAI,YAAY;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAGjC,OAAO,SAA2B,EAClC,KAAK,SAAuC,EAC5C,SAAS,SAAS;IAOpB;;;;;;;;OAQG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC;CA4C5F;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,GAAG,IAAI,CAyB9E"}