@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.
- package/ARCHITECTURE.md +31 -39
- package/README.md +20 -14
- package/bin/hypermem-status.mjs +1 -1
- package/dist/background-indexer.d.ts +14 -3
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +135 -27
- package/dist/budget-policy.d.ts +22 -0
- package/dist/budget-policy.d.ts.map +1 -0
- package/dist/budget-policy.js +27 -0
- package/dist/cache.d.ts +11 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/compositor-utils.d.ts +31 -0
- package/dist/compositor-utils.d.ts.map +1 -0
- package/dist/compositor-utils.js +47 -0
- package/dist/compositor.d.ts +163 -1
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +862 -130
- package/dist/content-hash.d.ts +43 -0
- package/dist/content-hash.d.ts.map +1 -0
- package/dist/content-hash.js +75 -0
- package/dist/context-store.d.ts +54 -0
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +102 -0
- package/dist/contradiction-audit-store.d.ts +54 -0
- package/dist/contradiction-audit-store.d.ts.map +1 -0
- package/dist/contradiction-audit-store.js +88 -0
- package/dist/contradiction-detector.d.ts +78 -0
- package/dist/contradiction-detector.d.ts.map +1 -0
- package/dist/contradiction-detector.js +362 -0
- package/dist/contradiction-resolution-policy.d.ts +21 -0
- package/dist/contradiction-resolution-policy.d.ts.map +1 -0
- package/dist/contradiction-resolution-policy.js +17 -0
- package/dist/cross-agent.d.ts +1 -1
- package/dist/cross-agent.js +17 -17
- package/dist/degradation.d.ts +102 -0
- package/dist/degradation.d.ts.map +1 -0
- package/dist/degradation.js +141 -0
- package/dist/dreaming-promoter.d.ts +39 -1
- package/dist/dreaming-promoter.d.ts.map +1 -1
- package/dist/dreaming-promoter.js +70 -4
- package/dist/expertise-store.d.ts +129 -0
- package/dist/expertise-store.d.ts.map +1 -0
- package/dist/expertise-store.js +342 -0
- package/dist/fact-store.d.ts +15 -0
- package/dist/fact-store.d.ts.map +1 -1
- package/dist/fact-store.js +52 -5
- package/dist/index.d.ts +74 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +407 -29
- package/dist/knowledge-lint.d.ts +2 -0
- package/dist/knowledge-lint.d.ts.map +1 -1
- package/dist/knowledge-lint.js +40 -1
- package/dist/library-schema.d.ts +7 -2
- package/dist/library-schema.d.ts.map +1 -1
- package/dist/library-schema.js +307 -2
- package/dist/message-store.d.ts +64 -1
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +137 -1
- package/dist/proactive-pass.d.ts +2 -2
- package/dist/proactive-pass.d.ts.map +1 -1
- package/dist/proactive-pass.js +66 -12
- package/dist/replay-recovery.d.ts +29 -0
- package/dist/replay-recovery.d.ts.map +1 -0
- package/dist/replay-recovery.js +82 -0
- package/dist/reranker.d.ts +95 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/reranker.js +308 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +46 -1
- package/dist/seed.d.ts +1 -1
- package/dist/seed.js +1 -1
- package/dist/session-flusher.d.ts +4 -4
- package/dist/session-flusher.d.ts.map +1 -1
- package/dist/session-flusher.js +3 -3
- package/dist/spawn-context.d.ts +1 -1
- package/dist/spawn-context.js +1 -1
- package/dist/temporal-store.d.ts +1 -0
- package/dist/temporal-store.d.ts.map +1 -1
- package/dist/tool-artifact-store.d.ts +98 -0
- package/dist/tool-artifact-store.d.ts.map +1 -0
- package/dist/tool-artifact-store.js +244 -0
- package/dist/topic-detector.js +2 -2
- package/dist/topic-store.d.ts +6 -0
- package/dist/topic-store.d.ts.map +1 -1
- package/dist/topic-store.js +39 -0
- package/dist/topic-synthesizer.js +1 -1
- package/dist/trigger-registry.d.ts +1 -1
- package/dist/trigger-registry.js +4 -4
- package/dist/types.d.ts +239 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-store.d.ts +2 -1
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +3 -0
- package/dist/version.d.ts +10 -10
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -10
- package/package.json +6 -4
package/dist/message-store.js
CHANGED
|
@@ -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.
|
package/dist/proactive-pass.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/proactive-pass.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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"}
|