audrey 0.14.0 → 0.16.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/src/rollback.js CHANGED
@@ -1,42 +1,42 @@
1
- import { safeJsonParse } from './utils.js';
2
-
3
- /**
4
- * @param {import('better-sqlite3').Database} db
5
- * @returns {Array<{ id: string, checkpoint_cursor: string|null, input_episode_ids: string, output_memory_ids: string, started_at: string, completed_at: string|null, status: string }>}
6
- */
7
- export function getConsolidationHistory(db) {
8
- return db.prepare(`
9
- SELECT id, checkpoint_cursor, input_episode_ids, output_memory_ids,
10
- started_at, completed_at, status
11
- FROM consolidation_runs ORDER BY started_at DESC
12
- `).all();
13
- }
14
-
15
- /**
16
- * @param {import('better-sqlite3').Database} db
17
- * @param {string} runId
18
- * @returns {{ rolledBackMemories: number, restoredEpisodes: number }}
19
- */
20
- export function rollbackConsolidation(db, runId) {
21
- const run = db.prepare('SELECT * FROM consolidation_runs WHERE id = ?').get(runId);
22
- if (!run) throw new Error(`Consolidation run not found: ${runId}`);
23
- if (run.status === 'rolled_back') throw new Error(`Run already rolled back: ${runId}`);
24
-
25
- const outputIds = safeJsonParse(run.output_memory_ids, []);
26
- const inputIds = safeJsonParse(run.input_episode_ids, []);
27
-
28
- const doRollback = db.transaction(() => {
29
- const markSemantics = db.prepare('UPDATE semantics SET state = ? WHERE id = ?');
30
- const markProcedures = db.prepare('UPDATE procedures SET state = ? WHERE id = ?');
31
- for (const id of outputIds) {
32
- markSemantics.run('rolled_back', id);
33
- markProcedures.run('rolled_back', id);
34
- }
35
- const unmark = db.prepare('UPDATE episodes SET consolidated = 0 WHERE id = ?');
36
- for (const id of inputIds) { unmark.run(id); }
37
- db.prepare('UPDATE consolidation_runs SET status = ? WHERE id = ?').run('rolled_back', runId);
38
- });
39
-
40
- doRollback();
41
- return { rolledBackMemories: outputIds.length, restoredEpisodes: inputIds.length };
42
- }
1
+ import { safeJsonParse } from './utils.js';
2
+
3
+ /**
4
+ * @param {import('better-sqlite3').Database} db
5
+ * @returns {Array<{ id: string, checkpoint_cursor: string|null, input_episode_ids: string, output_memory_ids: string, started_at: string, completed_at: string|null, status: string }>}
6
+ */
7
+ export function getConsolidationHistory(db) {
8
+ return db.prepare(`
9
+ SELECT id, checkpoint_cursor, input_episode_ids, output_memory_ids,
10
+ started_at, completed_at, status
11
+ FROM consolidation_runs ORDER BY started_at DESC
12
+ `).all();
13
+ }
14
+
15
+ /**
16
+ * @param {import('better-sqlite3').Database} db
17
+ * @param {string} runId
18
+ * @returns {{ rolledBackMemories: number, restoredEpisodes: number }}
19
+ */
20
+ export function rollbackConsolidation(db, runId) {
21
+ const run = db.prepare('SELECT * FROM consolidation_runs WHERE id = ?').get(runId);
22
+ if (!run) throw new Error(`Consolidation run not found: ${runId}`);
23
+ if (run.status === 'rolled_back') throw new Error(`Run already rolled back: ${runId}`);
24
+
25
+ const outputIds = safeJsonParse(run.output_memory_ids, []);
26
+ const inputIds = safeJsonParse(run.input_episode_ids, []);
27
+
28
+ const doRollback = db.transaction(() => {
29
+ const markSemantics = db.prepare('UPDATE semantics SET state = ? WHERE id = ?');
30
+ const markProcedures = db.prepare('UPDATE procedures SET state = ? WHERE id = ?');
31
+ for (const id of outputIds) {
32
+ markSemantics.run('rolled_back', id);
33
+ markProcedures.run('rolled_back', id);
34
+ }
35
+ const unmark = db.prepare('UPDATE episodes SET consolidated = 0 WHERE id = ?');
36
+ for (const id of inputIds) { unmark.run(id); }
37
+ db.prepare('UPDATE consolidation_runs SET status = ? WHERE id = ?').run('rolled_back', runId);
38
+ });
39
+
40
+ doRollback();
41
+ return { rolledBackMemories: outputIds.length, restoredEpisodes: inputIds.length };
42
+ }
package/src/ulid.js CHANGED
@@ -1,18 +1,18 @@
1
- import { monotonicFactory } from 'ulid';
2
- import { createHash } from 'node:crypto';
3
-
4
- const monotonic = monotonicFactory();
5
-
6
- /** @returns {string} */
7
- export function generateId() {
8
- return monotonic();
9
- }
10
-
11
- /**
12
- * @param {...*} parts
13
- * @returns {string}
14
- */
15
- export function generateDeterministicId(...parts) {
16
- const input = JSON.stringify(parts);
17
- return createHash('sha256').update(input).digest('hex').slice(0, 26);
18
- }
1
+ import { monotonicFactory } from 'ulid';
2
+ import { createHash } from 'node:crypto';
3
+
4
+ const monotonic = monotonicFactory();
5
+
6
+ /** @returns {string} */
7
+ export function generateId() {
8
+ return monotonic();
9
+ }
10
+
11
+ /**
12
+ * @param {...*} parts
13
+ * @returns {string}
14
+ */
15
+ export function generateDeterministicId(...parts) {
16
+ const input = JSON.stringify(parts);
17
+ return createHash('sha256').update(input).digest('hex').slice(0, 26);
18
+ }
package/src/utils.js CHANGED
@@ -1,38 +1,38 @@
1
- /**
2
- * @param {Buffer} bufA
3
- * @param {Buffer} bufB
4
- * @param {import('./embedding.js').EmbeddingProvider} provider
5
- * @returns {number}
6
- */
7
- export function cosineSimilarity(bufA, bufB, provider) {
8
- const a = provider.bufferToVector(bufA);
9
- const b = provider.bufferToVector(bufB);
10
- let dot = 0, magA = 0, magB = 0;
11
- for (let i = 0; i < a.length; i++) {
12
- dot += a[i] * b[i];
13
- magA += a[i] * a[i];
14
- magB += b[i] * b[i];
15
- }
16
- const mag = Math.sqrt(magA) * Math.sqrt(magB);
17
- return mag === 0 ? 0 : dot / mag;
18
- }
19
-
20
- /**
21
- * @param {string} dateStr
22
- * @param {Date} now
23
- * @returns {number}
24
- */
25
- export function daysBetween(dateStr, now) {
26
- return Math.max(0, (now.getTime() - new Date(dateStr).getTime()) / (1000 * 60 * 60 * 24));
27
- }
28
-
29
- /**
30
- * @param {string | null | undefined} str
31
- * @param {*} [fallback=null]
32
- * @returns {*}
33
- */
34
- export function safeJsonParse(str, fallback = null) {
35
- if (!str) return fallback;
36
- try { return JSON.parse(str); }
37
- catch { return fallback; }
38
- }
1
+ /**
2
+ * @param {Buffer} bufA
3
+ * @param {Buffer} bufB
4
+ * @param {import('./embedding.js').EmbeddingProvider} provider
5
+ * @returns {number}
6
+ */
7
+ export function cosineSimilarity(bufA, bufB, provider) {
8
+ const a = provider.bufferToVector(bufA);
9
+ const b = provider.bufferToVector(bufB);
10
+ let dot = 0, magA = 0, magB = 0;
11
+ for (let i = 0; i < a.length; i++) {
12
+ dot += a[i] * b[i];
13
+ magA += a[i] * a[i];
14
+ magB += b[i] * b[i];
15
+ }
16
+ const mag = Math.sqrt(magA) * Math.sqrt(magB);
17
+ return mag === 0 ? 0 : dot / mag;
18
+ }
19
+
20
+ /**
21
+ * @param {string} dateStr
22
+ * @param {Date} now
23
+ * @returns {number}
24
+ */
25
+ export function daysBetween(dateStr, now) {
26
+ return Math.max(0, (now.getTime() - new Date(dateStr).getTime()) / (1000 * 60 * 60 * 24));
27
+ }
28
+
29
+ /**
30
+ * @param {string | null | undefined} str
31
+ * @param {*} [fallback=null]
32
+ * @returns {*}
33
+ */
34
+ export function safeJsonParse(str, fallback = null) {
35
+ if (!str) return fallback;
36
+ try { return JSON.parse(str); }
37
+ catch { return fallback; }
38
+ }
package/src/validate.js CHANGED
@@ -1,172 +1,172 @@
1
- import { generateId } from './ulid.js';
2
- import { safeJsonParse } from './utils.js';
3
- import { buildContradictionDetectionPrompt } from './prompts.js';
4
-
5
- const REINFORCEMENT_THRESHOLD = 0.85;
6
- const CONTRADICTION_THRESHOLD = 0.60;
7
-
8
- /**
9
- * @param {import('better-sqlite3').Database} db
10
- * @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
11
- * @param {{ id: string, content: string, source: string }} episode
12
- * @param {{ threshold?: number, contradictionThreshold?: number, llmProvider?: { json: (messages: any) => Promise<any> } }} [options]
13
- * @returns {Promise<{ action: string, semanticId?: string, similarity?: number, contradictionId?: string, resolution?: string }>}
14
- */
15
- export async function validateMemory(db, embeddingProvider, episode, options = {}) {
16
- const {
17
- threshold = REINFORCEMENT_THRESHOLD,
18
- contradictionThreshold = CONTRADICTION_THRESHOLD,
19
- llmProvider,
20
- } = options;
21
-
22
- const episodeVector = await embeddingProvider.embed(episode.content);
23
- const episodeBuffer = embeddingProvider.vectorToBuffer(episodeVector);
24
-
25
- const nearestSemantic = db.prepare(`
26
- SELECT s.*, (1.0 - v.distance) AS similarity
27
- FROM vec_semantics v
28
- JOIN semantics s ON s.id = v.id
29
- WHERE v.embedding MATCH ?
30
- AND k = 1
31
- AND (v.state = 'active' OR v.state = 'context_dependent')
32
- `).get(episodeBuffer);
33
-
34
- let bestMatch = null;
35
- let bestSimilarity = 0;
36
-
37
- if (nearestSemantic) {
38
- bestMatch = nearestSemantic;
39
- bestSimilarity = nearestSemantic.similarity;
40
- }
41
-
42
- if (bestMatch && bestSimilarity >= threshold) {
43
- const evidenceIds = safeJsonParse(bestMatch.evidence_episode_ids, []);
44
- if (!evidenceIds.includes(episode.id)) {
45
- evidenceIds.push(episode.id);
46
- }
47
-
48
- const diversity = computeSourceDiversity(db, evidenceIds, episode);
49
-
50
- const now = new Date().toISOString();
51
- db.prepare(`
52
- UPDATE semantics SET
53
- supporting_count = supporting_count + 1,
54
- evidence_episode_ids = ?,
55
- evidence_count = ?,
56
- source_type_diversity = ?,
57
- last_reinforced_at = ?
58
- WHERE id = ?
59
- `).run(
60
- JSON.stringify(evidenceIds),
61
- evidenceIds.length,
62
- diversity,
63
- now,
64
- bestMatch.id,
65
- );
66
-
67
- return {
68
- action: 'reinforced',
69
- semanticId: bestMatch.id,
70
- similarity: bestSimilarity,
71
- };
72
- }
73
-
74
- if (bestMatch && bestSimilarity >= contradictionThreshold && llmProvider) {
75
- const messages = buildContradictionDetectionPrompt(episode.content, bestMatch.content);
76
- const verdict = await llmProvider.json(messages);
77
-
78
- if (verdict.contradicts) {
79
- const resolution = verdict.resolution === 'context_dependent'
80
- ? { type: 'context_dependent', conditions: verdict.conditions, explanation: verdict.explanation }
81
- : verdict.resolution
82
- ? { type: verdict.resolution, explanation: verdict.explanation }
83
- : null;
84
-
85
- const contradictionId = createContradiction(
86
- db,
87
- bestMatch.id,
88
- 'semantic',
89
- episode.id,
90
- 'episodic',
91
- resolution,
92
- );
93
-
94
- if (verdict.resolution === 'new_wins') {
95
- db.prepare("UPDATE semantics SET state = 'disputed' WHERE id = ?").run(bestMatch.id);
96
- } else if (verdict.resolution === 'context_dependent' && verdict.conditions) {
97
- db.prepare("UPDATE semantics SET state = 'context_dependent', conditions = ? WHERE id = ?")
98
- .run(JSON.stringify(verdict.conditions), bestMatch.id);
99
- }
100
-
101
- return {
102
- action: 'contradiction',
103
- contradictionId,
104
- semanticId: bestMatch.id,
105
- similarity: bestSimilarity,
106
- resolution: verdict.resolution || null,
107
- };
108
- }
109
- }
110
-
111
- return { action: 'none' };
112
- }
113
-
114
- function computeSourceDiversity(db, evidenceIds, currentEpisode) {
115
- const sourceTypes = new Set();
116
- sourceTypes.add(currentEpisode.source);
117
-
118
- if (evidenceIds.length > 0) {
119
- const placeholders = evidenceIds.map(() => '?').join(',');
120
- const rows = db.prepare(
121
- `SELECT DISTINCT source FROM episodes WHERE id IN (${placeholders})`
122
- ).all(...evidenceIds);
123
- for (const row of rows) {
124
- sourceTypes.add(row.source);
125
- }
126
- }
127
-
128
- return sourceTypes.size;
129
- }
130
-
131
- /**
132
- * @param {import('better-sqlite3').Database} db
133
- * @param {string} claimAId
134
- * @param {string} claimAType
135
- * @param {string} claimBId
136
- * @param {string} claimBType
137
- * @param {object|null} resolution
138
- * @returns {string}
139
- */
140
- export function createContradiction(db, claimAId, claimAType, claimBId, claimBType, resolution) {
141
- const id = generateId();
142
- const now = new Date().toISOString();
143
-
144
- const state = resolution ? 'resolved' : 'open';
145
- const resolvedAt = resolution ? now : null;
146
- const resolutionJson = resolution ? JSON.stringify(resolution) : null;
147
-
148
- db.prepare(`
149
- INSERT INTO contradictions (id, claim_a_id, claim_a_type, claim_b_id, claim_b_type,
150
- state, resolution, resolved_at, created_at)
151
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
152
- `).run(id, claimAId, claimAType, claimBId, claimBType, state, resolutionJson, resolvedAt, now);
153
-
154
- return id;
155
- }
156
-
157
- /**
158
- * @param {import('better-sqlite3').Database} db
159
- * @param {string} contradictionId
160
- * @param {string} newEvidenceId
161
- * @returns {void}
162
- */
163
- export function reopenContradiction(db, contradictionId, newEvidenceId) {
164
- const now = new Date().toISOString();
165
- db.prepare(`
166
- UPDATE contradictions SET
167
- state = 'reopened',
168
- reopen_evidence_id = ?,
169
- reopened_at = ?
170
- WHERE id = ?
171
- `).run(newEvidenceId, now, contradictionId);
172
- }
1
+ import { generateId } from './ulid.js';
2
+ import { safeJsonParse } from './utils.js';
3
+ import { buildContradictionDetectionPrompt } from './prompts.js';
4
+
5
+ const REINFORCEMENT_THRESHOLD = 0.85;
6
+ const CONTRADICTION_THRESHOLD = 0.60;
7
+
8
+ /**
9
+ * @param {import('better-sqlite3').Database} db
10
+ * @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
11
+ * @param {{ id: string, content: string, source: string }} episode
12
+ * @param {{ threshold?: number, contradictionThreshold?: number, llmProvider?: { json: (messages: any) => Promise<any> } }} [options]
13
+ * @returns {Promise<{ action: string, semanticId?: string, similarity?: number, contradictionId?: string, resolution?: string }>}
14
+ */
15
+ export async function validateMemory(db, embeddingProvider, episode, options = {}) {
16
+ const {
17
+ threshold = REINFORCEMENT_THRESHOLD,
18
+ contradictionThreshold = CONTRADICTION_THRESHOLD,
19
+ llmProvider,
20
+ } = options;
21
+
22
+ const episodeVector = await embeddingProvider.embed(episode.content);
23
+ const episodeBuffer = embeddingProvider.vectorToBuffer(episodeVector);
24
+
25
+ const nearestSemantic = db.prepare(`
26
+ SELECT s.*, (1.0 - v.distance) AS similarity
27
+ FROM vec_semantics v
28
+ JOIN semantics s ON s.id = v.id
29
+ WHERE v.embedding MATCH ?
30
+ AND k = 1
31
+ AND (v.state = 'active' OR v.state = 'context_dependent')
32
+ `).get(episodeBuffer);
33
+
34
+ let bestMatch = null;
35
+ let bestSimilarity = 0;
36
+
37
+ if (nearestSemantic) {
38
+ bestMatch = nearestSemantic;
39
+ bestSimilarity = nearestSemantic.similarity;
40
+ }
41
+
42
+ if (bestMatch && bestSimilarity >= threshold) {
43
+ const evidenceIds = safeJsonParse(bestMatch.evidence_episode_ids, []);
44
+ if (!evidenceIds.includes(episode.id)) {
45
+ evidenceIds.push(episode.id);
46
+ }
47
+
48
+ const diversity = computeSourceDiversity(db, evidenceIds, episode);
49
+
50
+ const now = new Date().toISOString();
51
+ db.prepare(`
52
+ UPDATE semantics SET
53
+ supporting_count = supporting_count + 1,
54
+ evidence_episode_ids = ?,
55
+ evidence_count = ?,
56
+ source_type_diversity = ?,
57
+ last_reinforced_at = ?
58
+ WHERE id = ?
59
+ `).run(
60
+ JSON.stringify(evidenceIds),
61
+ evidenceIds.length,
62
+ diversity,
63
+ now,
64
+ bestMatch.id,
65
+ );
66
+
67
+ return {
68
+ action: 'reinforced',
69
+ semanticId: bestMatch.id,
70
+ similarity: bestSimilarity,
71
+ };
72
+ }
73
+
74
+ if (bestMatch && bestSimilarity >= contradictionThreshold && llmProvider) {
75
+ const messages = buildContradictionDetectionPrompt(episode.content, bestMatch.content);
76
+ const verdict = await llmProvider.json(messages);
77
+
78
+ if (verdict.contradicts) {
79
+ const resolution = verdict.resolution === 'context_dependent'
80
+ ? { type: 'context_dependent', conditions: verdict.conditions, explanation: verdict.explanation }
81
+ : verdict.resolution
82
+ ? { type: verdict.resolution, explanation: verdict.explanation }
83
+ : null;
84
+
85
+ const contradictionId = createContradiction(
86
+ db,
87
+ bestMatch.id,
88
+ 'semantic',
89
+ episode.id,
90
+ 'episodic',
91
+ resolution,
92
+ );
93
+
94
+ if (verdict.resolution === 'new_wins') {
95
+ db.prepare("UPDATE semantics SET state = 'disputed' WHERE id = ?").run(bestMatch.id);
96
+ } else if (verdict.resolution === 'context_dependent' && verdict.conditions) {
97
+ db.prepare("UPDATE semantics SET state = 'context_dependent', conditions = ? WHERE id = ?")
98
+ .run(JSON.stringify(verdict.conditions), bestMatch.id);
99
+ }
100
+
101
+ return {
102
+ action: 'contradiction',
103
+ contradictionId,
104
+ semanticId: bestMatch.id,
105
+ similarity: bestSimilarity,
106
+ resolution: verdict.resolution || null,
107
+ };
108
+ }
109
+ }
110
+
111
+ return { action: 'none' };
112
+ }
113
+
114
+ function computeSourceDiversity(db, evidenceIds, currentEpisode) {
115
+ const sourceTypes = new Set();
116
+ sourceTypes.add(currentEpisode.source);
117
+
118
+ if (evidenceIds.length > 0) {
119
+ const placeholders = evidenceIds.map(() => '?').join(',');
120
+ const rows = db.prepare(
121
+ `SELECT DISTINCT source FROM episodes WHERE id IN (${placeholders})`
122
+ ).all(...evidenceIds);
123
+ for (const row of rows) {
124
+ sourceTypes.add(row.source);
125
+ }
126
+ }
127
+
128
+ return sourceTypes.size;
129
+ }
130
+
131
+ /**
132
+ * @param {import('better-sqlite3').Database} db
133
+ * @param {string} claimAId
134
+ * @param {string} claimAType
135
+ * @param {string} claimBId
136
+ * @param {string} claimBType
137
+ * @param {object|null} resolution
138
+ * @returns {string}
139
+ */
140
+ export function createContradiction(db, claimAId, claimAType, claimBId, claimBType, resolution) {
141
+ const id = generateId();
142
+ const now = new Date().toISOString();
143
+
144
+ const state = resolution ? 'resolved' : 'open';
145
+ const resolvedAt = resolution ? now : null;
146
+ const resolutionJson = resolution ? JSON.stringify(resolution) : null;
147
+
148
+ db.prepare(`
149
+ INSERT INTO contradictions (id, claim_a_id, claim_a_type, claim_b_id, claim_b_type,
150
+ state, resolution, resolved_at, created_at)
151
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
152
+ `).run(id, claimAId, claimAType, claimBId, claimBType, state, resolutionJson, resolvedAt, now);
153
+
154
+ return id;
155
+ }
156
+
157
+ /**
158
+ * @param {import('better-sqlite3').Database} db
159
+ * @param {string} contradictionId
160
+ * @param {string} newEvidenceId
161
+ * @returns {void}
162
+ */
163
+ export function reopenContradiction(db, contradictionId, newEvidenceId) {
164
+ const now = new Date().toISOString();
165
+ db.prepare(`
166
+ UPDATE contradictions SET
167
+ state = 'reopened',
168
+ reopen_evidence_id = ?,
169
+ reopened_at = ?
170
+ WHERE id = ?
171
+ `).run(newEvidenceId, now, contradictionId);
172
+ }