@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.
- package/CHANGELOG.md +16 -0
- package/INSTALL.md +73 -70
- package/README.md +33 -51
- package/assets/default-config.json +47 -0
- package/bin/hypermem-doctor.mjs +76 -2
- package/bin/hypermem-status.mjs +255 -7
- package/dist/adaptive-lifecycle.d.ts +39 -0
- package/dist/adaptive-lifecycle.d.ts.map +1 -1
- package/dist/adaptive-lifecycle.js +87 -9
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +7 -5
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +239 -20
- package/dist/hybrid-retrieval.d.ts +8 -0
- package/dist/hybrid-retrieval.d.ts.map +1 -1
- package/dist/hybrid-retrieval.js +112 -10
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/message-store.d.ts +62 -1
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +355 -2
- package/dist/open-domain.d.ts.map +1 -1
- package/dist/open-domain.js +3 -2
- package/dist/proactive-pass.d.ts +42 -2
- package/dist/proactive-pass.d.ts.map +1 -1
- package/dist/proactive-pass.js +294 -39
- package/dist/topic-synthesizer.d.ts.map +1 -1
- package/dist/topic-synthesizer.js +9 -3
- package/dist/types.d.ts +99 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-store.d.ts +10 -1
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +45 -9
- package/docs/DIAGNOSTICS.md +87 -0
- package/docs/INTEGRATION_VALIDATION.md +40 -1
- package/docs/ROADMAP.md +25 -12
- package/docs/TUNING.md +45 -4
- package/install.sh +5 -60
- package/memory-plugin/dist/index.d.ts +24 -0
- package/memory-plugin/dist/index.js +570 -0
- package/memory-plugin/openclaw.plugin.json +199 -2
- package/memory-plugin/package.json +3 -3
- package/package.json +24 -10
- package/plugin/dist/index.d.ts +210 -0
- package/plugin/dist/index.d.ts.map +1 -0
- package/plugin/dist/index.js +3641 -0
- package/plugin/dist/index.js.map +1 -0
- package/plugin/openclaw.plugin.json +199 -2
- package/plugin/package.json +4 -4
- package/scripts/install-packed-runtime.mjs +99 -0
- package/scripts/install-runtime.mjs +164 -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, 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,
|
|
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"}
|
package/dist/open-domain.js
CHANGED
|
@@ -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.
|
|
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);
|
package/dist/proactive-pass.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|