@psiclawops/hypermem 0.5.6 → 0.7.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/README.md +11 -54
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +26 -18
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +16 -2
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +146 -19
- package/dist/context-backfill.d.ts +46 -0
- package/dist/context-backfill.d.ts.map +1 -0
- package/dist/context-backfill.js +113 -0
- package/dist/context-store.d.ts +77 -0
- package/dist/context-store.d.ts.map +1 -0
- package/dist/context-store.js +177 -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/cross-agent.d.ts +12 -0
- package/dist/cross-agent.d.ts.map +1 -1
- package/dist/cross-agent.js +31 -19
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +8 -0
- package/dist/dreaming-promoter.d.ts +1 -1
- package/dist/dreaming-promoter.js +1 -1
- 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 +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -6
- package/dist/library-schema.d.ts +1 -1
- package/dist/library-schema.d.ts.map +1 -1
- package/dist/library-schema.js +72 -2
- package/dist/message-store.d.ts +31 -2
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +131 -17
- package/dist/preference-store.d.ts +1 -1
- package/dist/preference-store.js +1 -1
- package/dist/profiles.d.ts +3 -1
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +8 -0
- package/dist/repair-tool-pairs.d.ts.map +1 -1
- package/dist/repair-tool-pairs.js +73 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +27 -1
- package/dist/seed.d.ts +1 -1
- package/dist/seed.js +1 -1
- package/dist/session-flusher.d.ts +2 -2
- package/dist/session-flusher.js +2 -2
- 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/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 +15 -3
- 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 +353 -0
- package/dist/version.d.ts +5 -5
- package/dist/version.js +5 -5
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -20,7 +20,9 @@ export { PreferenceStore } from './preference-store.js';
|
|
|
20
20
|
export { FleetStore } from './fleet-store.js';
|
|
21
21
|
export { SystemStore } from './system-store.js';
|
|
22
22
|
export { WorkStore } from './work-store.js';
|
|
23
|
+
export { ensureContextSchema, getActiveContext, getOrCreateActiveContext, updateContextHead, archiveContext, rotateSessionContext } from './context-store.js';
|
|
23
24
|
export { DesiredStateStore } from './desired-state-store.js';
|
|
25
|
+
export { ExpertiseStore } from './expertise-store.js';
|
|
24
26
|
export { evictStaleContent, DEFAULT_EVICTION_CONFIG } from './image-eviction.js';
|
|
25
27
|
export { KnowledgeGraph } from './knowledge-graph.js';
|
|
26
28
|
export { RateLimiter, createRateLimitedEmbedder } from './rate-limiter.js';
|
|
@@ -34,6 +36,7 @@ export { migrate, SCHEMA_VERSION } from './schema.js';
|
|
|
34
36
|
export { migrateLibrary, LIBRARY_SCHEMA_VERSION } from './library-schema.js';
|
|
35
37
|
export { VectorStore, generateEmbeddings } from './vector-store.js';
|
|
36
38
|
export { hybridSearch, buildFtsQuery } from './hybrid-retrieval.js';
|
|
39
|
+
export { ContradictionDetector } from './contradiction-detector.js';
|
|
37
40
|
export { DocChunkStore } from './doc-chunk-store.js';
|
|
38
41
|
export { WorkspaceSeeder, seedWorkspace } from './seed.js';
|
|
39
42
|
export { chunkMarkdown, chunkFile, inferCollection, hashContent, ACA_COLLECTIONS } from './doc-chunker.js';
|
|
@@ -105,11 +108,13 @@ const DEFAULT_CONFIG = {
|
|
|
105
108
|
maxMessagesPerTick: 500,
|
|
106
109
|
},
|
|
107
110
|
embedding: {
|
|
111
|
+
provider: 'openai',
|
|
108
112
|
ollamaUrl: 'http://localhost:11434',
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
openaiBaseUrl: 'https://openrouter.ai/api/v1',
|
|
114
|
+
model: 'qwen/qwen3-embedding-8b',
|
|
115
|
+
dimensions: 4096,
|
|
116
|
+
timeout: 15000,
|
|
117
|
+
batchSize: 100,
|
|
113
118
|
},
|
|
114
119
|
};
|
|
115
120
|
/**
|
|
@@ -117,8 +122,8 @@ const DEFAULT_CONFIG = {
|
|
|
117
122
|
*
|
|
118
123
|
* Usage:
|
|
119
124
|
* const hm = await hypermem.create({ dataDir: '~/.openclaw/hypermem' });
|
|
120
|
-
* await hm.record('
|
|
121
|
-
* const result = await hm.compose({ agentId: '
|
|
125
|
+
* await hm.record('agent1', 'agent:agent1:webchat:main', userMsg);
|
|
126
|
+
* const result = await hm.compose({ agentId: 'agent1', sessionKey: '...', ... });
|
|
122
127
|
*/
|
|
123
128
|
export class HyperMem {
|
|
124
129
|
dbManager;
|
|
@@ -205,10 +210,18 @@ export class HyperMem {
|
|
|
205
210
|
provider: opts?.provider,
|
|
206
211
|
model: opts?.model,
|
|
207
212
|
});
|
|
213
|
+
let contextId;
|
|
214
|
+
try {
|
|
215
|
+
const { getOrCreateActiveContext } = await import('./context-store.js');
|
|
216
|
+
const ctx = getOrCreateActiveContext(db, agentId, sessionKey, conversation.id);
|
|
217
|
+
contextId = ctx.id;
|
|
218
|
+
}
|
|
219
|
+
catch (_) { /* context wiring is best-effort in Phase 1 */ }
|
|
208
220
|
const neutral = userMessageToNeutral(stripMessageMetadata(content));
|
|
209
221
|
const stored = store.recordMessage(conversation.id, agentId, neutral, {
|
|
210
222
|
tokenCount: opts?.tokenCount,
|
|
211
223
|
isHeartbeat: opts?.isHeartbeat,
|
|
224
|
+
contextId,
|
|
212
225
|
});
|
|
213
226
|
await this.cache.pushHistory(agentId, sessionKey, [stored], this.config.compositor.maxHistoryMessages);
|
|
214
227
|
await this.cache.touchSession(agentId, sessionKey);
|
|
@@ -224,8 +237,16 @@ export class HyperMem {
|
|
|
224
237
|
if (!conversation) {
|
|
225
238
|
throw new Error(`No conversation found for session ${sessionKey}`);
|
|
226
239
|
}
|
|
240
|
+
let contextId;
|
|
241
|
+
try {
|
|
242
|
+
const { getOrCreateActiveContext } = await import('./context-store.js');
|
|
243
|
+
const ctx = getOrCreateActiveContext(db, agentId, sessionKey, conversation.id);
|
|
244
|
+
contextId = ctx.id;
|
|
245
|
+
}
|
|
246
|
+
catch (_) { /* context wiring is best-effort in Phase 1 */ }
|
|
227
247
|
const stored = store.recordMessage(conversation.id, agentId, message, {
|
|
228
248
|
tokenCount: opts?.tokenCount,
|
|
249
|
+
contextId,
|
|
229
250
|
});
|
|
230
251
|
await this.cache.pushHistory(agentId, sessionKey, [stored], this.config.compositor.maxHistoryMessages);
|
|
231
252
|
await this.cache.touchSession(agentId, sessionKey);
|
package/dist/library-schema.d.ts
CHANGED
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
* 10. Topics (cross-session thread tracking)
|
|
18
18
|
*/
|
|
19
19
|
import type { DatabaseSync } from 'node:sqlite';
|
|
20
|
-
export declare const LIBRARY_SCHEMA_VERSION =
|
|
20
|
+
export declare const LIBRARY_SCHEMA_VERSION = 15;
|
|
21
21
|
export declare function migrateLibrary(db: DatabaseSync, engineVersion?: string): void;
|
|
22
22
|
//# sourceMappingURL=library-schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"library-schema.d.ts","sourceRoot":"","sources":["../src/library-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,sBAAsB,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"library-schema.d.ts","sourceRoot":"","sources":["../src/library-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAy5BzC,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CA6P7E"}
|
package/dist/library-schema.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 9. Work items (fleet kanban)
|
|
17
17
|
* 10. Topics (cross-session thread tracking)
|
|
18
18
|
*/
|
|
19
|
-
export const LIBRARY_SCHEMA_VERSION =
|
|
19
|
+
export const LIBRARY_SCHEMA_VERSION = 15;
|
|
20
20
|
function nowIso() {
|
|
21
21
|
return new Date().toISOString();
|
|
22
22
|
}
|
|
@@ -150,12 +150,15 @@ function applyV3Collections(db) {
|
|
|
150
150
|
updated_at TEXT NOT NULL,
|
|
151
151
|
expires_at TEXT,
|
|
152
152
|
superseded_by INTEGER,
|
|
153
|
-
decay_score REAL DEFAULT 0.0
|
|
153
|
+
decay_score REAL DEFAULT 0.0,
|
|
154
|
+
valid_from TEXT,
|
|
155
|
+
invalid_at TEXT
|
|
154
156
|
)
|
|
155
157
|
`);
|
|
156
158
|
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_agent ON facts(agent_id, scope, domain)');
|
|
157
159
|
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_visibility ON facts(visibility, agent_id)');
|
|
158
160
|
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_active ON facts(agent_id, superseded_by, decay_score, confidence DESC)');
|
|
161
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_temporal_validity ON facts(agent_id, valid_from, invalid_at)');
|
|
159
162
|
db.exec(`
|
|
160
163
|
CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts USING fts5(
|
|
161
164
|
content,
|
|
@@ -1016,6 +1019,73 @@ export function migrateLibrary(db, engineVersion) {
|
|
|
1016
1019
|
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1017
1020
|
.run(13, nowIso());
|
|
1018
1021
|
}
|
|
1022
|
+
// ── V14: Temporal validity columns on facts ──────────────────────────
|
|
1023
|
+
// valid_from / invalid_at enable "what was true on date X?" queries.
|
|
1024
|
+
if (currentVersion < 14) {
|
|
1025
|
+
try {
|
|
1026
|
+
db.exec('ALTER TABLE facts ADD COLUMN valid_from TEXT');
|
|
1027
|
+
}
|
|
1028
|
+
catch { /* already exists */ }
|
|
1029
|
+
try {
|
|
1030
|
+
db.exec('ALTER TABLE facts ADD COLUMN invalid_at TEXT');
|
|
1031
|
+
}
|
|
1032
|
+
catch { /* already exists */ }
|
|
1033
|
+
try {
|
|
1034
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_facts_temporal_validity ON facts(agent_id, valid_from, invalid_at)');
|
|
1035
|
+
}
|
|
1036
|
+
catch { /* already exists */ }
|
|
1037
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1038
|
+
.run(14, nowIso());
|
|
1039
|
+
}
|
|
1040
|
+
// ── V15: Expertise tables (domain expertise patterns) ──────────────
|
|
1041
|
+
// expertise_observations: raw learnings from conversations, pipelines, reviews
|
|
1042
|
+
// expertise_patterns: graduated observations with confirming evidence
|
|
1043
|
+
// expertise_evidence: links observations to patterns (confirms/contradicts)
|
|
1044
|
+
if (currentVersion < 15) {
|
|
1045
|
+
db.exec(`
|
|
1046
|
+
CREATE TABLE IF NOT EXISTS expertise_observations (
|
|
1047
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1048
|
+
agent_id TEXT NOT NULL,
|
|
1049
|
+
domain TEXT NOT NULL,
|
|
1050
|
+
context TEXT,
|
|
1051
|
+
observation_text TEXT NOT NULL,
|
|
1052
|
+
source_type TEXT NOT NULL DEFAULT 'conversation',
|
|
1053
|
+
source_ref TEXT,
|
|
1054
|
+
created_at TEXT NOT NULL
|
|
1055
|
+
)
|
|
1056
|
+
`);
|
|
1057
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_obs_agent ON expertise_observations(agent_id, domain)');
|
|
1058
|
+
db.exec(`
|
|
1059
|
+
CREATE TABLE IF NOT EXISTS expertise_patterns (
|
|
1060
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1061
|
+
agent_id TEXT NOT NULL,
|
|
1062
|
+
domain TEXT NOT NULL,
|
|
1063
|
+
pattern_text TEXT NOT NULL,
|
|
1064
|
+
confidence REAL DEFAULT 0.7,
|
|
1065
|
+
frequency INTEGER DEFAULT 1,
|
|
1066
|
+
first_seen TEXT NOT NULL,
|
|
1067
|
+
last_confirmed TEXT NOT NULL,
|
|
1068
|
+
invalidated_at TEXT,
|
|
1069
|
+
invalidation_reason TEXT,
|
|
1070
|
+
decay_score REAL DEFAULT 0.0,
|
|
1071
|
+
updated_at TEXT
|
|
1072
|
+
)
|
|
1073
|
+
`);
|
|
1074
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_patterns_agent ON expertise_patterns(agent_id, domain, invalidated_at)');
|
|
1075
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_patterns_active ON expertise_patterns(agent_id, invalidated_at, confidence DESC)');
|
|
1076
|
+
db.exec(`
|
|
1077
|
+
CREATE TABLE IF NOT EXISTS expertise_evidence (
|
|
1078
|
+
observation_id INTEGER NOT NULL,
|
|
1079
|
+
pattern_id INTEGER NOT NULL,
|
|
1080
|
+
relationship TEXT NOT NULL CHECK(relationship IN ('confirms', 'contradicts')),
|
|
1081
|
+
created_at TEXT NOT NULL,
|
|
1082
|
+
PRIMARY KEY (observation_id, pattern_id)
|
|
1083
|
+
)
|
|
1084
|
+
`);
|
|
1085
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_expertise_evidence_pattern ON expertise_evidence(pattern_id, relationship)');
|
|
1086
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
1087
|
+
.run(15, nowIso());
|
|
1088
|
+
}
|
|
1019
1089
|
// Always ensure meta exists before stamping the running engine version.
|
|
1020
1090
|
// Some legacy/stale DBs reached schema >=10 without the V10 migration having
|
|
1021
1091
|
// actually created the table, which would make startup fail with
|
package/dist/message-store.d.ts
CHANGED
|
@@ -43,22 +43,27 @@ export declare class MessageStore {
|
|
|
43
43
|
/**
|
|
44
44
|
* Record a message to the database.
|
|
45
45
|
* Returns the stored message with its assigned ID.
|
|
46
|
+
*
|
|
47
|
+
* Phase 2 (Turn DAG): automatically sets parent_id and depth.
|
|
48
|
+
* - parent_id = context.head_message_id (the previous message on this branch)
|
|
49
|
+
* - depth = parent.depth + 1 (or 0 if first message)
|
|
46
50
|
*/
|
|
47
51
|
recordMessage(conversationId: number, agentId: string, message: NeutralMessage, opts?: {
|
|
48
52
|
tokenCount?: number;
|
|
49
53
|
isHeartbeat?: boolean;
|
|
54
|
+
contextId?: number;
|
|
50
55
|
}): StoredMessage;
|
|
51
56
|
/**
|
|
52
57
|
* Get recent messages for a conversation.
|
|
53
58
|
*/
|
|
54
|
-
getRecentMessages(conversationId: number, limit?: number): StoredMessage[];
|
|
59
|
+
getRecentMessages(conversationId: number, limit?: number, minMessageId?: number): StoredMessage[];
|
|
55
60
|
/**
|
|
56
61
|
* Get recent messages scoped to a topic (P3.4, Option B).
|
|
57
62
|
* Returns messages matching the topic_id OR with topic_id IS NULL
|
|
58
63
|
* (legacy messages created before topic tracking was introduced).
|
|
59
64
|
* This is transition-safe: no legacy messages are silently dropped.
|
|
60
65
|
*/
|
|
61
|
-
getRecentMessagesByTopic(conversationId: number, topicId: string, limit?: number): StoredMessage[];
|
|
66
|
+
getRecentMessagesByTopic(conversationId: number, topicId: string, limit?: number, minMessageId?: number): StoredMessage[];
|
|
62
67
|
/**
|
|
63
68
|
* Get messages across all conversations for an agent (cross-session query).
|
|
64
69
|
*/
|
|
@@ -77,6 +82,30 @@ export declare class MessageStore {
|
|
|
77
82
|
* Returns up to `n` turns (capped at 50).
|
|
78
83
|
*/
|
|
79
84
|
getRecentTurns(sessionKey: string, n: number): RecentTurn[];
|
|
85
|
+
/**
|
|
86
|
+
* Get messages by walking the parent_id chain from a head message backward.
|
|
87
|
+
* This is the DAG-native read path introduced in Phase 3.
|
|
88
|
+
*
|
|
89
|
+
* Walks from headMessageId backward through parent_id links, collecting
|
|
90
|
+
* up to `limit` messages in chronological order.
|
|
91
|
+
*
|
|
92
|
+
* Falls back to getRecentMessages if the head message has no parent chain
|
|
93
|
+
* (e.g., legacy data before backfill).
|
|
94
|
+
*/
|
|
95
|
+
getHistoryByDAGWalk(headMessageId: number, limit?: number): StoredMessage[];
|
|
96
|
+
/**
|
|
97
|
+
* Get messages scoped to a specific context_id.
|
|
98
|
+
* Used by keystone/FTS/topic recall to constrain results to the active branch.
|
|
99
|
+
*/
|
|
100
|
+
getMessagesByContextId(contextId: number, limit?: number, opts?: {
|
|
101
|
+
excludeHeartbeats?: boolean;
|
|
102
|
+
requireText?: boolean;
|
|
103
|
+
}): StoredMessage[];
|
|
104
|
+
/**
|
|
105
|
+
* Full-text search constrained to a specific context_id.
|
|
106
|
+
* Phase 3: replaces unscoped searchMessages for composition paths.
|
|
107
|
+
*/
|
|
108
|
+
searchMessagesByContextId(contextId: number, query: string, limit?: number): StoredMessage[];
|
|
80
109
|
/**
|
|
81
110
|
* Get message count for a conversation.
|
|
82
111
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-store.d.ts","sourceRoot":"","sources":["../src/message-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,UAAU,EACX,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"message-store.d.ts","sourceRoot":"","sources":["../src/message-store.ts"],"names":[],"mappings":"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"}
|
package/dist/message-store.js
CHANGED
|
@@ -5,6 +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
9
|
function nowIso() {
|
|
9
10
|
return new Date().toISOString();
|
|
10
11
|
}
|
|
@@ -70,6 +71,8 @@ export class MessageStore {
|
|
|
70
71
|
`).run(sessionKey, agentId, channelType, opts?.channelId || null, opts?.provider || null, opts?.model || null, now, now);
|
|
71
72
|
// node:sqlite returns { changes, lastInsertRowid }
|
|
72
73
|
const id = result.lastInsertRowid;
|
|
74
|
+
// Ensure an active context exists for the new conversation (fire-and-forget side effect)
|
|
75
|
+
getOrCreateActiveContext(this.db, agentId, sessionKey, id);
|
|
73
76
|
return {
|
|
74
77
|
id,
|
|
75
78
|
sessionKey,
|
|
@@ -148,6 +151,10 @@ export class MessageStore {
|
|
|
148
151
|
/**
|
|
149
152
|
* Record a message to the database.
|
|
150
153
|
* Returns the stored message with its assigned ID.
|
|
154
|
+
*
|
|
155
|
+
* Phase 2 (Turn DAG): automatically sets parent_id and depth.
|
|
156
|
+
* - parent_id = context.head_message_id (the previous message on this branch)
|
|
157
|
+
* - depth = parent.depth + 1 (or 0 if first message)
|
|
151
158
|
*/
|
|
152
159
|
recordMessage(conversationId, agentId, message, opts) {
|
|
153
160
|
const now = nowIso();
|
|
@@ -156,11 +163,30 @@ export class MessageStore {
|
|
|
156
163
|
.prepare('SELECT MAX(message_index) AS max_idx FROM messages WHERE conversation_id = ?')
|
|
157
164
|
.get(conversationId);
|
|
158
165
|
const messageIndex = (lastRow?.max_idx ?? -1) + 1;
|
|
166
|
+
// Phase 2 (Turn DAG): resolve parent_id and depth from context head
|
|
167
|
+
let parentId = null;
|
|
168
|
+
let depth = 0;
|
|
169
|
+
if (opts?.contextId) {
|
|
170
|
+
const headRow = this.db
|
|
171
|
+
.prepare('SELECT head_message_id FROM contexts WHERE id = ?')
|
|
172
|
+
.get(opts.contextId);
|
|
173
|
+
if (headRow?.head_message_id != null) {
|
|
174
|
+
parentId = headRow.head_message_id;
|
|
175
|
+
const parentDepthRow = this.db
|
|
176
|
+
.prepare('SELECT depth FROM messages WHERE id = ?')
|
|
177
|
+
.get(parentId);
|
|
178
|
+
depth = (parentDepthRow?.depth ?? -1) + 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
159
181
|
const result = this.db.prepare(`
|
|
160
|
-
INSERT INTO messages (conversation_id, agent_id, role, text_content, tool_calls, tool_results, metadata, token_count, message_index, is_heartbeat, created_at)
|
|
161
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
162
|
-
`).run(conversationId, agentId, message.role, message.textContent, message.toolCalls ? JSON.stringify(message.toolCalls) : null, message.toolResults ? JSON.stringify(message.toolResults) : null, message.metadata ? JSON.stringify(message.metadata) : null, opts?.tokenCount || null, messageIndex, opts?.isHeartbeat ? 1 : 0, now);
|
|
182
|
+
INSERT INTO messages (conversation_id, agent_id, role, text_content, tool_calls, tool_results, metadata, token_count, message_index, is_heartbeat, created_at, context_id, parent_id, depth)
|
|
183
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
184
|
+
`).run(conversationId, agentId, message.role, message.textContent, message.toolCalls ? JSON.stringify(message.toolCalls) : null, message.toolResults ? JSON.stringify(message.toolResults) : null, message.metadata ? JSON.stringify(message.metadata) : null, opts?.tokenCount || null, messageIndex, opts?.isHeartbeat ? 1 : 0, now, opts?.contextId ?? null, parentId, depth);
|
|
163
185
|
const id = result.lastInsertRowid;
|
|
186
|
+
// Update context head pointer if contextId was provided
|
|
187
|
+
if (opts?.contextId) {
|
|
188
|
+
updateContextHead(this.db, opts.contextId, Number(id));
|
|
189
|
+
}
|
|
164
190
|
// Update conversation counters
|
|
165
191
|
const tokenDelta = opts?.tokenCount || 0;
|
|
166
192
|
const isOutput = message.role === 'assistant';
|
|
@@ -190,13 +216,16 @@ export class MessageStore {
|
|
|
190
216
|
/**
|
|
191
217
|
* Get recent messages for a conversation.
|
|
192
218
|
*/
|
|
193
|
-
getRecentMessages(conversationId, limit = 50) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
219
|
+
getRecentMessages(conversationId, limit = 50, minMessageId) {
|
|
220
|
+
const params = [conversationId];
|
|
221
|
+
let sql = 'SELECT * FROM messages WHERE conversation_id = ?';
|
|
222
|
+
if (minMessageId != null) {
|
|
223
|
+
sql += ' AND id >= ?';
|
|
224
|
+
params.push(minMessageId);
|
|
225
|
+
}
|
|
226
|
+
sql += ' ORDER BY message_index DESC LIMIT ?';
|
|
227
|
+
params.push(limit);
|
|
228
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
200
229
|
// Reverse to get chronological order
|
|
201
230
|
return rows.reverse().map(parseMessageRow);
|
|
202
231
|
}
|
|
@@ -206,13 +235,16 @@ export class MessageStore {
|
|
|
206
235
|
* (legacy messages created before topic tracking was introduced).
|
|
207
236
|
* This is transition-safe: no legacy messages are silently dropped.
|
|
208
237
|
*/
|
|
209
|
-
getRecentMessagesByTopic(conversationId, topicId, limit = 50) {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
238
|
+
getRecentMessagesByTopic(conversationId, topicId, limit = 50, minMessageId) {
|
|
239
|
+
const params = [conversationId, topicId];
|
|
240
|
+
let sql = 'SELECT * FROM messages WHERE conversation_id = ? AND (topic_id = ? OR topic_id IS NULL)';
|
|
241
|
+
if (minMessageId != null) {
|
|
242
|
+
sql += ' AND id >= ?';
|
|
243
|
+
params.push(minMessageId);
|
|
244
|
+
}
|
|
245
|
+
sql += ' ORDER BY message_index DESC LIMIT ?';
|
|
246
|
+
params.push(limit);
|
|
247
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
216
248
|
// Reverse to get chronological order
|
|
217
249
|
return rows.reverse().map(parseMessageRow);
|
|
218
250
|
}
|
|
@@ -291,6 +323,88 @@ export class MessageStore {
|
|
|
291
323
|
return [];
|
|
292
324
|
}
|
|
293
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Get messages by walking the parent_id chain from a head message backward.
|
|
328
|
+
* This is the DAG-native read path introduced in Phase 3.
|
|
329
|
+
*
|
|
330
|
+
* Walks from headMessageId backward through parent_id links, collecting
|
|
331
|
+
* up to `limit` messages in chronological order.
|
|
332
|
+
*
|
|
333
|
+
* Falls back to getRecentMessages if the head message has no parent chain
|
|
334
|
+
* (e.g., legacy data before backfill).
|
|
335
|
+
*/
|
|
336
|
+
getHistoryByDAGWalk(headMessageId, limit = 50) {
|
|
337
|
+
try {
|
|
338
|
+
// Use recursive CTE to walk backward from head
|
|
339
|
+
const rows = this.db.prepare(`
|
|
340
|
+
WITH RECURSIVE chain AS (
|
|
341
|
+
SELECT id, parent_id, depth, conversation_id, agent_id, role,
|
|
342
|
+
text_content, tool_calls, tool_results, metadata,
|
|
343
|
+
message_index, token_count, is_heartbeat, created_at,
|
|
344
|
+
1 AS chain_pos
|
|
345
|
+
FROM messages
|
|
346
|
+
WHERE id = ?
|
|
347
|
+
|
|
348
|
+
UNION ALL
|
|
349
|
+
|
|
350
|
+
SELECT m.id, m.parent_id, m.depth, m.conversation_id, m.agent_id, m.role,
|
|
351
|
+
m.text_content, m.tool_calls, m.tool_results, m.metadata,
|
|
352
|
+
m.message_index, m.token_count, m.is_heartbeat, m.created_at,
|
|
353
|
+
c.chain_pos + 1
|
|
354
|
+
FROM messages m
|
|
355
|
+
JOIN chain c ON m.id = c.parent_id
|
|
356
|
+
WHERE c.chain_pos < ?
|
|
357
|
+
)
|
|
358
|
+
SELECT * FROM chain ORDER BY depth ASC, message_index ASC
|
|
359
|
+
`).all(headMessageId, limit);
|
|
360
|
+
if (rows.length === 0)
|
|
361
|
+
return [];
|
|
362
|
+
return rows.map(parseMessageRow);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// DAG walk failed (e.g., no parent chain) — return empty, caller should fall back
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Get messages scoped to a specific context_id.
|
|
371
|
+
* Used by keystone/FTS/topic recall to constrain results to the active branch.
|
|
372
|
+
*/
|
|
373
|
+
getMessagesByContextId(contextId, limit = 200, opts) {
|
|
374
|
+
let sql = 'SELECT * FROM messages WHERE context_id = ?';
|
|
375
|
+
const params = [contextId];
|
|
376
|
+
if (opts?.excludeHeartbeats) {
|
|
377
|
+
sql += ' AND is_heartbeat = 0';
|
|
378
|
+
}
|
|
379
|
+
if (opts?.requireText) {
|
|
380
|
+
sql += " AND text_content IS NOT NULL AND text_content != ''";
|
|
381
|
+
}
|
|
382
|
+
sql += ' ORDER BY message_index DESC LIMIT ?';
|
|
383
|
+
params.push(limit);
|
|
384
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
385
|
+
return rows.reverse().map(parseMessageRow);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Full-text search constrained to a specific context_id.
|
|
389
|
+
* Phase 3: replaces unscoped searchMessages for composition paths.
|
|
390
|
+
*/
|
|
391
|
+
searchMessagesByContextId(contextId, query, limit = 20) {
|
|
392
|
+
try {
|
|
393
|
+
const rows = this.db.prepare(`
|
|
394
|
+
WITH fts_matches AS (
|
|
395
|
+
SELECT rowid, rank FROM messages_fts WHERE messages_fts MATCH ? ORDER BY rank LIMIT ?
|
|
396
|
+
)
|
|
397
|
+
SELECT m.* FROM messages m
|
|
398
|
+
JOIN fts_matches ON m.id = fts_matches.rowid
|
|
399
|
+
WHERE m.context_id = ?
|
|
400
|
+
ORDER BY fts_matches.rank
|
|
401
|
+
`).all(query, limit * 3, contextId);
|
|
402
|
+
return rows.slice(0, limit).map(parseMessageRow);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
294
408
|
/**
|
|
295
409
|
* Get message count for a conversation.
|
|
296
410
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Behavioral patterns observed about people, systems, and workflows.
|
|
5
5
|
* Lives in the central library DB.
|
|
6
|
-
* "
|
|
6
|
+
* "operator prefers architectural stability" is a preference, not a fact.
|
|
7
7
|
*/
|
|
8
8
|
import type { DatabaseSync } from 'node:sqlite';
|
|
9
9
|
export interface Preference {
|
package/dist/preference-store.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Behavioral patterns observed about people, systems, and workflows.
|
|
5
5
|
* Lives in the central library DB.
|
|
6
|
-
* "
|
|
6
|
+
* "operator prefers architectural stability" is a preference, not a fact.
|
|
7
7
|
*/
|
|
8
8
|
function nowIso() {
|
|
9
9
|
return new Date().toISOString();
|
package/dist/profiles.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* standard — 128k context, fleet default, balanced
|
|
10
10
|
* rich — 200k+ context, multi-agent, full feature set
|
|
11
11
|
*/
|
|
12
|
-
import type { HyperMemConfig } from './types.js';
|
|
12
|
+
import type { HyperMemConfig, EmbeddingProviderConfig } from './types.js';
|
|
13
|
+
/** Gemini embedding preset — use with mergeProfile() when switching fleet to Gemini. */
|
|
14
|
+
export declare const GEMINI_EMBEDDING: Partial<EmbeddingProviderConfig>;
|
|
13
15
|
export declare const lightProfile: HyperMemConfig;
|
|
14
16
|
export declare const standardProfile: HyperMemConfig;
|
|
15
17
|
export declare const fullProfile: HyperMemConfig;
|
package/dist/profiles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAmC,uBAAuB,EAAe,MAAM,YAAY,CAAC;AAoBxH,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,EAAE,OAAO,CAAC,uBAAuB,CAM7D,CAAC;AAwDF,eAAO,MAAM,YAAY,EAAE,cAa1B,CAAC;AAsDF,eAAO,MAAM,eAAe,EAAE,cAO7B,CAAC;AAuDF,eAAO,MAAM,WAAW,EAAE,cAWzB,CAAC;AAMF,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAGxD,eAAO,MAAM,cAAc,gBAAe,CAAC;AAC3C,eAAO,MAAM,eAAe,gBAAc,CAAC;AAC3C,eAAO,MAAM,WAAW,gBAAc,CAAC;AAEvC,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,cAAc,CAIxD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,cAAc,CAIzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,GAAG,UAAU,EAC9B,SAAS,EAAE,WAAW,CAAC,cAAc,CAAC,GACrC,cAAc,CAUhB;AAMD,KAAK,WAAW,CAAC,CAAC,IAAI;KACnB,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAChE,CAAC"}
|
package/dist/profiles.js
CHANGED
|
@@ -24,6 +24,14 @@ const BASE_EMBEDDING = {
|
|
|
24
24
|
timeout: 10000,
|
|
25
25
|
batchSize: 32,
|
|
26
26
|
};
|
|
27
|
+
/** Gemini embedding preset — use with mergeProfile() when switching fleet to Gemini. */
|
|
28
|
+
export const GEMINI_EMBEDDING = {
|
|
29
|
+
provider: 'gemini',
|
|
30
|
+
model: 'gemini-embedding-2-preview',
|
|
31
|
+
dimensions: 3072,
|
|
32
|
+
timeout: 15000,
|
|
33
|
+
batchSize: 100,
|
|
34
|
+
};
|
|
27
35
|
// ---------------------------------------------------------------------------
|
|
28
36
|
// light — 64k context window, single agent, constrained resources
|
|
29
37
|
//
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repair-tool-pairs.d.ts","sourceRoot":"","sources":["../src/repair-tool-pairs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"repair-tool-pairs.d.ts","sourceRoot":"","sources":["../src/repair-tool-pairs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CA0LpE"}
|
|
@@ -129,9 +129,80 @@ export function repairToolPairs(messages) {
|
|
|
129
129
|
}
|
|
130
130
|
result.push(msg);
|
|
131
131
|
}
|
|
132
|
+
// ── Pass 4: Intra-message content block integrity ─────────────────────────
|
|
133
|
+
// Anthropic requires that every tool_result content block in a user message
|
|
134
|
+
// references a tool_use_id that exists in the IMMEDIATELY PRECEDING assistant
|
|
135
|
+
// message's content blocks. Pass 3 only checks global existence. This pass
|
|
136
|
+
// enforces adjacency.
|
|
137
|
+
let intraBlockDropped = 0;
|
|
138
|
+
for (let i = 0; i < result.length; i++) {
|
|
139
|
+
const msg = result[i];
|
|
140
|
+
if (msg.role !== 'user' || !Array.isArray(msg.content))
|
|
141
|
+
continue;
|
|
142
|
+
const content = msg.content;
|
|
143
|
+
const hasToolResultBlocks = content.some(b => b.type === 'tool_result');
|
|
144
|
+
if (!hasToolResultBlocks)
|
|
145
|
+
continue;
|
|
146
|
+
// Find the immediately preceding assistant message
|
|
147
|
+
let precedingAssistant = null;
|
|
148
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
149
|
+
if (result[j].role === 'assistant') {
|
|
150
|
+
precedingAssistant = result[j];
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Collect tool_use IDs from the preceding assistant message only
|
|
155
|
+
const adjacentCallIds = new Set();
|
|
156
|
+
if (precedingAssistant) {
|
|
157
|
+
// Content array format
|
|
158
|
+
if (Array.isArray(precedingAssistant.content)) {
|
|
159
|
+
for (const block of precedingAssistant.content) {
|
|
160
|
+
if ((block.type === 'toolCall' || block.type === 'tool_use') &&
|
|
161
|
+
typeof block.id === 'string' && block.id) {
|
|
162
|
+
adjacentCallIds.add(block.id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// NeutralMessage format
|
|
167
|
+
if (Array.isArray(precedingAssistant.toolCalls)) {
|
|
168
|
+
for (const tc of precedingAssistant.toolCalls) {
|
|
169
|
+
if (typeof tc.id === 'string' && tc.id)
|
|
170
|
+
adjacentCallIds.add(tc.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Also check pi-agent ToolResultMessages that reference the preceding assistant
|
|
175
|
+
// (these are message-level, not content-block level — skip for adjacency check)
|
|
176
|
+
// Filter: keep tool_result blocks only if their tool_use_id is in the
|
|
177
|
+
// immediately preceding assistant message
|
|
178
|
+
const filteredContent = content.filter(block => {
|
|
179
|
+
if (block.type !== 'tool_result')
|
|
180
|
+
return true;
|
|
181
|
+
const toolUseId = typeof block.tool_use_id === 'string' ? block.tool_use_id : '';
|
|
182
|
+
if (!toolUseId)
|
|
183
|
+
return false; // malformed, drop
|
|
184
|
+
if (adjacentCallIds.has(toolUseId))
|
|
185
|
+
return true;
|
|
186
|
+
intraBlockDropped++;
|
|
187
|
+
return false;
|
|
188
|
+
});
|
|
189
|
+
if (filteredContent.length === 0) {
|
|
190
|
+
// All content blocks were orphaned — remove the message entirely
|
|
191
|
+
result.splice(i, 1);
|
|
192
|
+
i--; // re-check this index
|
|
193
|
+
}
|
|
194
|
+
else if (filteredContent.length !== content.length) {
|
|
195
|
+
result[i] = { ...msg, content: filteredContent };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
132
198
|
const dropped = messages.length - result.length;
|
|
133
|
-
if (dropped > 0) {
|
|
134
|
-
|
|
199
|
+
if (dropped > 0 || intraBlockDropped > 0) {
|
|
200
|
+
const parts = [];
|
|
201
|
+
if (dropped > 0)
|
|
202
|
+
parts.push(`${dropped} orphaned message(s)`);
|
|
203
|
+
if (intraBlockDropped > 0)
|
|
204
|
+
parts.push(`${intraBlockDropped} intra-message orphaned content block(s)`);
|
|
205
|
+
console.log(`[hypermem] repairToolPairs: dropped ${parts.join(' + ')} (${messages.length} → ${result.length})`);
|
|
135
206
|
}
|
|
136
207
|
return result;
|
|
137
208
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Contains ONLY conversation data — structured knowledge lives in library.db.
|
|
7
7
|
*/
|
|
8
8
|
import type { DatabaseSync } from 'node:sqlite';
|
|
9
|
-
export declare const LATEST_SCHEMA_VERSION =
|
|
9
|
+
export declare const LATEST_SCHEMA_VERSION = 8;
|
|
10
10
|
/**
|
|
11
11
|
* Run migrations on an agent message database.
|
|
12
12
|
*/
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAgJvC;;GAEG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAgJvC;;GAEG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA0H9C;AAED,OAAO,EAAE,qBAAqB,IAAI,cAAc,EAAE,CAAC"}
|