@mnexium/core 0.1.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/.env.example +37 -0
- package/README.md +37 -0
- package/docs/API.md +299 -0
- package/docs/BEHAVIOR.md +224 -0
- package/docs/OPERATIONS.md +116 -0
- package/docs/SETUP.md +239 -0
- package/package.json +22 -0
- package/scripts/e2e.lib.mjs +604 -0
- package/scripts/e2e.routes.mjs +32 -0
- package/scripts/e2e.sh +76 -0
- package/scripts/e2e.webapp.client.js +408 -0
- package/scripts/e2e.webapp.mjs +1065 -0
- package/sql/postgres/schema.sql +275 -0
- package/src/adapters/postgres/PostgresCoreStore.ts +1017 -0
- package/src/ai/memoryExtractionService.ts +265 -0
- package/src/ai/recallService.ts +442 -0
- package/src/ai/types.ts +11 -0
- package/src/contracts/storage.ts +137 -0
- package/src/contracts/types.ts +138 -0
- package/src/dev.ts +144 -0
- package/src/index.ts +15 -0
- package/src/providers/cerebras.ts +101 -0
- package/src/providers/openaiChat.ts +116 -0
- package/src/providers/openaiEmbedding.ts +52 -0
- package/src/server/createCoreServer.ts +1154 -0
- package/src/server/memoryEventBus.ts +57 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import type {
|
|
4
|
+
Claim,
|
|
5
|
+
ClaimAssertion,
|
|
6
|
+
ClaimEdge,
|
|
7
|
+
Memory,
|
|
8
|
+
MemoryRecallEvent,
|
|
9
|
+
MemoryRecallStats,
|
|
10
|
+
ResolvedTruthSlot,
|
|
11
|
+
} from "../../contracts/types";
|
|
12
|
+
import type {
|
|
13
|
+
CoreStore,
|
|
14
|
+
CreateClaimInput,
|
|
15
|
+
CreateMemoryInput,
|
|
16
|
+
UpdateMemoryInput,
|
|
17
|
+
} from "../../contracts/storage";
|
|
18
|
+
|
|
19
|
+
function clampInt(value: number | undefined, min: number, max: number, fallback: number): number {
|
|
20
|
+
const n = Number(value);
|
|
21
|
+
if (!Number.isFinite(n)) return fallback;
|
|
22
|
+
if (n < min) return min;
|
|
23
|
+
if (n > max) return max;
|
|
24
|
+
return Math.floor(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clampFloat(value: number | undefined, min: number, max: number, fallback: number): number {
|
|
28
|
+
const n = Number(value);
|
|
29
|
+
if (!Number.isFinite(n)) return fallback;
|
|
30
|
+
if (n < min) return min;
|
|
31
|
+
if (n > max) return max;
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toVectorLiteral(embedding: number[] | null | undefined): string | null {
|
|
36
|
+
if (!Array.isArray(embedding) || embedding.length === 0) return null;
|
|
37
|
+
const safe = embedding
|
|
38
|
+
.map((v) => Number(v))
|
|
39
|
+
.filter((v) => Number.isFinite(v))
|
|
40
|
+
.map((v) => (Object.is(v, -0) ? 0 : v));
|
|
41
|
+
if (safe.length === 0) return null;
|
|
42
|
+
return `[${safe.join(",")}]`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SEARCH_STOP_WORDS = new Set<string>([
|
|
46
|
+
"a",
|
|
47
|
+
"an",
|
|
48
|
+
"and",
|
|
49
|
+
"are",
|
|
50
|
+
"as",
|
|
51
|
+
"at",
|
|
52
|
+
"be",
|
|
53
|
+
"but",
|
|
54
|
+
"by",
|
|
55
|
+
"does",
|
|
56
|
+
"for",
|
|
57
|
+
"from",
|
|
58
|
+
"how",
|
|
59
|
+
"i",
|
|
60
|
+
"in",
|
|
61
|
+
"is",
|
|
62
|
+
"it",
|
|
63
|
+
"me",
|
|
64
|
+
"my",
|
|
65
|
+
"of",
|
|
66
|
+
"on",
|
|
67
|
+
"or",
|
|
68
|
+
"our",
|
|
69
|
+
"personal",
|
|
70
|
+
"preference",
|
|
71
|
+
"preferences",
|
|
72
|
+
"the",
|
|
73
|
+
"to",
|
|
74
|
+
"user",
|
|
75
|
+
"users",
|
|
76
|
+
"what",
|
|
77
|
+
"where",
|
|
78
|
+
"who",
|
|
79
|
+
"why",
|
|
80
|
+
"you",
|
|
81
|
+
"your",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
function buildSearchTokens(query: string): string[] {
|
|
85
|
+
const raw = String(query || "")
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
88
|
+
.split(/\s+/)
|
|
89
|
+
.map((v) => v.trim())
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
const filtered = raw.filter((token) => token.length >= 2 && !SEARCH_STOP_WORDS.has(token));
|
|
92
|
+
const seen = new Set<string>();
|
|
93
|
+
const out: string[] = [];
|
|
94
|
+
for (const token of filtered) {
|
|
95
|
+
if (seen.has(token)) continue;
|
|
96
|
+
seen.add(token);
|
|
97
|
+
out.push(token);
|
|
98
|
+
}
|
|
99
|
+
return out.slice(0, 10);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inferSlot(predicate: string): string {
|
|
103
|
+
return String(predicate || "").trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function inferClaimType(predicate: string): string {
|
|
107
|
+
const p = String(predicate || "").trim();
|
|
108
|
+
if (!p) return "fact";
|
|
109
|
+
if (p.startsWith("favorite_") || p.startsWith("likes_") || p.startsWith("dislikes_")) return "preference";
|
|
110
|
+
if (p.includes("goal") || p.startsWith("wants_")) return "goal";
|
|
111
|
+
if (p.startsWith("did_") || p.startsWith("event_")) return "event";
|
|
112
|
+
return "fact";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class PostgresCoreStore implements CoreStore {
|
|
116
|
+
private pool: Pool;
|
|
117
|
+
|
|
118
|
+
constructor(pool: Pool) {
|
|
119
|
+
this.pool = pool;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async listMemories(args: {
|
|
123
|
+
project_id: string;
|
|
124
|
+
subject_id: string;
|
|
125
|
+
limit: number;
|
|
126
|
+
offset: number;
|
|
127
|
+
include_deleted?: boolean;
|
|
128
|
+
include_superseded?: boolean;
|
|
129
|
+
}): Promise<Memory[]> {
|
|
130
|
+
const limit = clampInt(args.limit, 1, 200, 50);
|
|
131
|
+
const offset = clampInt(args.offset, 0, 1_000_000, 0);
|
|
132
|
+
const includeDeleted = args.include_deleted === true;
|
|
133
|
+
const includeSuperseded = args.include_superseded === true;
|
|
134
|
+
|
|
135
|
+
const result = await this.pool.query(
|
|
136
|
+
`
|
|
137
|
+
SELECT *
|
|
138
|
+
FROM memories
|
|
139
|
+
WHERE project_id = $1
|
|
140
|
+
AND subject_id = $2
|
|
141
|
+
AND ($3::boolean = TRUE OR is_deleted = FALSE)
|
|
142
|
+
AND ($4::boolean = TRUE OR status = 'active')
|
|
143
|
+
ORDER BY created_at DESC
|
|
144
|
+
LIMIT $5 OFFSET $6
|
|
145
|
+
`,
|
|
146
|
+
[args.project_id, args.subject_id, includeDeleted, includeSuperseded, limit, offset],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return result.rows;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async searchMemories(args: {
|
|
153
|
+
project_id: string;
|
|
154
|
+
subject_id: string;
|
|
155
|
+
q: string;
|
|
156
|
+
query_embedding: number[] | null;
|
|
157
|
+
limit: number;
|
|
158
|
+
min_score: number;
|
|
159
|
+
}): Promise<Array<Memory & { score: number; effective_score: number }>> {
|
|
160
|
+
const limit = clampInt(args.limit, 1, 200, 25);
|
|
161
|
+
const minScore = clampFloat(args.min_score, 0, 100, 30);
|
|
162
|
+
const q = String(args.q || "").trim();
|
|
163
|
+
const searchTokens = buildSearchTokens(q);
|
|
164
|
+
const vector = toVectorLiteral(args.query_embedding);
|
|
165
|
+
|
|
166
|
+
if (vector) {
|
|
167
|
+
const result = await this.pool.query(
|
|
168
|
+
`
|
|
169
|
+
SELECT
|
|
170
|
+
m.*,
|
|
171
|
+
(
|
|
172
|
+
CASE
|
|
173
|
+
WHEN m.embedding IS NULL THEN 0
|
|
174
|
+
ELSE ((1 - (m.embedding <=> $3::vector)) * 100)
|
|
175
|
+
END
|
|
176
|
+
) AS score,
|
|
177
|
+
((0.60 * (
|
|
178
|
+
CASE
|
|
179
|
+
WHEN m.embedding IS NULL THEN 0
|
|
180
|
+
ELSE ((1 - (m.embedding <=> $3::vector)) * 100)
|
|
181
|
+
END
|
|
182
|
+
))
|
|
183
|
+
+ (0.25 * m.importance)
|
|
184
|
+
+ (0.15 * m.confidence * 100)
|
|
185
|
+
+ (
|
|
186
|
+
CASE
|
|
187
|
+
WHEN $4::text <> '' AND m.text ILIKE ('%' || $4 || '%') THEN 20
|
|
188
|
+
WHEN EXISTS (
|
|
189
|
+
SELECT 1
|
|
190
|
+
FROM unnest($6::text[]) AS tok(token)
|
|
191
|
+
WHERE m.text ILIKE ('%' || tok.token || '%')
|
|
192
|
+
) THEN 16
|
|
193
|
+
ELSE 0
|
|
194
|
+
END
|
|
195
|
+
)
|
|
196
|
+
) AS effective_score
|
|
197
|
+
FROM memories m
|
|
198
|
+
WHERE m.project_id = $1
|
|
199
|
+
AND m.subject_id = $2
|
|
200
|
+
AND m.is_deleted = FALSE
|
|
201
|
+
AND m.status = 'active'
|
|
202
|
+
AND (
|
|
203
|
+
$4::text = ''
|
|
204
|
+
OR m.text ILIKE ('%' || $4 || '%')
|
|
205
|
+
OR EXISTS (
|
|
206
|
+
SELECT 1
|
|
207
|
+
FROM unnest($6::text[]) AS tok(token)
|
|
208
|
+
WHERE m.text ILIKE ('%' || tok.token || '%')
|
|
209
|
+
)
|
|
210
|
+
OR (
|
|
211
|
+
m.embedding IS NOT NULL
|
|
212
|
+
AND ((1 - (m.embedding <=> $3::vector)) * 100) >= $5
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
ORDER BY effective_score DESC, score DESC
|
|
216
|
+
LIMIT $7
|
|
217
|
+
`,
|
|
218
|
+
[args.project_id, args.subject_id, vector, q, minScore, searchTokens, limit],
|
|
219
|
+
);
|
|
220
|
+
return result.rows;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const result = await this.pool.query(
|
|
224
|
+
`
|
|
225
|
+
SELECT
|
|
226
|
+
m.*,
|
|
227
|
+
0::double precision AS score,
|
|
228
|
+
(0.25 * m.importance + 0.15 * m.confidence * 100)::double precision AS effective_score
|
|
229
|
+
FROM memories m
|
|
230
|
+
WHERE m.project_id = $1
|
|
231
|
+
AND m.subject_id = $2
|
|
232
|
+
AND m.is_deleted = FALSE
|
|
233
|
+
AND m.status = 'active'
|
|
234
|
+
AND (
|
|
235
|
+
$3::text = ''
|
|
236
|
+
OR m.text ILIKE ('%' || $3 || '%')
|
|
237
|
+
OR EXISTS (
|
|
238
|
+
SELECT 1
|
|
239
|
+
FROM unnest($5::text[]) AS tok(token)
|
|
240
|
+
WHERE m.text ILIKE ('%' || tok.token || '%')
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
ORDER BY m.importance DESC, m.created_at DESC
|
|
244
|
+
LIMIT $4
|
|
245
|
+
`,
|
|
246
|
+
[args.project_id, args.subject_id, q, limit, searchTokens],
|
|
247
|
+
);
|
|
248
|
+
return result.rows;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async createMemory(input: CreateMemoryInput): Promise<Memory> {
|
|
252
|
+
const result = await this.pool.query(
|
|
253
|
+
`
|
|
254
|
+
INSERT INTO memories (
|
|
255
|
+
id, project_id, subject_id, text, kind, visibility, importance,
|
|
256
|
+
confidence, is_temporal, tags, metadata, embedding, source_type,
|
|
257
|
+
status, superseded_by, is_deleted, last_reinforced_at
|
|
258
|
+
)
|
|
259
|
+
VALUES (
|
|
260
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
261
|
+
$8, $9, $10, $11::jsonb, $12::vector, $13,
|
|
262
|
+
'active', NULL, FALSE, NOW()
|
|
263
|
+
)
|
|
264
|
+
RETURNING *
|
|
265
|
+
`,
|
|
266
|
+
[
|
|
267
|
+
input.id,
|
|
268
|
+
input.project_id,
|
|
269
|
+
input.subject_id,
|
|
270
|
+
String(input.text || "").trim(),
|
|
271
|
+
input.kind || "fact",
|
|
272
|
+
input.visibility || "private",
|
|
273
|
+
clampInt(input.importance, 0, 100, 50),
|
|
274
|
+
clampFloat(input.confidence, 0, 1, 0.95),
|
|
275
|
+
input.is_temporal === true,
|
|
276
|
+
Array.isArray(input.tags) ? input.tags.map(String) : [],
|
|
277
|
+
JSON.stringify(input.metadata || {}),
|
|
278
|
+
toVectorLiteral(input.embedding),
|
|
279
|
+
input.source_type || "explicit",
|
|
280
|
+
],
|
|
281
|
+
);
|
|
282
|
+
return result.rows[0];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async getMemory(args: { project_id: string; id: string }): Promise<Memory | null> {
|
|
286
|
+
const result = await this.pool.query(
|
|
287
|
+
`
|
|
288
|
+
SELECT *
|
|
289
|
+
FROM memories
|
|
290
|
+
WHERE project_id = $1
|
|
291
|
+
AND id = $2
|
|
292
|
+
LIMIT 1
|
|
293
|
+
`,
|
|
294
|
+
[args.project_id, args.id],
|
|
295
|
+
);
|
|
296
|
+
return result.rows[0] || null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async getMemoryClaims(args: { project_id: string; memory_id: string }): Promise<ClaimAssertion[]> {
|
|
300
|
+
const result = await this.pool.query(
|
|
301
|
+
`
|
|
302
|
+
SELECT
|
|
303
|
+
ca.assertion_id,
|
|
304
|
+
ca.project_id,
|
|
305
|
+
ca.subject_id,
|
|
306
|
+
ca.claim_id,
|
|
307
|
+
ca.memory_id,
|
|
308
|
+
ca.predicate,
|
|
309
|
+
ca.object_type,
|
|
310
|
+
ca.value_string,
|
|
311
|
+
ca.value_number,
|
|
312
|
+
ca.value_date,
|
|
313
|
+
ca.value_json,
|
|
314
|
+
ca.confidence,
|
|
315
|
+
ca.status,
|
|
316
|
+
ca.first_seen_at,
|
|
317
|
+
ca.last_seen_at
|
|
318
|
+
FROM claim_assertions ca
|
|
319
|
+
WHERE ca.project_id = $1
|
|
320
|
+
AND ca.memory_id = $2
|
|
321
|
+
ORDER BY ca.last_seen_at DESC
|
|
322
|
+
`,
|
|
323
|
+
[args.project_id, args.memory_id],
|
|
324
|
+
);
|
|
325
|
+
return result.rows;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async updateMemory(args: { project_id: string; id: string; patch: UpdateMemoryInput }): Promise<Memory | null> {
|
|
329
|
+
const existing = await this.getMemory({ project_id: args.project_id, id: args.id });
|
|
330
|
+
if (!existing) return null;
|
|
331
|
+
|
|
332
|
+
const patch = args.patch || {};
|
|
333
|
+
const merged = {
|
|
334
|
+
text: patch.text !== undefined ? String(patch.text).trim() : existing.text,
|
|
335
|
+
kind: patch.kind !== undefined ? patch.kind : existing.kind,
|
|
336
|
+
visibility: patch.visibility !== undefined ? patch.visibility : existing.visibility,
|
|
337
|
+
importance: patch.importance !== undefined ? clampInt(patch.importance, 0, 100, existing.importance) : existing.importance,
|
|
338
|
+
confidence: patch.confidence !== undefined ? clampFloat(patch.confidence, 0, 1, existing.confidence) : existing.confidence,
|
|
339
|
+
is_temporal: patch.is_temporal !== undefined ? patch.is_temporal : existing.is_temporal,
|
|
340
|
+
tags: patch.tags !== undefined ? patch.tags.map(String) : existing.tags,
|
|
341
|
+
metadata: patch.metadata !== undefined ? patch.metadata : existing.metadata,
|
|
342
|
+
embedding: patch.embedding !== undefined ? patch.embedding : null,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const result = await this.pool.query(
|
|
346
|
+
`
|
|
347
|
+
UPDATE memories
|
|
348
|
+
SET
|
|
349
|
+
text = $3,
|
|
350
|
+
kind = $4,
|
|
351
|
+
visibility = $5,
|
|
352
|
+
importance = $6,
|
|
353
|
+
confidence = $7,
|
|
354
|
+
is_temporal = $8,
|
|
355
|
+
tags = $9,
|
|
356
|
+
metadata = $10::jsonb,
|
|
357
|
+
embedding = COALESCE($11::vector, embedding)
|
|
358
|
+
WHERE project_id = $1
|
|
359
|
+
AND id = $2
|
|
360
|
+
RETURNING *
|
|
361
|
+
`,
|
|
362
|
+
[
|
|
363
|
+
args.project_id,
|
|
364
|
+
args.id,
|
|
365
|
+
merged.text,
|
|
366
|
+
merged.kind,
|
|
367
|
+
merged.visibility,
|
|
368
|
+
merged.importance,
|
|
369
|
+
merged.confidence,
|
|
370
|
+
merged.is_temporal,
|
|
371
|
+
merged.tags,
|
|
372
|
+
JSON.stringify(merged.metadata || {}),
|
|
373
|
+
toVectorLiteral(merged.embedding),
|
|
374
|
+
],
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
return result.rows[0] || null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async deleteMemory(args: { project_id: string; id: string }): Promise<{ ok: true; deleted: boolean }> {
|
|
381
|
+
const result = await this.pool.query(
|
|
382
|
+
`
|
|
383
|
+
UPDATE memories
|
|
384
|
+
SET is_deleted = TRUE
|
|
385
|
+
WHERE project_id = $1
|
|
386
|
+
AND id = $2
|
|
387
|
+
AND is_deleted = FALSE
|
|
388
|
+
`,
|
|
389
|
+
[args.project_id, args.id],
|
|
390
|
+
);
|
|
391
|
+
return { ok: true, deleted: result.rowCount > 0 };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async listSupersededMemories(args: {
|
|
395
|
+
project_id: string;
|
|
396
|
+
subject_id: string;
|
|
397
|
+
limit: number;
|
|
398
|
+
offset: number;
|
|
399
|
+
}): Promise<Memory[]> {
|
|
400
|
+
const limit = clampInt(args.limit, 1, 200, 50);
|
|
401
|
+
const offset = clampInt(args.offset, 0, 1_000_000, 0);
|
|
402
|
+
const result = await this.pool.query(
|
|
403
|
+
`
|
|
404
|
+
SELECT *
|
|
405
|
+
FROM memories
|
|
406
|
+
WHERE project_id = $1
|
|
407
|
+
AND subject_id = $2
|
|
408
|
+
AND is_deleted = FALSE
|
|
409
|
+
AND status = 'superseded'
|
|
410
|
+
ORDER BY created_at DESC
|
|
411
|
+
LIMIT $3 OFFSET $4
|
|
412
|
+
`,
|
|
413
|
+
[args.project_id, args.subject_id, limit, offset],
|
|
414
|
+
);
|
|
415
|
+
return result.rows;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async restoreMemory(args: { project_id: string; id: string }): Promise<Memory | null> {
|
|
419
|
+
const result = await this.pool.query(
|
|
420
|
+
`
|
|
421
|
+
UPDATE memories
|
|
422
|
+
SET status = 'active', superseded_by = NULL
|
|
423
|
+
WHERE project_id = $1
|
|
424
|
+
AND id = $2
|
|
425
|
+
AND is_deleted = FALSE
|
|
426
|
+
RETURNING *
|
|
427
|
+
`,
|
|
428
|
+
[args.project_id, args.id],
|
|
429
|
+
);
|
|
430
|
+
return result.rows[0] || null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async findDuplicateMemory(args: {
|
|
434
|
+
project_id: string;
|
|
435
|
+
subject_id: string;
|
|
436
|
+
embedding: number[];
|
|
437
|
+
threshold: number;
|
|
438
|
+
}): Promise<{ id: string; similarity: number } | null> {
|
|
439
|
+
const vector = toVectorLiteral(args.embedding);
|
|
440
|
+
if (!vector) return null;
|
|
441
|
+
const threshold = clampFloat(args.threshold, 0, 100, 85);
|
|
442
|
+
const result = await this.pool.query(
|
|
443
|
+
`
|
|
444
|
+
SELECT
|
|
445
|
+
m.id,
|
|
446
|
+
((1 - (m.embedding <=> $3::vector)) * 100) AS similarity
|
|
447
|
+
FROM memories m
|
|
448
|
+
WHERE m.project_id = $1
|
|
449
|
+
AND m.subject_id = $2
|
|
450
|
+
AND m.is_deleted = FALSE
|
|
451
|
+
AND m.status = 'active'
|
|
452
|
+
AND m.embedding IS NOT NULL
|
|
453
|
+
AND ((1 - (m.embedding <=> $3::vector)) * 100) >= $4
|
|
454
|
+
ORDER BY similarity DESC
|
|
455
|
+
LIMIT 1
|
|
456
|
+
`,
|
|
457
|
+
[args.project_id, args.subject_id, vector, threshold],
|
|
458
|
+
);
|
|
459
|
+
return result.rows[0] || null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async findConflictingMemories(args: {
|
|
463
|
+
project_id: string;
|
|
464
|
+
subject_id: string;
|
|
465
|
+
embedding: number[];
|
|
466
|
+
min_similarity: number;
|
|
467
|
+
max_similarity: number;
|
|
468
|
+
limit: number;
|
|
469
|
+
}): Promise<Array<{ id: string; similarity: number }>> {
|
|
470
|
+
const vector = toVectorLiteral(args.embedding);
|
|
471
|
+
if (!vector) return [];
|
|
472
|
+
const minSimilarity = clampFloat(args.min_similarity, 0, 100, 60);
|
|
473
|
+
const maxSimilarity = clampFloat(args.max_similarity, minSimilarity, 100, 85);
|
|
474
|
+
const limit = clampInt(args.limit, 1, 200, 25);
|
|
475
|
+
const result = await this.pool.query(
|
|
476
|
+
`
|
|
477
|
+
SELECT
|
|
478
|
+
m.id,
|
|
479
|
+
((1 - (m.embedding <=> $3::vector)) * 100) AS similarity
|
|
480
|
+
FROM memories m
|
|
481
|
+
WHERE m.project_id = $1
|
|
482
|
+
AND m.subject_id = $2
|
|
483
|
+
AND m.is_deleted = FALSE
|
|
484
|
+
AND m.status = 'active'
|
|
485
|
+
AND m.embedding IS NOT NULL
|
|
486
|
+
AND ((1 - (m.embedding <=> $3::vector)) * 100) >= $4
|
|
487
|
+
AND ((1 - (m.embedding <=> $3::vector)) * 100) < $5
|
|
488
|
+
ORDER BY similarity DESC
|
|
489
|
+
LIMIT $6
|
|
490
|
+
`,
|
|
491
|
+
[args.project_id, args.subject_id, vector, minSimilarity, maxSimilarity, limit],
|
|
492
|
+
);
|
|
493
|
+
return result.rows;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async supersedeMemories(args: {
|
|
497
|
+
project_id: string;
|
|
498
|
+
subject_id: string;
|
|
499
|
+
memory_ids: string[];
|
|
500
|
+
superseded_by: string;
|
|
501
|
+
}): Promise<number> {
|
|
502
|
+
const ids = Array.isArray(args.memory_ids) ? args.memory_ids.map((v) => String(v || "").trim()).filter(Boolean) : [];
|
|
503
|
+
if (ids.length === 0) return 0;
|
|
504
|
+
const result = await this.pool.query(
|
|
505
|
+
`
|
|
506
|
+
UPDATE memories
|
|
507
|
+
SET status = 'superseded', superseded_by = $4
|
|
508
|
+
WHERE project_id = $1
|
|
509
|
+
AND subject_id = $2
|
|
510
|
+
AND id = ANY($3::text[])
|
|
511
|
+
AND is_deleted = FALSE
|
|
512
|
+
AND status = 'active'
|
|
513
|
+
`,
|
|
514
|
+
[args.project_id, args.subject_id, ids, args.superseded_by],
|
|
515
|
+
);
|
|
516
|
+
return Number(result.rowCount || 0);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async getRecallEventsByChat(args: { project_id: string; chat_id: string }): Promise<MemoryRecallEvent[]> {
|
|
520
|
+
const result = await this.pool.query(
|
|
521
|
+
`
|
|
522
|
+
SELECT *
|
|
523
|
+
FROM memory_recall_events
|
|
524
|
+
WHERE project_id = $1
|
|
525
|
+
AND chat_id = $2
|
|
526
|
+
ORDER BY recalled_at ASC, message_index ASC
|
|
527
|
+
`,
|
|
528
|
+
[args.project_id, args.chat_id],
|
|
529
|
+
);
|
|
530
|
+
return result.rows;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async getRecallEventsByMemory(args: {
|
|
534
|
+
project_id: string;
|
|
535
|
+
memory_id: string;
|
|
536
|
+
limit: number;
|
|
537
|
+
}): Promise<MemoryRecallEvent[]> {
|
|
538
|
+
const limit = clampInt(args.limit, 1, 1000, 100);
|
|
539
|
+
const result = await this.pool.query(
|
|
540
|
+
`
|
|
541
|
+
SELECT *
|
|
542
|
+
FROM memory_recall_events
|
|
543
|
+
WHERE project_id = $1
|
|
544
|
+
AND memory_id = $2
|
|
545
|
+
ORDER BY recalled_at DESC
|
|
546
|
+
LIMIT $3
|
|
547
|
+
`,
|
|
548
|
+
[args.project_id, args.memory_id, limit],
|
|
549
|
+
);
|
|
550
|
+
return result.rows;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async getMemoryRecallStats(args: {
|
|
554
|
+
project_id: string;
|
|
555
|
+
memory_id: string;
|
|
556
|
+
}): Promise<MemoryRecallStats> {
|
|
557
|
+
const result = await this.pool.query(
|
|
558
|
+
`
|
|
559
|
+
SELECT
|
|
560
|
+
COUNT(*)::int AS total_recalls,
|
|
561
|
+
COUNT(DISTINCT chat_id)::int AS unique_chats,
|
|
562
|
+
COUNT(DISTINCT subject_id)::int AS unique_subjects,
|
|
563
|
+
COALESCE(AVG(similarity_score), 0)::double precision AS avg_score,
|
|
564
|
+
MIN(recalled_at) AS first_recalled_at,
|
|
565
|
+
MAX(recalled_at) AS last_recalled_at
|
|
566
|
+
FROM memory_recall_events
|
|
567
|
+
WHERE project_id = $1
|
|
568
|
+
AND memory_id = $2
|
|
569
|
+
`,
|
|
570
|
+
[args.project_id, args.memory_id],
|
|
571
|
+
);
|
|
572
|
+
return (
|
|
573
|
+
result.rows[0] || {
|
|
574
|
+
total_recalls: 0,
|
|
575
|
+
unique_chats: 0,
|
|
576
|
+
unique_subjects: 0,
|
|
577
|
+
avg_score: 0,
|
|
578
|
+
first_recalled_at: null,
|
|
579
|
+
last_recalled_at: null,
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async createClaim(input: CreateClaimInput): Promise<Claim> {
|
|
585
|
+
const slot = input.slot || inferSlot(input.predicate);
|
|
586
|
+
const claimType = input.claim_type || inferClaimType(input.predicate);
|
|
587
|
+
const confidence = clampFloat(input.confidence, 0, 1, 0.8);
|
|
588
|
+
const importance = clampFloat(input.importance, 0, 1, 0.5);
|
|
589
|
+
|
|
590
|
+
const client = await this.pool.connect();
|
|
591
|
+
try {
|
|
592
|
+
await client.query("BEGIN");
|
|
593
|
+
|
|
594
|
+
const claimResult = await client.query(
|
|
595
|
+
`
|
|
596
|
+
INSERT INTO claims (
|
|
597
|
+
claim_id, project_id, subject_id, predicate, object_value, slot, claim_type,
|
|
598
|
+
confidence, importance, tags, source_memory_id, source_observation_id,
|
|
599
|
+
subject_entity, status, embedding, valid_from, valid_until
|
|
600
|
+
)
|
|
601
|
+
VALUES (
|
|
602
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
603
|
+
$8, $9, $10, $11, $12,
|
|
604
|
+
$13, 'active', $14::vector, $15::timestamptz, $16::timestamptz
|
|
605
|
+
)
|
|
606
|
+
RETURNING *
|
|
607
|
+
`,
|
|
608
|
+
[
|
|
609
|
+
input.claim_id,
|
|
610
|
+
input.project_id,
|
|
611
|
+
input.subject_id,
|
|
612
|
+
input.predicate,
|
|
613
|
+
input.object_value,
|
|
614
|
+
slot,
|
|
615
|
+
claimType,
|
|
616
|
+
confidence,
|
|
617
|
+
importance,
|
|
618
|
+
Array.isArray(input.tags) ? input.tags.map(String) : [],
|
|
619
|
+
input.source_memory_id || null,
|
|
620
|
+
input.source_observation_id || null,
|
|
621
|
+
input.subject_entity || "self",
|
|
622
|
+
toVectorLiteral(input.embedding),
|
|
623
|
+
input.valid_from || null,
|
|
624
|
+
input.valid_until || null,
|
|
625
|
+
],
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const assertionId = `ast_${randomUUID()}`;
|
|
629
|
+
await client.query(
|
|
630
|
+
`
|
|
631
|
+
INSERT INTO claim_assertions (
|
|
632
|
+
assertion_id, project_id, subject_id, claim_id, memory_id,
|
|
633
|
+
predicate, object_type, value_string, confidence, status
|
|
634
|
+
)
|
|
635
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'string', $7, $8, 'active')
|
|
636
|
+
`,
|
|
637
|
+
[
|
|
638
|
+
assertionId,
|
|
639
|
+
input.project_id,
|
|
640
|
+
input.subject_id,
|
|
641
|
+
input.claim_id,
|
|
642
|
+
input.source_memory_id || null,
|
|
643
|
+
input.predicate,
|
|
644
|
+
input.object_value,
|
|
645
|
+
confidence,
|
|
646
|
+
],
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
await client.query(
|
|
650
|
+
`
|
|
651
|
+
INSERT INTO slot_state (
|
|
652
|
+
project_id, subject_id, slot, active_claim_id, status, replaced_by_claim_id
|
|
653
|
+
)
|
|
654
|
+
VALUES ($1, $2, $3, $4, 'active', NULL)
|
|
655
|
+
ON CONFLICT (project_id, subject_id, slot)
|
|
656
|
+
DO UPDATE SET
|
|
657
|
+
active_claim_id = EXCLUDED.active_claim_id,
|
|
658
|
+
status = 'active',
|
|
659
|
+
replaced_by_claim_id = NULL,
|
|
660
|
+
updated_at = NOW()
|
|
661
|
+
`,
|
|
662
|
+
[input.project_id, input.subject_id, slot, input.claim_id],
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
await client.query("COMMIT");
|
|
666
|
+
return claimResult.rows[0];
|
|
667
|
+
} catch (err) {
|
|
668
|
+
await client.query("ROLLBACK");
|
|
669
|
+
throw err;
|
|
670
|
+
} finally {
|
|
671
|
+
client.release();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async getClaim(args: { project_id: string; claim_id: string }): Promise<Claim | null> {
|
|
676
|
+
const result = await this.pool.query(
|
|
677
|
+
`
|
|
678
|
+
SELECT *
|
|
679
|
+
FROM claims
|
|
680
|
+
WHERE project_id = $1
|
|
681
|
+
AND claim_id = $2
|
|
682
|
+
LIMIT 1
|
|
683
|
+
`,
|
|
684
|
+
[args.project_id, args.claim_id],
|
|
685
|
+
);
|
|
686
|
+
return result.rows[0] || null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async getAssertionsForClaim(args: { project_id: string; claim_id: string }): Promise<ClaimAssertion[]> {
|
|
690
|
+
const result = await this.pool.query(
|
|
691
|
+
`
|
|
692
|
+
SELECT *
|
|
693
|
+
FROM claim_assertions
|
|
694
|
+
WHERE project_id = $1
|
|
695
|
+
AND claim_id = $2
|
|
696
|
+
ORDER BY last_seen_at DESC
|
|
697
|
+
`,
|
|
698
|
+
[args.project_id, args.claim_id],
|
|
699
|
+
);
|
|
700
|
+
return result.rows;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async getEdgesForClaim(args: { project_id: string; claim_id: string }): Promise<ClaimEdge[]> {
|
|
704
|
+
const result = await this.pool.query(
|
|
705
|
+
`
|
|
706
|
+
SELECT *
|
|
707
|
+
FROM claim_edges
|
|
708
|
+
WHERE project_id = $1
|
|
709
|
+
AND (from_claim_id = $2 OR to_claim_id = $2)
|
|
710
|
+
ORDER BY created_at DESC
|
|
711
|
+
`,
|
|
712
|
+
[args.project_id, args.claim_id],
|
|
713
|
+
);
|
|
714
|
+
return result.rows;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async getCurrentTruth(args: { project_id: string; subject_id: string }): Promise<ResolvedTruthSlot[]> {
|
|
718
|
+
const result = await this.pool.query(
|
|
719
|
+
`
|
|
720
|
+
SELECT
|
|
721
|
+
ss.slot,
|
|
722
|
+
ss.active_claim_id,
|
|
723
|
+
c.predicate,
|
|
724
|
+
c.object_value,
|
|
725
|
+
c.claim_type,
|
|
726
|
+
c.confidence,
|
|
727
|
+
c.tags,
|
|
728
|
+
ss.updated_at,
|
|
729
|
+
c.source_memory_id,
|
|
730
|
+
c.source_observation_id
|
|
731
|
+
FROM slot_state ss
|
|
732
|
+
INNER JOIN claims c
|
|
733
|
+
ON c.project_id = ss.project_id
|
|
734
|
+
AND c.claim_id = ss.active_claim_id
|
|
735
|
+
WHERE ss.project_id = $1
|
|
736
|
+
AND ss.subject_id = $2
|
|
737
|
+
AND ss.status = 'active'
|
|
738
|
+
AND c.status = 'active'
|
|
739
|
+
ORDER BY ss.updated_at DESC
|
|
740
|
+
`,
|
|
741
|
+
[args.project_id, args.subject_id],
|
|
742
|
+
);
|
|
743
|
+
return result.rows;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async getCurrentSlot(args: {
|
|
747
|
+
project_id: string;
|
|
748
|
+
subject_id: string;
|
|
749
|
+
slot: string;
|
|
750
|
+
}): Promise<ResolvedTruthSlot | null> {
|
|
751
|
+
const result = await this.pool.query(
|
|
752
|
+
`
|
|
753
|
+
SELECT
|
|
754
|
+
ss.slot,
|
|
755
|
+
ss.active_claim_id,
|
|
756
|
+
c.predicate,
|
|
757
|
+
c.object_value,
|
|
758
|
+
c.claim_type,
|
|
759
|
+
c.confidence,
|
|
760
|
+
c.tags,
|
|
761
|
+
ss.updated_at,
|
|
762
|
+
c.source_memory_id,
|
|
763
|
+
c.source_observation_id
|
|
764
|
+
FROM slot_state ss
|
|
765
|
+
INNER JOIN claims c
|
|
766
|
+
ON c.project_id = ss.project_id
|
|
767
|
+
AND c.claim_id = ss.active_claim_id
|
|
768
|
+
WHERE ss.project_id = $1
|
|
769
|
+
AND ss.subject_id = $2
|
|
770
|
+
AND ss.slot = $3
|
|
771
|
+
AND ss.status = 'active'
|
|
772
|
+
AND c.status = 'active'
|
|
773
|
+
LIMIT 1
|
|
774
|
+
`,
|
|
775
|
+
[args.project_id, args.subject_id, args.slot],
|
|
776
|
+
);
|
|
777
|
+
return result.rows[0] || null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async getSlots(args: {
|
|
781
|
+
project_id: string;
|
|
782
|
+
subject_id: string;
|
|
783
|
+
limit: number;
|
|
784
|
+
}): Promise<Array<ResolvedTruthSlot & { status: string }>> {
|
|
785
|
+
const limit = clampInt(args.limit, 1, 500, 100);
|
|
786
|
+
const result = await this.pool.query(
|
|
787
|
+
`
|
|
788
|
+
SELECT
|
|
789
|
+
ss.slot,
|
|
790
|
+
ss.active_claim_id,
|
|
791
|
+
COALESCE(c.predicate, '') AS predicate,
|
|
792
|
+
COALESCE(c.object_value, '') AS object_value,
|
|
793
|
+
COALESCE(c.claim_type, '') AS claim_type,
|
|
794
|
+
COALESCE(c.confidence, 0) AS confidence,
|
|
795
|
+
COALESCE(c.tags, '{}') AS tags,
|
|
796
|
+
ss.updated_at,
|
|
797
|
+
c.source_memory_id,
|
|
798
|
+
c.source_observation_id,
|
|
799
|
+
ss.status
|
|
800
|
+
FROM slot_state ss
|
|
801
|
+
LEFT JOIN claims c
|
|
802
|
+
ON c.project_id = ss.project_id
|
|
803
|
+
AND c.claim_id = ss.active_claim_id
|
|
804
|
+
WHERE ss.project_id = $1
|
|
805
|
+
AND ss.subject_id = $2
|
|
806
|
+
ORDER BY ss.updated_at DESC
|
|
807
|
+
LIMIT $3
|
|
808
|
+
`,
|
|
809
|
+
[args.project_id, args.subject_id, limit],
|
|
810
|
+
);
|
|
811
|
+
return result.rows;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async getClaimGraph(args: {
|
|
815
|
+
project_id: string;
|
|
816
|
+
subject_id: string;
|
|
817
|
+
limit: number;
|
|
818
|
+
}): Promise<{ claims: Claim[]; edges: ClaimEdge[] }> {
|
|
819
|
+
const limit = clampInt(args.limit, 1, 200, 50);
|
|
820
|
+
const claimResult = await this.pool.query(
|
|
821
|
+
`
|
|
822
|
+
SELECT *
|
|
823
|
+
FROM claims
|
|
824
|
+
WHERE project_id = $1
|
|
825
|
+
AND subject_id = $2
|
|
826
|
+
ORDER BY asserted_at DESC
|
|
827
|
+
LIMIT $3
|
|
828
|
+
`,
|
|
829
|
+
[args.project_id, args.subject_id, limit],
|
|
830
|
+
);
|
|
831
|
+
const claims = claimResult.rows as Claim[];
|
|
832
|
+
if (claims.length === 0) return { claims: [], edges: [] };
|
|
833
|
+
|
|
834
|
+
const claimIds = claims.map((c) => c.claim_id);
|
|
835
|
+
const edgeResult = await this.pool.query(
|
|
836
|
+
`
|
|
837
|
+
SELECT *
|
|
838
|
+
FROM claim_edges
|
|
839
|
+
WHERE project_id = $1
|
|
840
|
+
AND (from_claim_id = ANY($2::text[]) OR to_claim_id = ANY($2::text[]))
|
|
841
|
+
ORDER BY created_at DESC
|
|
842
|
+
`,
|
|
843
|
+
[args.project_id, claimIds],
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
return { claims, edges: edgeResult.rows };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async getClaimHistory(args: {
|
|
850
|
+
project_id: string;
|
|
851
|
+
subject_id: string;
|
|
852
|
+
slot?: string | null;
|
|
853
|
+
limit: number;
|
|
854
|
+
}): Promise<{ claims: Claim[]; edges: ClaimEdge[]; by_slot: Record<string, Claim[]> }> {
|
|
855
|
+
const limit = clampInt(args.limit, 1, 500, 100);
|
|
856
|
+
const hasSlot = !!(args.slot && String(args.slot).trim());
|
|
857
|
+
const claimResult = await this.pool.query(
|
|
858
|
+
`
|
|
859
|
+
SELECT *
|
|
860
|
+
FROM claims
|
|
861
|
+
WHERE project_id = $1
|
|
862
|
+
AND subject_id = $2
|
|
863
|
+
AND ($3::boolean = FALSE OR slot = $4)
|
|
864
|
+
ORDER BY asserted_at DESC
|
|
865
|
+
LIMIT $5
|
|
866
|
+
`,
|
|
867
|
+
[args.project_id, args.subject_id, hasSlot, args.slot || null, limit],
|
|
868
|
+
);
|
|
869
|
+
const claims = claimResult.rows as Claim[];
|
|
870
|
+
const claimIds = claims.map((c) => c.claim_id);
|
|
871
|
+
|
|
872
|
+
const bySlot: Record<string, Claim[]> = {};
|
|
873
|
+
for (const claim of claims) {
|
|
874
|
+
const slot = claim.slot || "_unknown";
|
|
875
|
+
if (!bySlot[slot]) bySlot[slot] = [];
|
|
876
|
+
bySlot[slot].push(claim);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (claimIds.length === 0) return { claims, edges: [], by_slot: bySlot };
|
|
880
|
+
|
|
881
|
+
const edgeResult = await this.pool.query(
|
|
882
|
+
`
|
|
883
|
+
SELECT *
|
|
884
|
+
FROM claim_edges
|
|
885
|
+
WHERE project_id = $1
|
|
886
|
+
AND edge_type = 'supersedes'
|
|
887
|
+
AND (from_claim_id = ANY($2::text[]) OR to_claim_id = ANY($2::text[]))
|
|
888
|
+
ORDER BY created_at DESC
|
|
889
|
+
`,
|
|
890
|
+
[args.project_id, claimIds],
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
return { claims, edges: edgeResult.rows, by_slot: bySlot };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async retractClaim(args: {
|
|
897
|
+
project_id: string;
|
|
898
|
+
claim_id: string;
|
|
899
|
+
reason: string;
|
|
900
|
+
}): Promise<{
|
|
901
|
+
success: boolean;
|
|
902
|
+
claim_id: string;
|
|
903
|
+
slot: string;
|
|
904
|
+
previous_claim_id: string | null;
|
|
905
|
+
restored_previous: boolean;
|
|
906
|
+
}> {
|
|
907
|
+
const client = await this.pool.connect();
|
|
908
|
+
try {
|
|
909
|
+
await client.query("BEGIN");
|
|
910
|
+
|
|
911
|
+
const currentResult = await client.query(
|
|
912
|
+
`
|
|
913
|
+
SELECT *
|
|
914
|
+
FROM claims
|
|
915
|
+
WHERE project_id = $1
|
|
916
|
+
AND claim_id = $2
|
|
917
|
+
LIMIT 1
|
|
918
|
+
`,
|
|
919
|
+
[args.project_id, args.claim_id],
|
|
920
|
+
);
|
|
921
|
+
const claim = currentResult.rows[0] as Claim | undefined;
|
|
922
|
+
if (!claim) {
|
|
923
|
+
await client.query("ROLLBACK");
|
|
924
|
+
return {
|
|
925
|
+
success: false,
|
|
926
|
+
claim_id: args.claim_id,
|
|
927
|
+
slot: "",
|
|
928
|
+
previous_claim_id: null,
|
|
929
|
+
restored_previous: false,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
await client.query(
|
|
934
|
+
`
|
|
935
|
+
UPDATE claims
|
|
936
|
+
SET status = 'retracted',
|
|
937
|
+
retracted_at = NOW(),
|
|
938
|
+
retract_reason = $3
|
|
939
|
+
WHERE project_id = $1
|
|
940
|
+
AND claim_id = $2
|
|
941
|
+
`,
|
|
942
|
+
[args.project_id, args.claim_id, args.reason],
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const previousResult = await client.query(
|
|
946
|
+
`
|
|
947
|
+
SELECT claim_id
|
|
948
|
+
FROM claims
|
|
949
|
+
WHERE project_id = $1
|
|
950
|
+
AND subject_id = $2
|
|
951
|
+
AND slot = $3
|
|
952
|
+
AND status = 'active'
|
|
953
|
+
AND claim_id <> $4
|
|
954
|
+
ORDER BY asserted_at DESC
|
|
955
|
+
LIMIT 1
|
|
956
|
+
`,
|
|
957
|
+
[args.project_id, claim.subject_id, claim.slot, args.claim_id],
|
|
958
|
+
);
|
|
959
|
+
const previous = previousResult.rows[0]?.claim_id || null;
|
|
960
|
+
|
|
961
|
+
await client.query(
|
|
962
|
+
`
|
|
963
|
+
INSERT INTO slot_state (
|
|
964
|
+
project_id, subject_id, slot, active_claim_id, status, replaced_by_claim_id
|
|
965
|
+
)
|
|
966
|
+
VALUES (
|
|
967
|
+
$1, $2, $3, $4, $5, $6
|
|
968
|
+
)
|
|
969
|
+
ON CONFLICT (project_id, subject_id, slot)
|
|
970
|
+
DO UPDATE SET
|
|
971
|
+
active_claim_id = EXCLUDED.active_claim_id,
|
|
972
|
+
status = EXCLUDED.status,
|
|
973
|
+
replaced_by_claim_id = EXCLUDED.replaced_by_claim_id,
|
|
974
|
+
updated_at = NOW()
|
|
975
|
+
`,
|
|
976
|
+
[
|
|
977
|
+
args.project_id,
|
|
978
|
+
claim.subject_id,
|
|
979
|
+
claim.slot,
|
|
980
|
+
previous,
|
|
981
|
+
previous ? "active" : "retracted",
|
|
982
|
+
args.claim_id,
|
|
983
|
+
],
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
if (previous) {
|
|
987
|
+
await client.query(
|
|
988
|
+
`
|
|
989
|
+
INSERT INTO claim_edges (
|
|
990
|
+
project_id, subject_id, from_claim_id, to_claim_id, edge_type, weight, reason_code, reason_text
|
|
991
|
+
)
|
|
992
|
+
VALUES ($1, $2, $3, $4, 'retracts', 1, 'manual_retraction', $5)
|
|
993
|
+
ON CONFLICT (project_id, from_claim_id, to_claim_id, edge_type) DO NOTHING
|
|
994
|
+
`,
|
|
995
|
+
[args.project_id, claim.subject_id, args.claim_id, previous, args.reason],
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
await client.query("COMMIT");
|
|
1000
|
+
return {
|
|
1001
|
+
success: true,
|
|
1002
|
+
claim_id: args.claim_id,
|
|
1003
|
+
slot: claim.slot,
|
|
1004
|
+
previous_claim_id: previous,
|
|
1005
|
+
restored_previous: !!previous,
|
|
1006
|
+
};
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
await client.query("ROLLBACK");
|
|
1009
|
+
throw err;
|
|
1010
|
+
} finally {
|
|
1011
|
+
client.release();
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export type { CoreStore, CreateClaimInput, CreateMemoryInput, UpdateMemoryInput };
|