@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
@@ -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, getArchivedContext } from './context-store.js';
8
+ import { getOrCreateActiveContext, updateContextHead, getArchivedContext, getActiveContext, getContextById } from './context-store.js';
9
9
  function nowIso() {
10
10
  return new Date().toISOString();
11
11
  }
@@ -233,6 +233,47 @@ export class MessageStore {
233
233
  // Reverse to get chronological order
234
234
  return rows.reverse().map(parseMessageRow);
235
235
  }
236
+ /**
237
+ * Get recent human-readable transcript messages for continuity guards.
238
+ *
239
+ * Tool-call/tool-result carrier rows are valid runtime messages, but they often
240
+ * have empty text_content. They must not consume the small "recent turns" depth
241
+ * used by transcript recovery and fork warming, or a dense tool loop can push
242
+ * the immediate conversational antecedent out of the selected window.
243
+ */
244
+ getRecentMeaningfulMessages(conversationId, limit = 12, minMessageId) {
245
+ const params = [conversationId];
246
+ let sql = `
247
+ SELECT
248
+ id,
249
+ conversation_id,
250
+ agent_id,
251
+ role,
252
+ text_content,
253
+ NULL AS tool_calls,
254
+ NULL AS tool_results,
255
+ metadata,
256
+ token_count,
257
+ message_index,
258
+ is_heartbeat,
259
+ created_at,
260
+ topic_id
261
+ FROM messages
262
+ WHERE conversation_id = ?
263
+ AND role IN ('user', 'assistant')
264
+ AND text_content IS NOT NULL
265
+ AND trim(text_content) != ''
266
+ AND is_heartbeat = 0
267
+ `;
268
+ if (minMessageId != null) {
269
+ sql += ' AND id >= ?';
270
+ params.push(minMessageId);
271
+ }
272
+ sql += ' ORDER BY message_index DESC LIMIT ?';
273
+ params.push(limit);
274
+ const rows = this.db.prepare(sql).all(...params);
275
+ return rows.reverse().map(parseMessageRow);
276
+ }
236
277
  /**
237
278
  * Get recent messages scoped to a topic (P3.4, Option B).
238
279
  * Returns messages matching the topic_id OR with topic_id IS NULL
@@ -307,6 +348,9 @@ export class MessageStore {
307
348
  JOIN conversations c ON m.conversation_id = c.id
308
349
  WHERE c.session_key = ?
309
350
  AND m.role IN ('user', 'assistant')
351
+ AND m.text_content IS NOT NULL
352
+ AND trim(m.text_content) != ''
353
+ AND m.is_heartbeat = 0
310
354
  ORDER BY m.message_index DESC
311
355
  LIMIT ?
312
356
  `).all(sessionKey, limit);
@@ -387,7 +431,7 @@ export class MessageStore {
387
431
  sql += ' AND is_heartbeat = 0';
388
432
  }
389
433
  if (opts?.requireText) {
390
- sql += " AND text_content IS NOT NULL AND text_content != ''";
434
+ sql += " AND role IN ('user', 'assistant') AND text_content IS NOT NULL AND trim(text_content) != ''";
391
435
  }
392
436
  sql += ' ORDER BY message_index DESC LIMIT ?';
393
437
  params.push(limit);
@@ -407,6 +451,10 @@ export class MessageStore {
407
451
  SELECT m.* FROM messages m
408
452
  JOIN fts_matches ON m.id = fts_matches.rowid
409
453
  WHERE m.context_id = ?
454
+ AND m.role IN ('user', 'assistant')
455
+ AND m.text_content IS NOT NULL
456
+ AND trim(m.text_content) != ''
457
+ AND m.is_heartbeat = 0
410
458
  ORDER BY fts_matches.rank
411
459
  `).all(query, limit * 3, contextId);
412
460
  return rows.slice(0, limit).map(parseMessageRow);
@@ -554,6 +602,311 @@ export class MessageStore {
554
602
  }
555
603
  return results;
556
604
  }
605
+ // ─── History Query Surface (0.9.4) ─────────────────────────────────────────
606
+ /**
607
+ * Per-mode hard caps and default limits for queryHistory.
608
+ * No mode may return more than its hard cap regardless of what the caller requests.
609
+ */
610
+ static HISTORY_QUERY_CAPS = {
611
+ runtime_chain: { defaultLimit: 80, hardCap: 200 },
612
+ transcript_tail: { defaultLimit: 40, hardCap: 120 },
613
+ tool_events: { defaultLimit: 40, hardCap: 120 },
614
+ by_topic: { defaultLimit: 60, hardCap: 160 },
615
+ by_context: { defaultLimit: 80, hardCap: 200 },
616
+ cross_session: { defaultLimit: 20, hardCap: 80 },
617
+ };
618
+ /**
619
+ * Apply per-mode hard caps to a caller-supplied limit.
620
+ * Returns [effectiveLimit, wasClamped].
621
+ *
622
+ * SQLite treats LIMIT -1 as unlimited, so never pass caller input through
623
+ * directly. Non-finite, zero, and negative limits fall back to the default.
624
+ */
625
+ capHistoryLimit(mode, requestedLimit) {
626
+ const caps = MessageStore.HISTORY_QUERY_CAPS[mode];
627
+ if (requestedLimit == null) {
628
+ return [caps.defaultLimit, false];
629
+ }
630
+ if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) {
631
+ return [caps.defaultLimit, true];
632
+ }
633
+ const requested = Math.floor(requestedLimit);
634
+ const effective = Math.min(requested, caps.hardCap);
635
+ return [effective, effective < requested];
636
+ }
637
+ /**
638
+ * Resolve conversation id from either a provided conversationId or a sessionKey lookup.
639
+ * Returns null when neither is available or the conversation cannot be found.
640
+ */
641
+ resolveConversationScope(query) {
642
+ const conv = query.conversationId != null
643
+ ? this.getConversationById(query.conversationId)
644
+ : query.sessionKey
645
+ ? this.getConversation(query.sessionKey)
646
+ : null;
647
+ if (!conv)
648
+ return null;
649
+ if (conv.agentId !== query.agentId) {
650
+ throw new Error(`queryHistory: conversation ${conv.id} is not owned by agent '${query.agentId}'`);
651
+ }
652
+ return conv.id;
653
+ }
654
+ /**
655
+ * Format a StoredMessage into the HistoryQueryMessage wire shape.
656
+ * contextId is passed explicitly when available from the query context.
657
+ */
658
+ formatHistoryMessage(msg, contextId) {
659
+ return {
660
+ id: msg.id,
661
+ role: msg.role,
662
+ textContent: msg.textContent,
663
+ toolCalls: msg.toolCalls,
664
+ toolResults: msg.toolResults,
665
+ messageIndex: msg.messageIndex,
666
+ createdAt: msg.createdAt,
667
+ topicId: msg.topicId ?? null,
668
+ contextId: contextId ?? null,
669
+ };
670
+ }
671
+ /**
672
+ * Redact tool payloads for tool_events mode when includeToolPayloads is false (default).
673
+ *
674
+ * Redaction replaces raw arguments/content with metadata-only stubs.
675
+ * This prevents accidental leakage of secrets or large payloads.
676
+ */
677
+ redactToolEvents(messages) {
678
+ return messages.map(msg => {
679
+ const redactedCalls = Array.isArray(msg.toolCalls)
680
+ ? msg.toolCalls.map(tc => ({
681
+ id: tc.id,
682
+ name: tc.name,
683
+ arguments: '[redacted]',
684
+ }))
685
+ : msg.toolCalls;
686
+ const redactedResults = Array.isArray(msg.toolResults)
687
+ ? msg.toolResults.map(tr => ({
688
+ callId: tr.callId,
689
+ name: tr.name,
690
+ content: '[redacted]',
691
+ isError: tr.isError,
692
+ }))
693
+ : msg.toolResults;
694
+ return { ...msg, toolCalls: redactedCalls, toolResults: redactedResults };
695
+ });
696
+ }
697
+ /**
698
+ * Unified read-only message history query surface (HyperMem 0.9.4).
699
+ *
700
+ * Routes to a mode-specific safe SQL path. No general SQL execution.
701
+ * All modes are capped, parameterized, and compaction-fence-aware where applicable.
702
+ *
703
+ * Modes:
704
+ * runtime_chain — full runtime rows (tool-bearing) via DAG chain or recency.
705
+ * transcript_tail — nonblank user/assistant text rows only (tool fields null).
706
+ * tool_events — rows with tool calls or results; payloads redacted by default.
707
+ * by_topic — runtime rows scoped to one topic.
708
+ * by_context — runtime rows scoped to one context; active default, archived/forked with includeArchived.
709
+ * cross_session — transcript rows across all conversations for one agent; per-conversation fence enforced.
710
+ */
711
+ queryHistory(query) {
712
+ const { agentId, mode } = query;
713
+ // Validate allowed modes first — prevent action-passthrough / unknown mode injection
714
+ const ALLOWED_MODES = new Set([
715
+ 'runtime_chain', 'transcript_tail', 'tool_events',
716
+ 'by_topic', 'by_context', 'cross_session',
717
+ ]);
718
+ if (!ALLOWED_MODES.has(mode)) {
719
+ throw new Error(`queryHistory: unknown mode '${mode}'. Allowed: ${[...ALLOWED_MODES].join(', ')}`);
720
+ }
721
+ const [limit, wasClamped] = this.capHistoryLimit(mode, query.limit);
722
+ // ─ runtime_chain ───────────────────────────────────────────────────────────────
723
+ if (mode === 'runtime_chain') {
724
+ const conversationId = this.resolveConversationScope(query);
725
+ if (conversationId == null) {
726
+ throw new Error('queryHistory(runtime_chain): requires sessionKey or conversationId');
727
+ }
728
+ // Prefer active context DAG chain
729
+ let messages = [];
730
+ const resolvedSessionKey = query.sessionKey ?? this.getConversationById(conversationId)?.sessionKey;
731
+ if (resolvedSessionKey) {
732
+ const ctx = getActiveContext(this.db, agentId, resolvedSessionKey);
733
+ if (ctx?.headMessageId != null) {
734
+ messages = this.getHistoryByDAGWalk(ctx.headMessageId, limit);
735
+ }
736
+ }
737
+ // Fallback to recency when DAG walk returns nothing
738
+ if (messages.length === 0) {
739
+ messages = this.getRecentMessages(conversationId, limit, query.minMessageId);
740
+ }
741
+ const truncated = wasClamped || messages.length === limit;
742
+ return {
743
+ mode,
744
+ scopedBy: { agentId, sessionKey: query.sessionKey, conversationId },
745
+ messages: messages.map(m => this.formatHistoryMessage(m)),
746
+ truncated,
747
+ redacted: false,
748
+ };
749
+ }
750
+ // ─ transcript_tail ────────────────────────────────────────────────────────────
751
+ if (mode === 'transcript_tail') {
752
+ const conversationId = this.resolveConversationScope(query);
753
+ if (conversationId == null) {
754
+ throw new Error('queryHistory(transcript_tail): requires sessionKey or conversationId');
755
+ }
756
+ // getRecentMeaningfulMessages already nulls tool_calls / tool_results in its SQL projection
757
+ const messages = this.getRecentMeaningfulMessages(conversationId, limit, query.minMessageId);
758
+ const truncated = wasClamped || messages.length === limit;
759
+ return {
760
+ mode,
761
+ scopedBy: { agentId, sessionKey: query.sessionKey, conversationId },
762
+ messages: messages.map(m => this.formatHistoryMessage(m)),
763
+ truncated,
764
+ redacted: false,
765
+ };
766
+ }
767
+ // ─ tool_events ───────────────────────────────────────────────────────────────
768
+ if (mode === 'tool_events') {
769
+ const conversationId = this.resolveConversationScope(query);
770
+ if (conversationId == null) {
771
+ throw new Error('queryHistory(tool_events): requires sessionKey or conversationId');
772
+ }
773
+ // Parameterized query — only tool-bearing rows
774
+ const rows = this.db.prepare(`
775
+ SELECT * FROM messages
776
+ WHERE conversation_id = ?
777
+ AND (tool_calls IS NOT NULL OR tool_results IS NOT NULL)
778
+ AND is_heartbeat = 0
779
+ ORDER BY message_index DESC
780
+ LIMIT ?
781
+ `).all(conversationId, limit);
782
+ let formatted = rows.reverse().map(row => {
783
+ const msg = parseMessageRow(row);
784
+ return this.formatHistoryMessage(msg);
785
+ });
786
+ const didRedact = !query.includeToolPayloads;
787
+ if (didRedact) {
788
+ formatted = this.redactToolEvents(formatted);
789
+ }
790
+ const truncated = wasClamped || rows.length === limit;
791
+ return {
792
+ mode,
793
+ scopedBy: { agentId, sessionKey: query.sessionKey, conversationId },
794
+ messages: formatted,
795
+ truncated,
796
+ redacted: didRedact,
797
+ };
798
+ }
799
+ // ─ by_topic ───────────────────────────────────────────────────────────────────
800
+ if (mode === 'by_topic') {
801
+ if (!query.topicId) {
802
+ throw new Error('queryHistory(by_topic): requires topicId');
803
+ }
804
+ const conversationId = this.resolveConversationScope(query);
805
+ if (conversationId == null) {
806
+ throw new Error('queryHistory(by_topic): requires sessionKey or conversationId');
807
+ }
808
+ const messages = this.getRecentMessagesByTopic(conversationId, query.topicId, limit, query.minMessageId);
809
+ const truncated = wasClamped || messages.length === limit;
810
+ return {
811
+ mode,
812
+ scopedBy: { agentId, sessionKey: query.sessionKey, conversationId, topicId: query.topicId },
813
+ messages: messages.map(m => this.formatHistoryMessage(m)),
814
+ truncated,
815
+ redacted: false,
816
+ };
817
+ }
818
+ // ─ by_context ──────────────────────────────────────────────────────────────────
819
+ if (mode === 'by_context') {
820
+ if (query.contextId == null) {
821
+ throw new Error('queryHistory(by_context): requires contextId');
822
+ }
823
+ const ctx = getContextById(this.db, query.contextId);
824
+ if (!ctx) {
825
+ throw new Error(`queryHistory(by_context): context ${query.contextId} not found`);
826
+ }
827
+ if (ctx.agentId !== agentId) {
828
+ throw new Error(`queryHistory(by_context): context ${query.contextId} is not owned by agent '${agentId}'`);
829
+ }
830
+ // Active context is always allowed.
831
+ // Archived or forked contexts require explicit opt-in via includeArchived.
832
+ if (ctx.status !== 'active' && !query.includeArchived) {
833
+ throw new Error(`queryHistory(by_context): context ${query.contextId} has status '${ctx.status}'. ` +
834
+ `Set includeArchived: true to query non-active contexts.`);
835
+ }
836
+ const messages = this.getMessagesByContextId(query.contextId, limit, { excludeHeartbeats: true });
837
+ const truncated = wasClamped || messages.length === limit;
838
+ return {
839
+ mode,
840
+ scopedBy: { agentId, contextId: query.contextId, sessionKey: ctx.sessionKey },
841
+ messages: messages.map(m => this.formatHistoryMessage(m, query.contextId)),
842
+ truncated,
843
+ redacted: false,
844
+ };
845
+ }
846
+ // ─ cross_session ──────────────────────────────────────────────────────────────
847
+ if (mode === 'cross_session') {
848
+ // Do NOT reuse getAgentMessages: it does not enforce per-conversation compaction fences.
849
+ // Each conversation's fence is respected via the LEFT JOIN on compaction_fences.
850
+ // Transcript-only rows: user/assistant with non-empty text. Tool fields projected as NULL.
851
+ const params = [agentId];
852
+ let sql = `
853
+ SELECT
854
+ m.id,
855
+ m.conversation_id,
856
+ m.agent_id,
857
+ m.role,
858
+ m.text_content,
859
+ NULL AS tool_calls,
860
+ NULL AS tool_results,
861
+ m.metadata,
862
+ m.token_count,
863
+ m.message_index,
864
+ m.is_heartbeat,
865
+ m.created_at,
866
+ m.topic_id
867
+ FROM messages m
868
+ LEFT JOIN compaction_fences cf ON cf.conversation_id = m.conversation_id
869
+ WHERE m.agent_id = ?
870
+ AND m.role IN ('user', 'assistant')
871
+ AND m.text_content IS NOT NULL
872
+ AND trim(m.text_content) != ''
873
+ AND m.is_heartbeat = 0
874
+ AND (cf.fence_message_id IS NULL OR m.id >= cf.fence_message_id)
875
+ `;
876
+ if (query.since) {
877
+ sql += ' AND m.created_at > ?';
878
+ params.push(query.since);
879
+ }
880
+ sql += ' ORDER BY m.created_at DESC LIMIT ?';
881
+ params.push(limit);
882
+ const rows = this.db.prepare(sql).all(...params);
883
+ // Reverse to chronological order
884
+ rows.reverse();
885
+ const formatted = rows.map(row => {
886
+ const msg = parseMessageRow(row);
887
+ return this.formatHistoryMessage(msg);
888
+ });
889
+ const truncated = wasClamped || rows.length === limit;
890
+ return {
891
+ mode,
892
+ scopedBy: { agentId },
893
+ messages: formatted,
894
+ truncated,
895
+ redacted: false,
896
+ };
897
+ }
898
+ // Should never reach here — all modes are covered above and the mode is validated at entry
899
+ throw new Error(`queryHistory: unhandled mode '${mode}'`);
900
+ }
901
+ /**
902
+ * Get a conversation by id (internal use by queryHistory).
903
+ */
904
+ getConversationById(conversationId) {
905
+ const row = this.db
906
+ .prepare('SELECT * FROM conversations WHERE id = ?')
907
+ .get(conversationId);
908
+ return row ? parseConversationRow(row) : null;
909
+ }
557
910
  // ─── Helpers ─────────────────────────────────────────────────
