@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Context Backfill
|
|
3
|
+
*
|
|
4
|
+
* One-time migration that creates context rows for existing conversations
|
|
5
|
+
* that don't yet have an active context. Designed to be idempotent — running
|
|
6
|
+
* it multiple times produces the same result without modifying existing data.
|
|
7
|
+
*
|
|
8
|
+
* Also provides parent chain backfill (Phase 2): reconstructs parent_id/depth
|
|
9
|
+
* for legacy flat conversations that were created before the Turn DAG model.
|
|
10
|
+
*/
|
|
11
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
12
|
+
/**
|
|
13
|
+
* Backfill active contexts for all existing conversations.
|
|
14
|
+
*
|
|
15
|
+
* For each conversation without an active context:
|
|
16
|
+
* 1. Creates an active context via getOrCreateActiveContext
|
|
17
|
+
* 2. If the conversation has messages, advances the head pointer
|
|
18
|
+
* to the highest message ID
|
|
19
|
+
*
|
|
20
|
+
* @returns counts of created and skipped conversations
|
|
21
|
+
*/
|
|
22
|
+
export declare function backfillContexts(db: DatabaseSync): {
|
|
23
|
+
created: number;
|
|
24
|
+
skipped: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Backfill parent_id and depth for existing messages that lack them.
|
|
28
|
+
*
|
|
29
|
+
* Reconstructs a linear chain per conversation ordered by message_index:
|
|
30
|
+
* - first message: parent_id = NULL, depth = 0
|
|
31
|
+
* - each subsequent: parent_id = previous.id, depth = previous.depth + 1
|
|
32
|
+
*
|
|
33
|
+
* Idempotent: only touches messages where parent_id IS NULL AND depth = 0
|
|
34
|
+
* AND there is more than one message or the message is not the first.
|
|
35
|
+
* In practice, we simply skip messages that already have parent_id set.
|
|
36
|
+
*
|
|
37
|
+
* Also stamps context_id on messages that lack it, using the active context
|
|
38
|
+
* for their conversation.
|
|
39
|
+
*
|
|
40
|
+
* @returns counts of messages updated and conversations processed
|
|
41
|
+
*/
|
|
42
|
+
export declare function backfillParentChains(db: DatabaseSync): {
|
|
43
|
+
conversationsProcessed: number;
|
|
44
|
+
messagesUpdated: number;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=context-backfill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-backfill.d.ts","sourceRoot":"","sources":["../src/context-backfill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAwCvF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,YAAY,GACf;IAAE,sBAAsB,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CA+D7D"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Context Backfill
|
|
3
|
+
*
|
|
4
|
+
* One-time migration that creates context rows for existing conversations
|
|
5
|
+
* that don't yet have an active context. Designed to be idempotent — running
|
|
6
|
+
* it multiple times produces the same result without modifying existing data.
|
|
7
|
+
*
|
|
8
|
+
* Also provides parent chain backfill (Phase 2): reconstructs parent_id/depth
|
|
9
|
+
* for legacy flat conversations that were created before the Turn DAG model.
|
|
10
|
+
*/
|
|
11
|
+
import { getOrCreateActiveContext, updateContextHead } from './context-store.js';
|
|
12
|
+
/**
|
|
13
|
+
* Backfill active contexts for all existing conversations.
|
|
14
|
+
*
|
|
15
|
+
* For each conversation without an active context:
|
|
16
|
+
* 1. Creates an active context via getOrCreateActiveContext
|
|
17
|
+
* 2. If the conversation has messages, advances the head pointer
|
|
18
|
+
* to the highest message ID
|
|
19
|
+
*
|
|
20
|
+
* @returns counts of created and skipped conversations
|
|
21
|
+
*/
|
|
22
|
+
export function backfillContexts(db) {
|
|
23
|
+
let created = 0;
|
|
24
|
+
let skipped = 0;
|
|
25
|
+
const conversations = db
|
|
26
|
+
.prepare('SELECT id, agent_id, session_key FROM conversations')
|
|
27
|
+
.all();
|
|
28
|
+
for (const conv of conversations) {
|
|
29
|
+
// Check if an active context already exists for this conversation
|
|
30
|
+
const existing = db
|
|
31
|
+
.prepare("SELECT id FROM contexts WHERE conversation_id = ? AND status = 'active'")
|
|
32
|
+
.get(conv.id);
|
|
33
|
+
if (existing) {
|
|
34
|
+
skipped++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Find the max message ID for this conversation (may be null if no messages)
|
|
38
|
+
const maxRow = db
|
|
39
|
+
.prepare('SELECT MAX(id) as max_id FROM messages WHERE conversation_id = ?')
|
|
40
|
+
.get(conv.id);
|
|
41
|
+
const maxId = maxRow?.max_id ?? null;
|
|
42
|
+
// Create the active context
|
|
43
|
+
const context = getOrCreateActiveContext(db, conv.agent_id, conv.session_key, conv.id);
|
|
44
|
+
// If conversation has messages, advance head to the latest
|
|
45
|
+
if (maxId !== null) {
|
|
46
|
+
updateContextHead(db, context.id, maxId);
|
|
47
|
+
}
|
|
48
|
+
created++;
|
|
49
|
+
}
|
|
50
|
+
return { created, skipped };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Backfill parent_id and depth for existing messages that lack them.
|
|
54
|
+
*
|
|
55
|
+
* Reconstructs a linear chain per conversation ordered by message_index:
|
|
56
|
+
* - first message: parent_id = NULL, depth = 0
|
|
57
|
+
* - each subsequent: parent_id = previous.id, depth = previous.depth + 1
|
|
58
|
+
*
|
|
59
|
+
* Idempotent: only touches messages where parent_id IS NULL AND depth = 0
|
|
60
|
+
* AND there is more than one message or the message is not the first.
|
|
61
|
+
* In practice, we simply skip messages that already have parent_id set.
|
|
62
|
+
*
|
|
63
|
+
* Also stamps context_id on messages that lack it, using the active context
|
|
64
|
+
* for their conversation.
|
|
65
|
+
*
|
|
66
|
+
* @returns counts of messages updated and conversations processed
|
|
67
|
+
*/
|
|
68
|
+
export function backfillParentChains(db) {
|
|
69
|
+
let conversationsProcessed = 0;
|
|
70
|
+
let messagesUpdated = 0;
|
|
71
|
+
// Get all conversations that have at least one non-first message without parent_id.
|
|
72
|
+
// This is the idempotency guard: after backfill, all messages at index > 0
|
|
73
|
+
// have parent_id set. The first message (index 0) legitimately has parent_id = NULL.
|
|
74
|
+
const conversations = db
|
|
75
|
+
.prepare(`SELECT DISTINCT conversation_id
|
|
76
|
+
FROM messages
|
|
77
|
+
WHERE message_index > 0 AND parent_id IS NULL
|
|
78
|
+
ORDER BY conversation_id`)
|
|
79
|
+
.all();
|
|
80
|
+
const updateStmt = db.prepare('UPDATE messages SET parent_id = ?, depth = ?, context_id = COALESCE(context_id, ?) WHERE id = ?');
|
|
81
|
+
for (const { conversation_id: convId } of conversations) {
|
|
82
|
+
// Get active context for this conversation (if any)
|
|
83
|
+
const ctxRow = db
|
|
84
|
+
.prepare("SELECT id FROM contexts WHERE conversation_id = ? AND status = 'active' LIMIT 1")
|
|
85
|
+
.get(convId);
|
|
86
|
+
const contextId = ctxRow?.id ?? null;
|
|
87
|
+
// Get all messages for this conversation without parent_id, ordered by message_index
|
|
88
|
+
const messages = db
|
|
89
|
+
.prepare('SELECT id, message_index FROM messages WHERE conversation_id = ? AND parent_id IS NULL ORDER BY message_index ASC')
|
|
90
|
+
.all(convId);
|
|
91
|
+
if (messages.length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
// Also get the last message that already HAS a parent_id (if any),
|
|
94
|
+
// so we can chain the backfilled messages after it.
|
|
95
|
+
const lastChainedRow = db
|
|
96
|
+
.prepare('SELECT id, depth FROM messages WHERE conversation_id = ? AND parent_id IS NOT NULL ORDER BY message_index DESC LIMIT 1')
|
|
97
|
+
.get(convId);
|
|
98
|
+
let prevId = lastChainedRow?.id ?? null;
|
|
99
|
+
let prevDepth = lastChainedRow?.depth ?? -1;
|
|
100
|
+
// If there's no prior chain AND the first unlinked message is truly
|
|
101
|
+
// message_index 0, start the chain from scratch
|
|
102
|
+
for (const msg of messages) {
|
|
103
|
+
const depth = prevDepth + 1;
|
|
104
|
+
updateStmt.run(prevId, depth, contextId, msg.id);
|
|
105
|
+
prevId = msg.id;
|
|
106
|
+
prevDepth = depth;
|
|
107
|
+
messagesUpdated++;
|
|
108
|
+
}
|
|
109
|
+
conversationsProcessed++;
|
|
110
|
+
}
|
|
111
|
+
return { conversationsProcessed, messagesUpdated };
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=context-backfill.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Context Store
|
|
3
|
+
*
|
|
4
|
+
* Manages the `contexts` table — a durable record of agent conversation
|
|
5
|
+
* contexts that tracks which session is active, what the current head
|
|
6
|
+
* message is, and supports archival and forking.
|
|
7
|
+
*
|
|
8
|
+
* Each agent + session pair has at most one active context at a time.
|
|
9
|
+
* Contexts are the anchor point for the compositor: they track the
|
|
10
|
+
* head message (most recent message included in composed context) and
|
|
11
|
+
* link back to the underlying conversation.
|
|
12
|
+
*
|
|
13
|
+
* Design principles:
|
|
14
|
+
* - All functions take DatabaseSync as first arg (standalone, no classes)
|
|
15
|
+
* - Fully idempotent — safe to call on every startup
|
|
16
|
+
* - Head pointer is monotone-forward (never moves backward)
|
|
17
|
+
* - Archive is idempotent (no-op if already archived)
|
|
18
|
+
*/
|
|
19
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
20
|
+
export interface Context {
|
|
21
|
+
id: number;
|
|
22
|
+
agentId: string;
|
|
23
|
+
sessionKey: string;
|
|
24
|
+
conversationId: number;
|
|
25
|
+
headMessageId: number | null;
|
|
26
|
+
parentContextId: number | null;
|
|
27
|
+
status: 'active' | 'archived' | 'forked';
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
metadataJson: string | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Add the contexts table and related indexes to an existing messages.db.
|
|
34
|
+
* Also ALTERs the messages table to add a context_id foreign key column.
|
|
35
|
+
* Idempotent — safe to call on every startup.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensureContextSchema(db: DatabaseSync): void;
|
|
38
|
+
/**
|
|
39
|
+
* Get the active context for an agent + session pair.
|
|
40
|
+
* Returns null if no active context exists.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getActiveContext(db: DatabaseSync, agentId: string, sessionKey: string): Context | null;
|
|
43
|
+
/**
|
|
44
|
+
* Get the active context for an agent + session, creating one if none exists.
|
|
45
|
+
*
|
|
46
|
+
* If an active context already exists, returns it unchanged.
|
|
47
|
+
* Otherwise INSERTs a new context with status='active', head_message_id=NULL,
|
|
48
|
+
* and the given conversationId.
|
|
49
|
+
*
|
|
50
|
+
* Idempotent — safe to call repeatedly.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getOrCreateActiveContext(db: DatabaseSync, agentId: string, sessionKey: string, conversationId: number): Context;
|
|
53
|
+
/**
|
|
54
|
+
* Update the head message pointer for a context.
|
|
55
|
+
*
|
|
56
|
+
* Monotone forward: only updates if messageId > current head_message_id
|
|
57
|
+
* (or current is NULL). This prevents accidental regression of the head
|
|
58
|
+
* pointer, matching the compaction-fence monotone progress pattern.
|
|
59
|
+
*/
|
|
60
|
+
export declare function updateContextHead(db: DatabaseSync, contextId: number, messageId: number): void;
|
|
61
|
+
/**
|
|
62
|
+
* Archive a context, setting its status to 'archived'.
|
|
63
|
+
* Idempotent — no-op if already archived.
|
|
64
|
+
*/
|
|
65
|
+
export declare function archiveContext(db: DatabaseSync, contextId: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Rotate a session's active context: archive the current active context
|
|
68
|
+
* and create a new one, optionally linking back via parent_context_id.
|
|
69
|
+
*
|
|
70
|
+
* Used on session restarts/rotations so the new context starts with a
|
|
71
|
+
* clean head pointer instead of inheriting the stale tail.
|
|
72
|
+
*
|
|
73
|
+
* Returns the newly created active context.
|
|
74
|
+
* If no active context exists, simply creates one (no archive step).
|
|
75
|
+
*/
|
|
76
|
+
export declare function rotateSessionContext(db: DatabaseSync, agentId: string, sessionKey: string, conversationId: number): Context;
|
|
77
|
+
//# sourceMappingURL=context-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-store.d.ts","sourceRoot":"","sources":["../src/context-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAyBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAmC1D;AAID;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,GAAG,IAAI,CAShB;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,GACrB,OAAO,CAwBT;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,IAAI,CAeN;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,GAChB,IAAI,CAMN;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,GACrB,OAAO,CA2BT"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Context Store
|
|
3
|
+
*
|
|
4
|
+
* Manages the `contexts` table — a durable record of agent conversation
|
|
5
|
+
* contexts that tracks which session is active, what the current head
|
|
6
|
+
* message is, and supports archival and forking.
|
|
7
|
+
*
|
|
8
|
+
* Each agent + session pair has at most one active context at a time.
|
|
9
|
+
* Contexts are the anchor point for the compositor: they track the
|
|
10
|
+
* head message (most recent message included in composed context) and
|
|
11
|
+
* link back to the underlying conversation.
|
|
12
|
+
*
|
|
13
|
+
* Design principles:
|
|
14
|
+
* - All functions take DatabaseSync as first arg (standalone, no classes)
|
|
15
|
+
* - Fully idempotent — safe to call on every startup
|
|
16
|
+
* - Head pointer is monotone-forward (never moves backward)
|
|
17
|
+
* - Archive is idempotent (no-op if already archived)
|
|
18
|
+
*/
|
|
19
|
+
// ─── Internal Helpers ───────────────────────────────────────────
|
|
20
|
+
function parseContextRow(row) {
|
|
21
|
+
return {
|
|
22
|
+
id: row.id,
|
|
23
|
+
agentId: row.agent_id,
|
|
24
|
+
sessionKey: row.session_key,
|
|
25
|
+
conversationId: row.conversation_id,
|
|
26
|
+
headMessageId: row.head_message_id ?? null,
|
|
27
|
+
parentContextId: row.parent_context_id ?? null,
|
|
28
|
+
status: row.status,
|
|
29
|
+
createdAt: row.created_at,
|
|
30
|
+
updatedAt: row.updated_at,
|
|
31
|
+
metadataJson: row.metadata_json ?? null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function nowIso() {
|
|
35
|
+
return new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
// ─── Schema ─────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Add the contexts table and related indexes to an existing messages.db.
|
|
40
|
+
* Also ALTERs the messages table to add a context_id foreign key column.
|
|
41
|
+
* Idempotent — safe to call on every startup.
|
|
42
|
+
*/
|
|
43
|
+
export function ensureContextSchema(db) {
|
|
44
|
+
db.exec(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS contexts (
|
|
46
|
+
id INTEGER PRIMARY KEY,
|
|
47
|
+
agent_id TEXT NOT NULL,
|
|
48
|
+
session_key TEXT NOT NULL,
|
|
49
|
+
conversation_id INTEGER REFERENCES conversations(id),
|
|
50
|
+
head_message_id INTEGER REFERENCES messages(id),
|
|
51
|
+
parent_context_id INTEGER REFERENCES contexts(id),
|
|
52
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
53
|
+
created_at TEXT NOT NULL,
|
|
54
|
+
updated_at TEXT NOT NULL,
|
|
55
|
+
metadata_json TEXT
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_contexts_active_session
|
|
60
|
+
ON contexts(agent_id, session_key, status)
|
|
61
|
+
WHERE status = 'active'
|
|
62
|
+
`);
|
|
63
|
+
db.exec(`
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_contexts_head
|
|
65
|
+
ON contexts(head_message_id)
|
|
66
|
+
`);
|
|
67
|
+
// ALTER messages table to add context_id column (PRAGMA guard)
|
|
68
|
+
const msgCols = db.prepare('PRAGMA table_info(messages)').all().map(r => r.name);
|
|
69
|
+
if (!msgCols.includes('context_id')) {
|
|
70
|
+
db.exec('ALTER TABLE messages ADD COLUMN context_id INTEGER REFERENCES contexts(id)');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ─── Context Operations ─────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Get the active context for an agent + session pair.
|
|
76
|
+
* Returns null if no active context exists.
|
|
77
|
+
*/
|
|
78
|
+
export function getActiveContext(db, agentId, sessionKey) {
|
|
79
|
+
const row = db
|
|
80
|
+
.prepare('SELECT * FROM contexts WHERE agent_id = ? AND session_key = ? AND status = ?')
|
|
81
|
+
.get(agentId, sessionKey, 'active');
|
|
82
|
+
if (!row)
|
|
83
|
+
return null;
|
|
84
|
+
return parseContextRow(row);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the active context for an agent + session, creating one if none exists.
|
|
88
|
+
*
|
|
89
|
+
* If an active context already exists, returns it unchanged.
|
|
90
|
+
* Otherwise INSERTs a new context with status='active', head_message_id=NULL,
|
|
91
|
+
* and the given conversationId.
|
|
92
|
+
*
|
|
93
|
+
* Idempotent — safe to call repeatedly.
|
|
94
|
+
*/
|
|
95
|
+
export function getOrCreateActiveContext(db, agentId, sessionKey, conversationId) {
|
|
96
|
+
const existing = getActiveContext(db, agentId, sessionKey);
|
|
97
|
+
if (existing)
|
|
98
|
+
return existing;
|
|
99
|
+
const now = nowIso();
|
|
100
|
+
const result = db
|
|
101
|
+
.prepare(`INSERT INTO contexts (agent_id, session_key, conversation_id, head_message_id, parent_context_id, status, created_at, updated_at, metadata_json)
|
|
102
|
+
VALUES (?, ?, ?, NULL, NULL, 'active', ?, ?, NULL)`)
|
|
103
|
+
.run(agentId, sessionKey, conversationId, now, now);
|
|
104
|
+
return {
|
|
105
|
+
id: Number(result.lastInsertRowid),
|
|
106
|
+
agentId,
|
|
107
|
+
sessionKey,
|
|
108
|
+
conversationId,
|
|
109
|
+
headMessageId: null,
|
|
110
|
+
parentContextId: null,
|
|
111
|
+
status: 'active',
|
|
112
|
+
createdAt: now,
|
|
113
|
+
updatedAt: now,
|
|
114
|
+
metadataJson: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Update the head message pointer for a context.
|
|
119
|
+
*
|
|
120
|
+
* Monotone forward: only updates if messageId > current head_message_id
|
|
121
|
+
* (or current is NULL). This prevents accidental regression of the head
|
|
122
|
+
* pointer, matching the compaction-fence monotone progress pattern.
|
|
123
|
+
*/
|
|
124
|
+
export function updateContextHead(db, contextId, messageId) {
|
|
125
|
+
const now = nowIso();
|
|
126
|
+
const row = db
|
|
127
|
+
.prepare('SELECT head_message_id FROM contexts WHERE id = ?')
|
|
128
|
+
.get(contextId);
|
|
129
|
+
if (!row)
|
|
130
|
+
return;
|
|
131
|
+
// Monotone forward: only advance, never regress
|
|
132
|
+
if (row.head_message_id === null || messageId > row.head_message_id) {
|
|
133
|
+
db.prepare('UPDATE contexts SET head_message_id = ?, updated_at = ? WHERE id = ?').run(messageId, now, contextId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Archive a context, setting its status to 'archived'.
|
|
138
|
+
* Idempotent — no-op if already archived.
|
|
139
|
+
*/
|
|
140
|
+
export function archiveContext(db, contextId) {
|
|
141
|
+
const now = nowIso();
|
|
142
|
+
db.prepare(`UPDATE contexts SET status = 'archived', updated_at = ? WHERE id = ? AND status != 'archived'`).run(now, contextId);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Rotate a session's active context: archive the current active context
|
|
146
|
+
* and create a new one, optionally linking back via parent_context_id.
|
|
147
|
+
*
|
|
148
|
+
* Used on session restarts/rotations so the new context starts with a
|
|
149
|
+
* clean head pointer instead of inheriting the stale tail.
|
|
150
|
+
*
|
|
151
|
+
* Returns the newly created active context.
|
|
152
|
+
* If no active context exists, simply creates one (no archive step).
|
|
153
|
+
*/
|
|
154
|
+
export function rotateSessionContext(db, agentId, sessionKey, conversationId) {
|
|
155
|
+
const existing = getActiveContext(db, agentId, sessionKey);
|
|
156
|
+
if (existing) {
|
|
157
|
+
archiveContext(db, existing.id);
|
|
158
|
+
}
|
|
159
|
+
const now = nowIso();
|
|
160
|
+
const result = db
|
|
161
|
+
.prepare(`INSERT INTO contexts (agent_id, session_key, conversation_id, head_message_id, parent_context_id, status, created_at, updated_at, metadata_json)
|
|
162
|
+
VALUES (?, ?, ?, NULL, ?, 'active', ?, ?, NULL)`)
|
|
163
|
+
.run(agentId, sessionKey, conversationId, existing?.id ?? null, now, now);
|
|
164
|
+
return {
|
|
165
|
+
id: Number(result.lastInsertRowid),
|
|
166
|
+
agentId,
|
|
167
|
+
sessionKey,
|
|
168
|
+
conversationId,
|
|
169
|
+
headMessageId: null,
|
|
170
|
+
parentContextId: existing?.id ?? null,
|
|
171
|
+
status: 'active',
|
|
172
|
+
createdAt: now,
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
metadataJson: null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=context-store.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contradiction Detector — heuristic-based contradiction detection for the fact store.
|
|
3
|
+
*
|
|
4
|
+
* Detects when a newly ingested fact contradicts existing active facts using
|
|
5
|
+
* vector similarity (when available) and FTS candidate retrieval, scored by
|
|
6
|
+
* pattern-based heuristics (negation, numeric conflict, state conflict, temporal).
|
|
7
|
+
*
|
|
8
|
+
* No LLM calls — v1 is purely heuristic. LLM-enhanced scoring is a future item.
|
|
9
|
+
*/
|
|
10
|
+
import type { FactStore } from './fact-store.js';
|
|
11
|
+
import type { VectorStore } from './vector-store.js';
|
|
12
|
+
export interface ContradictionCandidate {
|
|
13
|
+
existingFactId: number;
|
|
14
|
+
existingContent: string;
|
|
15
|
+
similarityScore: number;
|
|
16
|
+
contradictionScore: number;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ContradictionResult {
|
|
20
|
+
contradictions: ContradictionCandidate[];
|
|
21
|
+
autoResolved: boolean;
|
|
22
|
+
resolvedCount: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ContradictionDetectorConfig {
|
|
25
|
+
/** Minimum similarity to consider as candidate. Default: 0.6 */
|
|
26
|
+
minSimilarity?: number;
|
|
27
|
+
/** Minimum contradiction score for auto-resolution. Default: 0.85 */
|
|
28
|
+
autoResolveThreshold?: number;
|
|
29
|
+
/** Max candidates to evaluate per ingest. Default: 10 */
|
|
30
|
+
maxCandidates?: number;
|
|
31
|
+
/** Enable auto-resolution. Default: true */
|
|
32
|
+
autoResolve?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class ContradictionDetector {
|
|
35
|
+
private readonly factStore;
|
|
36
|
+
private readonly vectorStore?;
|
|
37
|
+
private readonly config;
|
|
38
|
+
constructor(factStore: FactStore, vectorStore?: VectorStore | undefined, config?: ContradictionDetectorConfig);
|
|
39
|
+
/**
|
|
40
|
+
* On fact ingest, check if the new fact contradicts existing active facts.
|
|
41
|
+
* Uses vector similarity (when available) + FTS to find candidates, then
|
|
42
|
+
* scores each candidate with heuristic contradiction checks.
|
|
43
|
+
*/
|
|
44
|
+
detectOnIngest(agentId: string, newFact: {
|
|
45
|
+
content: string;
|
|
46
|
+
domain?: string;
|
|
47
|
+
}): Promise<ContradictionResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a detected contradiction between an existing fact and a new fact.
|
|
50
|
+
*/
|
|
51
|
+
resolveContradiction(oldFactId: number, newFactId: number, resolution: 'supersede' | 'keep-both' | 'reject-new'): void;
|
|
52
|
+
/**
|
|
53
|
+
* Auto-resolve high-confidence contradictions: newer supersedes older.
|
|
54
|
+
* Only resolves candidates above the autoResolveThreshold.
|
|
55
|
+
*
|
|
56
|
+
* @param agentId - The agent whose facts are being resolved (for audit trail)
|
|
57
|
+
* @param candidates - Scored contradiction candidates from detectOnIngest
|
|
58
|
+
* @returns Count of auto-resolved contradictions
|
|
59
|
+
*/
|
|
60
|
+
autoResolve(_agentId: string, candidates: ContradictionCandidate[]): Promise<number>;
|
|
61
|
+
/**
|
|
62
|
+
* Find candidate facts that might contradict the new fact.
|
|
63
|
+
* Uses vector search (if available) and FTS, deduplicates, and returns
|
|
64
|
+
* up to maxCandidates results above minSimilarity.
|
|
65
|
+
*/
|
|
66
|
+
private findCandidates;
|
|
67
|
+
/**
|
|
68
|
+
* Score a candidate fact against the new fact content for contradiction.
|
|
69
|
+
* Returns a ContradictionCandidate if any heuristic fires, null otherwise.
|
|
70
|
+
*/
|
|
71
|
+
private scoreContradiction;
|
|
72
|
+
/**
|
|
73
|
+
* Compute Jaccard-like token overlap between two texts.
|
|
74
|
+
* Returns 0-1 where 1 means identical token sets.
|
|
75
|
+
*/
|
|
76
|
+
private tokenOverlapSimilarity;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=contradiction-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contradiction-detector.d.ts","sourceRoot":"","sources":["../src/contradiction-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,mBAAmB,CAAC;AAIzE,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,sBAAsB,EAAE,CAAC;IACzC,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,2BAA2B;IAC1C,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAkKD,qBAAa,qBAAqB;IAI9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAJ/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;gBAG5C,SAAS,EAAE,SAAS,EACpB,WAAW,CAAC,EAAE,WAAW,YAAA,EAC1C,MAAM,CAAC,EAAE,2BAA2B;IAKtC;;;;OAIG;IACG,cAAc,CAClB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5C,OAAO,CAAC,mBAAmB,CAAC;IAuB/B;;OAEG;IACH,oBAAoB,CAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GACnD,IAAI;IAcP;;;;;;;OAOG;IACG,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,sBAAsB,EAAE,GACnC,OAAO,CAAC,MAAM,CAAC;IAoBlB;;;;OAIG;YACW,cAAc;IA0E5B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IA0D1B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;CAa/B"}
|