558
911
  /**
559
912
  * Infer channel type from session key format.
@@ -1 +1 @@
1
- {"version":3,"file":"open-domain.d.ts","sourceRoot":"","sources":["../src/open-domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUhD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBxD;AAID;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBpE;AAID,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,MAAM,EACvB,KAAK,GAAE,MAAW,GACjB,gBAAgB,EAAE,CA4CpB"}
1
+ {"version":3,"file":"open-domain.d.ts","sourceRoot":"","sources":["../src/open-domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUhD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBxD;AAID;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBpE;AAID,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,MAAM,EACvB,KAAK,GAAE,MAAW,GACjB,gBAAgB,EAAE,CA6CpB"}
@@ -94,8 +94,9 @@ export function searchOpenDomain(db, query, existingContent, limit = 10) {
94
94
  m.created_at AS createdAt
95
95
  FROM messages m
96
96
  JOIN fts_matches ON m.id = fts_matches.rowid
97
- WHERE m.text_content IS NOT NULL
98
- AND m.text_content != ''
97
+ WHERE m.role IN ('user', 'assistant')
98
+ AND m.text_content IS NOT NULL
99
+ AND trim(m.text_content) != ''
99
100
  AND m.is_heartbeat = 0
100
101
  ORDER BY fts_matches.rank
101
102
  `).all(ftsQuery, limit * 2);
@@ -27,11 +27,51 @@ export interface NoiseSweepResult {
27
27
  messagesDeleted: number;
28
28
  passType: 'noise_sweep';
29
29
  }
30
+ export interface ReferencedNoiseDebtResult {
31
+ passType: 'referenced_noise_debt';
32
+ conversationsScanned: number;
33
+ noiseCandidates: number;
34
+ referencedNoise: number;
35
+ parentReferencedNoise: number;
36
+ contextReferencedNoise: number;
37
+ snapshotReferencedNoise: number;
38
+ otherReferencedNoise: number;
39
+ sampleRefs: string[];
40
+ }
41
+ export interface TreeSafeNoiseCompactionResult {
42
+ passType: 'tree_safe_noise_compaction';
43
+ conversationsScanned: number;
44
+ candidates: number;
45
+ reparented: number;
46
+ repairedContextHeads: number;
47
+ repairedSnapshotHeads: number;
48
+ deleted: number;
49
+ skippedBlocked: number;
50
+ skippedRoot: number;
51
+ fkCheck: string;
52
+ }
53
+ export interface ProactivePassContext {
54
+ agentId?: string;
55
+ dbPath?: string;
56
+ }
30
57
  export interface ToolDecayResult {
31
58
  messagesUpdated: number;
32
59
  bytesFreed: number;
33
60
  passType: 'tool_decay';
34
61
  }
62
+ /**
63
+ * Measure noise rows that maintenance cannot delete because they are still FK
64
+ * targets. This is health debt, not corruption: the message tree is preserving
65
+ * referential integrity, but low-signal nodes need tree-safe compaction.
66
+ */
67
+ export declare function collectReferencedNoiseDebt(db: DatabaseSync, conversationId?: number, recentWindowSize?: number, maxCandidatesPerConversation?: number): ReferencedNoiseDebtResult;
68
+ /**
69
+ * Safely collapse referenced noise nodes by moving children and durable head
70
+ * pointers to the deleted node's parent. The repair only handles known safe
71
+ * message-head references: messages.parent_id, contexts.head_message_id, and
72
+ * composition_snapshots.head_message_id. Other FK blockers remain preserved.
73
+ */
74
+ export declare function runTreeSafeNoiseCompaction(db: DatabaseSync, conversationId?: number, recentWindowSize?: number, maxMutations?: number): TreeSafeNoiseCompactionResult;
35
75
  /**
36
76
  * Delete noise and heartbeat messages outside the recent window.
37
77
  *
@@ -42,7 +82,7 @@ export interface ToolDecayResult {
42
82
  * Deletions are wrapped in a single transaction. The FTS5 trigger handles
43
83
  * index cleanup automatically (msg_fts_ad fires on DELETE).
44
84
  */
45
- export declare function runNoiseSweep(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number): NoiseSweepResult;
85
+ export declare function runNoiseSweep(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number, context?: ProactivePassContext): NoiseSweepResult;
46
86
  /**
47
87
  * Truncate oversized tool_results outside the recent window.
48
88
  *
@@ -59,5 +99,5 @@ export declare function runNoiseSweep(db: DatabaseSync, conversationId: number,
59
99
  *
60
100
  * Mutations are committed in a single transaction.
61
101
  */
62
- export declare function runToolDecay(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number): ToolDecayResult;
102
+ export declare function runToolDecay(db: DatabaseSync, conversationId: number, recentWindowSize?: number, maxCandidates?: number, context?: ProactivePassContext): ToolDecayResult;
63
103
  //# 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;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"}
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,yBAAyB;IACxC,QAAQ,EAAE,uBAAuB,CAAC;IAClC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,uBAAuB,EAAE,MAAM,CAAC;IAChC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAyND;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,cAAc,CAAC,EAAE,MAAM,EACvB,gBAAgB,GAAE,MAAW,EAC7B,4BAA4B,GAAE,MAAiB,GAC9C,yBAAyB,CA+B3B;AAwDD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,cAAc,CAAC,EAAE,MAAM,EACvB,gBAAgB,GAAE,MAAW,EAC7B,YAAY,GAAE,MAAY,GACzB,6BAA6B,CA4D/B;AAID;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,EAC7B,aAAa,GAAE,MAAiB,EAChC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,gBAAgB,CA0GlB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,EAC7B,aAAa,GAAE,MAAiB,EAChC,OAAO,CAAC,EAAE,oBAAoB,GAC7B,eAAe,CAgHjB"}