@devflow-tools/memory-engine 0.16.17 → 0.16.19
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/dist/explicit-memory-enricher.d.ts +9 -0
- package/dist/explicit-memory-enricher.d.ts.map +1 -1
- package/dist/explicit-memory-enricher.js +25 -5
- package/dist/explicit-memory-enricher.js.map +1 -1
- package/dist/hybrid-search.d.ts.map +1 -1
- package/dist/hybrid-search.js +20 -1
- package/dist/hybrid-search.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-engine.d.ts.map +1 -1
- package/dist/memory-engine.js +6 -0
- package/dist/memory-engine.js.map +1 -1
- package/dist/memory-gate.d.ts.map +1 -1
- package/dist/memory-gate.js +54 -31
- package/dist/memory-gate.js.map +1 -1
- package/dist/memory-relevance.js +2 -2
- package/dist/memory-store.d.ts +40 -0
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +329 -9
- package/dist/memory-store.js.map +1 -1
- package/dist/migrations/v9-to-v10.d.ts +3 -0
- package/dist/migrations/v9-to-v10.d.ts.map +1 -0
- package/dist/migrations/v9-to-v10.js +45 -0
- package/dist/migrations/v9-to-v10.js.map +1 -0
- package/dist/migrations/v9-to-v10.test.d.ts +2 -0
- package/dist/migrations/v9-to-v10.test.d.ts.map +1 -0
- package/dist/migrations/v9-to-v10.test.js +40 -0
- package/dist/migrations/v9-to-v10.test.js.map +1 -0
- package/dist/semantic-memory.d.ts +38 -0
- package/dist/semantic-memory.d.ts.map +1 -0
- package/dist/semantic-memory.js +205 -0
- package/dist/semantic-memory.js.map +1 -0
- package/dist/semantic-memory.test.d.ts +2 -0
- package/dist/semantic-memory.test.d.ts.map +1 -0
- package/dist/semantic-memory.test.js +39 -0
- package/dist/semantic-memory.test.js.map +1 -0
- package/dist/session-summarizer.d.ts +2 -1
- package/dist/session-summarizer.d.ts.map +1 -1
- package/dist/session-summarizer.js.map +1 -1
- package/package.json +3 -3
package/dist/memory-store.js
CHANGED
|
@@ -9,8 +9,10 @@ import { applyV4ToV5Migration } from './migrations/v4-to-v5.js';
|
|
|
9
9
|
import { applyV6ToV7Migration } from './migrations/v6-to-v7.js';
|
|
10
10
|
import { applyV7ToV8Migration } from './migrations/v7-to-v8.js';
|
|
11
11
|
import { applyV8ToV9Migration } from './migrations/v8-to-v9.js';
|
|
12
|
+
import { applyV9ToV10Migration } from './migrations/v9-to-v10.js';
|
|
12
13
|
import { observationDedupKey, } from './observation-types.js';
|
|
13
14
|
import { analysesConflict, analyzePreference, analyzePreferenceObservation, } from './preference-conflicts.js';
|
|
15
|
+
import { mergeSemanticEntities, normalizeEntityKey, normalizeSemanticMemory, } from './semantic-memory.js';
|
|
14
16
|
// ── Schema ─────────────────────────────────────────────
|
|
15
17
|
const SCHEMA_V2 = `
|
|
16
18
|
-- ============================================================
|
|
@@ -176,7 +178,17 @@ CREATE TABLE IF NOT EXISTS observations (
|
|
|
176
178
|
timestamp INTEGER NOT NULL,
|
|
177
179
|
explicit_accepted_at INTEGER,
|
|
178
180
|
evidence_status TEXT NOT NULL DEFAULT 'unverified',
|
|
179
|
-
evidence_details TEXT NOT NULL DEFAULT '[]'
|
|
181
|
+
evidence_details TEXT NOT NULL DEFAULT '[]',
|
|
182
|
+
semantic_subject TEXT NOT NULL DEFAULT '',
|
|
183
|
+
semantic_predicate TEXT NOT NULL DEFAULT '',
|
|
184
|
+
semantic_value TEXT NOT NULL DEFAULT '',
|
|
185
|
+
semantic_polarity TEXT NOT NULL DEFAULT 'neutral',
|
|
186
|
+
semantic_key TEXT,
|
|
187
|
+
facet_key TEXT,
|
|
188
|
+
entities TEXT NOT NULL DEFAULT '[]',
|
|
189
|
+
temporal TEXT,
|
|
190
|
+
reinforce_count INTEGER NOT NULL DEFAULT 0,
|
|
191
|
+
last_reinforced_at INTEGER
|
|
180
192
|
);
|
|
181
193
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
182
194
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
|
@@ -411,6 +423,7 @@ export class MemoryStore {
|
|
|
411
423
|
applyV6ToV7Migration(this.db);
|
|
412
424
|
applyV7ToV8Migration(this.db);
|
|
413
425
|
applyV8ToV9Migration(this.db);
|
|
426
|
+
applyV9ToV10Migration(this.db);
|
|
414
427
|
this.repairOrphanSessions(rootPath);
|
|
415
428
|
console.log('[MemoryStore] Schema initialized', {
|
|
416
429
|
dbPath: this.dbPath,
|
|
@@ -1232,8 +1245,8 @@ export class MemoryStore {
|
|
|
1232
1245
|
this.ensureSession(obs.sessionId, obs.project ?? this.defaultProjectRoot);
|
|
1233
1246
|
const id = obs.id ?? `obs:${Date.now()}:${randomUUID().slice(0, 8)}`;
|
|
1234
1247
|
const now = obs.timestamp ?? Date.now();
|
|
1235
|
-
const dedupKey = observationDedupKey(obs.type, obs.title);
|
|
1236
|
-
const duplicate = this.findObservationByDedupKey(dedupKey);
|
|
1248
|
+
const dedupKey = obs.semantic?.semanticKey ?? observationDedupKey(obs.type, obs.title);
|
|
1249
|
+
const duplicate = obs.semantic ? null : this.findObservationByDedupKey(dedupKey);
|
|
1237
1250
|
if (duplicate) {
|
|
1238
1251
|
this.linkObservationToEvents(duplicate.id, obs.sourceEventIds ?? []);
|
|
1239
1252
|
this.incrementObservationAccess(duplicate.id, now);
|
|
@@ -1244,8 +1257,10 @@ export class MemoryStore {
|
|
|
1244
1257
|
(id, session_id, project, type, title, narrative, facts, files, tools, concepts,
|
|
1245
1258
|
importance, confidence, source, dedup_key, source_event_ids, timestamp, updated_at,
|
|
1246
1259
|
quality_status, quarantine_reason, quality_details, quarantined_at,
|
|
1247
|
-
failure_category, resolution_state, evidence_status, evidence_details
|
|
1248
|
-
|
|
1260
|
+
failure_category, resolution_state, evidence_status, evidence_details,
|
|
1261
|
+
semantic_subject, semantic_predicate, semantic_value, semantic_polarity,
|
|
1262
|
+
semantic_key, facet_key, entities, temporal, reinforce_count, last_reinforced_at)
|
|
1263
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1249
1264
|
`).run(id, obs.sessionId, obs.project ?? this.defaultProjectRoot, obs.type, obs.title, obs.narrative ?? '', JSON.stringify(obs.facts ?? []), JSON.stringify(obs.files ?? []), JSON.stringify(obs.tools ?? []), JSON.stringify(obs.concepts ?? []), obs.importance ?? 5, obs.confidence ?? 0.5, obs.source ?? 'distill', dedupKey, JSON.stringify(obs.sourceEventIds ?? []), now, now, obs.qualityStatus ?? 'accepted', obs.quarantineReason ?? null, obs.qualityDetails ? JSON.stringify(obs.qualityDetails) : null, obs.qualityStatus === 'quarantined' ? now : null, obs.failureCategory ?? null, obs.resolutionState ?? 'current', obs.evidenceStatus
|
|
1250
1265
|
?? (obs.source === 'explicit_intent'
|
|
1251
1266
|
? 'unverified'
|
|
@@ -1255,7 +1270,7 @@ export class MemoryStore {
|
|
|
1255
1270
|
kind: 'source_event',
|
|
1256
1271
|
source,
|
|
1257
1272
|
summary: 'Observation was accepted from an evidence-bound session event.',
|
|
1258
|
-
}))));
|
|
1273
|
+
}))), obs.semantic?.subject ?? '', obs.semantic?.predicate ?? '', obs.semantic?.value ?? '', obs.semantic?.polarity ?? 'neutral', obs.semantic?.semanticKey ?? null, obs.semantic?.facetKey ?? null, JSON.stringify(obs.semantic?.entities ?? []), obs.semantic?.temporal ? JSON.stringify(obs.semantic.temporal) : null, 0, null);
|
|
1259
1274
|
// Insert concept junction rows
|
|
1260
1275
|
if (obs.concepts && obs.concepts.length > 0) {
|
|
1261
1276
|
const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
|
|
@@ -1263,11 +1278,184 @@ export class MemoryStore {
|
|
|
1263
1278
|
insertConcept.run(id, concept);
|
|
1264
1279
|
}
|
|
1265
1280
|
}
|
|
1281
|
+
this.replaceObservationEntities(id, obs.project ?? this.defaultProjectRoot, obs.semantic?.entities ?? []);
|
|
1266
1282
|
console.log('[MemoryStore] Observation added', {
|
|
1267
1283
|
id, sessionId: obs.sessionId, type: obs.type, conceptCount: obs.concepts?.length ?? 0,
|
|
1268
1284
|
});
|
|
1269
1285
|
return this.getObservation(id);
|
|
1270
1286
|
}
|
|
1287
|
+
acceptSemanticObservation(obs) {
|
|
1288
|
+
return this.db.transaction(() => {
|
|
1289
|
+
const project = obs.project ?? this.defaultProjectRoot;
|
|
1290
|
+
const now = obs.timestamp ?? Date.now();
|
|
1291
|
+
const exact = this.findCurrentObservationBySemanticKey(project, obs.semantic.semanticKey)
|
|
1292
|
+
?? this.findCurrentObservationByNarrative(project, obs.narrative ?? '');
|
|
1293
|
+
if (exact) {
|
|
1294
|
+
const sourceEventIds = uniqueStrings([
|
|
1295
|
+
...parseStringArray(exact.source_event_ids),
|
|
1296
|
+
...(obs.sourceEventIds ?? []),
|
|
1297
|
+
]);
|
|
1298
|
+
const facts = uniqueStrings([...parseStringArray(exact.facts), ...(obs.facts ?? [])]);
|
|
1299
|
+
const concepts = uniqueStrings([
|
|
1300
|
+
...parseStringArray(exact.concepts),
|
|
1301
|
+
...(obs.concepts ?? []),
|
|
1302
|
+
obs.semantic.subject,
|
|
1303
|
+
obs.semantic.predicate,
|
|
1304
|
+
obs.semantic.value,
|
|
1305
|
+
...obs.semantic.entities.map(normalizeEntityKey),
|
|
1306
|
+
]);
|
|
1307
|
+
const entities = mergeSemanticEntities(parseStringArray(exact.entities), obs.semantic.entities);
|
|
1308
|
+
const narrative = semanticRichness(obs.narrative ?? '') > semanticRichness(exact.narrative)
|
|
1309
|
+
? obs.narrative ?? ''
|
|
1310
|
+
: exact.narrative;
|
|
1311
|
+
const title = semanticRichness(obs.title) > semanticRichness(exact.title) ? obs.title : exact.title;
|
|
1312
|
+
const contentChanged = exact.title !== title
|
|
1313
|
+
|| exact.narrative !== narrative;
|
|
1314
|
+
this.db.prepare(`
|
|
1315
|
+
UPDATE observations
|
|
1316
|
+
SET type = ?, title = ?, narrative = ?, facts = ?, concepts = ?, importance = MAX(importance, ?),
|
|
1317
|
+
confidence = MAX(confidence, ?), source_event_ids = ?, updated_at = ?,
|
|
1318
|
+
access_count = access_count + 1, reinforce_count = reinforce_count + 1, last_reinforced_at = ?,
|
|
1319
|
+
source = CASE WHEN ? = 'explicit_intent' THEN 'explicit_intent' ELSE source END,
|
|
1320
|
+
quality_status = CASE WHEN quality_status = 'quarantined' AND ? = 'accepted' THEN 'accepted' ELSE quality_status END,
|
|
1321
|
+
quarantine_reason = CASE WHEN ? = 'accepted' THEN NULL ELSE quarantine_reason END,
|
|
1322
|
+
resolution_state = 'current', superseded_by_observation = NULL,
|
|
1323
|
+
semantic_subject = ?, semantic_predicate = ?, semantic_value = ?, semantic_polarity = ?,
|
|
1324
|
+
semantic_key = ?, facet_key = ?, dedup_key = ?, entities = ?,
|
|
1325
|
+
temporal = COALESCE(?, temporal), evidence_status = ?, evidence_details = ?,
|
|
1326
|
+
embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END
|
|
1327
|
+
WHERE id = ?
|
|
1328
|
+
`).run(obs.type, title, narrative, JSON.stringify(facts), JSON.stringify(concepts), obs.importance ?? 5, obs.confidence ?? 0.5, JSON.stringify(sourceEventIds), now, now, obs.source ?? 'distill', obs.qualityStatus ?? 'accepted', obs.qualityStatus ?? 'accepted', obs.semantic.subject, obs.semantic.predicate, obs.semantic.value, obs.semantic.polarity, obs.semantic.semanticKey, obs.semantic.facetKey, obs.semantic.semanticKey, JSON.stringify(entities), obs.semantic.temporal ? JSON.stringify(obs.semantic.temporal) : null, strongerEvidenceStatus(exact.evidence_status, obs.evidenceStatus), JSON.stringify(mergeEvidenceDetails(exact.evidence_details, obs.evidenceDetails ?? [])), contentChanged ? 1 : 0, exact.id);
|
|
1329
|
+
if (contentChanged) {
|
|
1330
|
+
this.db.prepare("DELETE FROM embeddings WHERE source_type = 'observation' AND source_id = ?").run(exact.id);
|
|
1331
|
+
}
|
|
1332
|
+
this.replaceObservationConcepts(exact.id, concepts);
|
|
1333
|
+
this.replaceObservationEntities(exact.id, project, entities);
|
|
1334
|
+
for (const eventId of obs.sourceEventIds ?? [])
|
|
1335
|
+
this.linkObservationToEvents(exact.id, [eventId]);
|
|
1336
|
+
const supersededIds = this.supersedeSemanticFacetConflicts(project, exact.id, obs.semantic, now);
|
|
1337
|
+
return { row: this.getObservation(exact.id), action: 'REINFORCE', supersededIds, linkedIds: [] };
|
|
1338
|
+
}
|
|
1339
|
+
const row = this.addObservation(obs);
|
|
1340
|
+
if ((obs.qualityStatus ?? 'accepted') === 'quarantined') {
|
|
1341
|
+
return { row, action: 'HOLD', supersededIds: [], linkedIds: [] };
|
|
1342
|
+
}
|
|
1343
|
+
const supersededIds = this.supersedeSemanticFacetConflicts(project, row.id, obs.semantic, now);
|
|
1344
|
+
const relatedIds = this.findCurrentObservationIdsByEntities(project, obs.semantic.entities, new Set([row.id, ...supersededIds])).slice(0, 12);
|
|
1345
|
+
for (const relatedId of relatedIds) {
|
|
1346
|
+
this.addObservationRelation(row.id, relatedId, 'related_to', now);
|
|
1347
|
+
}
|
|
1348
|
+
return {
|
|
1349
|
+
row: this.getObservation(row.id),
|
|
1350
|
+
action: supersededIds.length > 0 ? 'SUPERSEDE' : relatedIds.length > 0 ? 'LINK' : 'ADD',
|
|
1351
|
+
supersededIds,
|
|
1352
|
+
linkedIds: relatedIds,
|
|
1353
|
+
};
|
|
1354
|
+
})();
|
|
1355
|
+
}
|
|
1356
|
+
findCurrentObservationBySemanticKey(project, semanticKey) {
|
|
1357
|
+
return this.db.prepare(`
|
|
1358
|
+
SELECT * FROM observations
|
|
1359
|
+
WHERE project = ? AND semantic_key = ?
|
|
1360
|
+
ORDER BY CASE WHEN resolution_state = 'current' THEN 0 ELSE 1 END,
|
|
1361
|
+
COALESCE(explicit_accepted_at, timestamp) DESC, id DESC
|
|
1362
|
+
LIMIT 1
|
|
1363
|
+
`).get(project, semanticKey) ?? null;
|
|
1364
|
+
}
|
|
1365
|
+
findCurrentObservationByNarrative(project, narrative) {
|
|
1366
|
+
if (!narrative.trim())
|
|
1367
|
+
return null;
|
|
1368
|
+
return this.db.prepare(`
|
|
1369
|
+
SELECT * FROM observations
|
|
1370
|
+
WHERE project = ? AND narrative = ? AND resolution_state = 'current'
|
|
1371
|
+
AND quality_status = 'accepted'
|
|
1372
|
+
ORDER BY COALESCE(explicit_accepted_at, timestamp) DESC, id DESC
|
|
1373
|
+
LIMIT 1
|
|
1374
|
+
`).get(project, narrative) ?? null;
|
|
1375
|
+
}
|
|
1376
|
+
findCurrentObservationsByFacetKey(project, facetKey) {
|
|
1377
|
+
return this.db.prepare(`
|
|
1378
|
+
SELECT * FROM observations
|
|
1379
|
+
WHERE project = ? AND facet_key = ? AND resolution_state = 'current'
|
|
1380
|
+
AND quality_status = 'accepted'
|
|
1381
|
+
ORDER BY COALESCE(explicit_accepted_at, timestamp) DESC, id DESC
|
|
1382
|
+
`).all(project, facetKey);
|
|
1383
|
+
}
|
|
1384
|
+
searchObservationEntities(project, entities, limit = 30) {
|
|
1385
|
+
const normalized = uniqueStrings(entities.map(normalizeEntityKey)).filter(Boolean);
|
|
1386
|
+
if (normalized.length === 0)
|
|
1387
|
+
return [];
|
|
1388
|
+
const placeholders = normalized.map(() => '?').join(', ');
|
|
1389
|
+
return this.db.prepare(`
|
|
1390
|
+
SELECT o.*, COUNT(DISTINCT oe.normalized_entity) AS entity_match_count
|
|
1391
|
+
FROM observation_entities oe
|
|
1392
|
+
JOIN observations o ON o.id = oe.observation_id
|
|
1393
|
+
WHERE oe.project = ? AND oe.normalized_entity IN (${placeholders})
|
|
1394
|
+
AND o.quality_status = 'accepted' AND o.resolution_state = 'current'
|
|
1395
|
+
GROUP BY o.id
|
|
1396
|
+
ORDER BY entity_match_count DESC, COALESCE(o.explicit_accepted_at, o.timestamp) DESC
|
|
1397
|
+
LIMIT ?
|
|
1398
|
+
`).all(project, ...normalized, limit);
|
|
1399
|
+
}
|
|
1400
|
+
searchDefaultProjectObservationEntities(entities, limit = 30) {
|
|
1401
|
+
return this.searchObservationEntities(this.defaultProjectRoot, entities, limit);
|
|
1402
|
+
}
|
|
1403
|
+
listObservationRelations(observationId) {
|
|
1404
|
+
return this.db.prepare(`
|
|
1405
|
+
SELECT * FROM observation_relations
|
|
1406
|
+
WHERE source_observation_id = ? OR target_observation_id = ?
|
|
1407
|
+
ORDER BY created_at ASC, source_observation_id ASC, target_observation_id ASC
|
|
1408
|
+
`).all(observationId, observationId);
|
|
1409
|
+
}
|
|
1410
|
+
replaceObservationConcepts(observationId, concepts) {
|
|
1411
|
+
this.db.prepare('DELETE FROM observation_concepts WHERE observation_id = ?').run(observationId);
|
|
1412
|
+
const insert = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
|
|
1413
|
+
for (const concept of uniqueStrings(concepts))
|
|
1414
|
+
insert.run(observationId, concept);
|
|
1415
|
+
}
|
|
1416
|
+
replaceObservationEntities(observationId, project, entities) {
|
|
1417
|
+
this.db.prepare('DELETE FROM observation_entities WHERE observation_id = ?').run(observationId);
|
|
1418
|
+
const insert = this.db.prepare(`
|
|
1419
|
+
INSERT OR IGNORE INTO observation_entities
|
|
1420
|
+
(observation_id, project, normalized_entity, display_entity)
|
|
1421
|
+
VALUES (?, ?, ?, ?)
|
|
1422
|
+
`);
|
|
1423
|
+
for (const entity of mergeSemanticEntities(entities)) {
|
|
1424
|
+
const normalized = normalizeEntityKey(entity);
|
|
1425
|
+
if (normalized)
|
|
1426
|
+
insert.run(observationId, project, normalized, entity);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
findCurrentObservationIdsByEntities(project, entities, excludedIds) {
|
|
1430
|
+
return this.searchObservationEntities(project, entities, 50)
|
|
1431
|
+
.map(observation => observation.id)
|
|
1432
|
+
.filter(id => !excludedIds.has(id));
|
|
1433
|
+
}
|
|
1434
|
+
addObservationRelation(sourceObservationId, targetObservationId, type, createdAt = Date.now()) {
|
|
1435
|
+
if (sourceObservationId === targetObservationId)
|
|
1436
|
+
return;
|
|
1437
|
+
this.db.prepare(`
|
|
1438
|
+
INSERT OR IGNORE INTO observation_relations
|
|
1439
|
+
(source_observation_id, target_observation_id, type, created_at)
|
|
1440
|
+
VALUES (?, ?, ?, ?)
|
|
1441
|
+
`).run(sourceObservationId, targetObservationId, type, createdAt);
|
|
1442
|
+
}
|
|
1443
|
+
supersedeSemanticFacetConflicts(project, winnerId, semantic, now) {
|
|
1444
|
+
const conflicts = this.findCurrentObservationsByFacetKey(project, semantic.facetKey)
|
|
1445
|
+
.filter(candidate => candidate.id !== winnerId)
|
|
1446
|
+
.filter(candidate => semanticObservationsConflict(candidate, semantic));
|
|
1447
|
+
const supersededIds = [];
|
|
1448
|
+
for (const conflict of conflicts) {
|
|
1449
|
+
this.db.prepare(`
|
|
1450
|
+
UPDATE observations
|
|
1451
|
+
SET resolution_state = 'superseded', superseded_by_observation = ?, dedup_key = NULL, updated_at = ?
|
|
1452
|
+
WHERE id = ? AND resolution_state = 'current'
|
|
1453
|
+
`).run(winnerId, now, conflict.id);
|
|
1454
|
+
this.addObservationRelation(winnerId, conflict.id, 'supersedes', now);
|
|
1455
|
+
supersededIds.push(conflict.id);
|
|
1456
|
+
}
|
|
1457
|
+
return supersededIds;
|
|
1458
|
+
}
|
|
1271
1459
|
addObservationWithEmbedding(obs, embedding) {
|
|
1272
1460
|
return this.db.transaction(() => {
|
|
1273
1461
|
const row = this.addObservation(obs);
|
|
@@ -1276,6 +1464,67 @@ export class MemoryStore {
|
|
|
1276
1464
|
})();
|
|
1277
1465
|
}
|
|
1278
1466
|
acceptExplicitPreference(input) {
|
|
1467
|
+
if (input.enrichment) {
|
|
1468
|
+
this.ensureSession(input.sessionId, input.project);
|
|
1469
|
+
const preferenceAnalysis = analyzePreference(input.content);
|
|
1470
|
+
const hold = preferenceAnalysis.status === 'ambiguous';
|
|
1471
|
+
const baseSemantic = semanticFromExplicitEnrichment(input.enrichment);
|
|
1472
|
+
const semantic = hold
|
|
1473
|
+
? normalizeSemanticMemory({
|
|
1474
|
+
project: input.project,
|
|
1475
|
+
type: input.enrichment.type,
|
|
1476
|
+
title: input.enrichment.title,
|
|
1477
|
+
narrative: input.enrichment.narrative,
|
|
1478
|
+
subject: baseSemantic.subject,
|
|
1479
|
+
predicate: baseSemantic.predicate,
|
|
1480
|
+
value: input.content,
|
|
1481
|
+
polarity: 'neutral',
|
|
1482
|
+
entities: baseSemantic.entities,
|
|
1483
|
+
temporal: baseSemantic.temporal ?? undefined,
|
|
1484
|
+
})
|
|
1485
|
+
: baseSemantic;
|
|
1486
|
+
const result = this.acceptSemanticObservation({
|
|
1487
|
+
sessionId: input.sessionId,
|
|
1488
|
+
project: input.project,
|
|
1489
|
+
type: input.enrichment.type,
|
|
1490
|
+
title: input.enrichment.title,
|
|
1491
|
+
narrative: input.enrichment.narrative,
|
|
1492
|
+
facts: input.enrichment.facts,
|
|
1493
|
+
files: input.enrichment.files,
|
|
1494
|
+
tools: input.enrichment.tools,
|
|
1495
|
+
concepts: input.enrichment.concepts,
|
|
1496
|
+
importance: 10,
|
|
1497
|
+
confidence: 0.95,
|
|
1498
|
+
source: 'explicit_intent',
|
|
1499
|
+
sourceEventIds: input.sourceEventId ? [input.sourceEventId] : [],
|
|
1500
|
+
qualityStatus: hold ? 'quarantined' : 'accepted',
|
|
1501
|
+
quarantineReason: hold ? 'ambiguous_explicit_preference' : undefined,
|
|
1502
|
+
evidenceStatus: 'verified',
|
|
1503
|
+
evidenceDetails: [
|
|
1504
|
+
...input.enrichment.evidence.map(item => ({ ...item })),
|
|
1505
|
+
...(input.enrichment.evidence.length === 0 ? [{
|
|
1506
|
+
kind: 'source_event',
|
|
1507
|
+
source: input.sourceEventId ?? `explicit:${input.sessionId}`,
|
|
1508
|
+
summary: 'Observation was accepted from a direct explicit-memory request.',
|
|
1509
|
+
}] : []),
|
|
1510
|
+
],
|
|
1511
|
+
semantic,
|
|
1512
|
+
});
|
|
1513
|
+
const latest = this.db.prepare(`
|
|
1514
|
+
SELECT MAX(COALESCE(explicit_accepted_at, timestamp)) AS latest
|
|
1515
|
+
FROM observations
|
|
1516
|
+
WHERE project = ? AND source = 'explicit_intent'
|
|
1517
|
+
`).get(input.project);
|
|
1518
|
+
const acceptedAt = Math.max(Date.now(), (latest.latest ?? 0) + 1);
|
|
1519
|
+
this.db.prepare(`
|
|
1520
|
+
UPDATE observations
|
|
1521
|
+
SET explicit_accepted_at = MAX(COALESCE(explicit_accepted_at, 0), ?), updated_at = MAX(updated_at, ?)
|
|
1522
|
+
WHERE id = ?
|
|
1523
|
+
`).run(acceptedAt, acceptedAt, result.row.id);
|
|
1524
|
+
if (input.sourceEventId)
|
|
1525
|
+
this.markEventsProcessed([input.sourceEventId]);
|
|
1526
|
+
return this.getObservation(result.row.id);
|
|
1527
|
+
}
|
|
1279
1528
|
return this.db.transaction(() => {
|
|
1280
1529
|
this.ensureSession(input.sessionId, input.project);
|
|
1281
1530
|
const latest = this.db.prepare(`
|
|
@@ -1341,7 +1590,7 @@ export class MemoryStore {
|
|
|
1341
1590
|
if (current.source !== 'explicit_intent') {
|
|
1342
1591
|
throw new Error(`Observation ${observationId} is not an explicit memory`);
|
|
1343
1592
|
}
|
|
1344
|
-
const dedupKey =
|
|
1593
|
+
const dedupKey = enrichment.semanticKey;
|
|
1345
1594
|
const collisions = this.db.prepare(`
|
|
1346
1595
|
SELECT id, timestamp, explicit_accepted_at FROM observations
|
|
1347
1596
|
WHERE project = ? AND dedup_key = ? AND id <> ?
|
|
@@ -1390,9 +1639,11 @@ export class MemoryStore {
|
|
|
1390
1639
|
UPDATE observations
|
|
1391
1640
|
SET type = ?, title = ?, narrative = ?, facts = ?, files = ?, tools = ?, concepts = ?,
|
|
1392
1641
|
dedup_key = ?, evidence_status = ?, evidence_details = ?, updated_at = ?,
|
|
1393
|
-
embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END
|
|
1642
|
+
embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END,
|
|
1643
|
+
semantic_subject = ?, semantic_predicate = ?, semantic_value = ?, semantic_polarity = ?,
|
|
1644
|
+
semantic_key = ?, facet_key = ?, entities = ?, temporal = ?
|
|
1394
1645
|
WHERE id = ?
|
|
1395
|
-
`).run(enrichment.type, enrichment.title, enrichment.narrative, nextFacts, nextFiles, nextTools, nextConcepts, dedupKeyForCurrent, enrichment.evidenceStatus, JSON.stringify(enrichment.evidence), now, semanticContentChanged ? 1 : 0, observationId);
|
|
1646
|
+
`).run(enrichment.type, enrichment.title, enrichment.narrative, nextFacts, nextFiles, nextTools, nextConcepts, dedupKeyForCurrent, enrichment.evidenceStatus, JSON.stringify(enrichment.evidence), now, semanticContentChanged ? 1 : 0, enrichment.subject, enrichment.predicate, enrichment.value, enrichment.polarity, enrichment.semanticKey, enrichment.facetKey, JSON.stringify(enrichment.entities), enrichment.temporal ? JSON.stringify(enrichment.temporal) : null, observationId);
|
|
1396
1647
|
if (semanticContentChanged) {
|
|
1397
1648
|
this.db.prepare("DELETE FROM embeddings WHERE source_type = 'observation' AND source_id = ?").run(observationId);
|
|
1398
1649
|
}
|
|
@@ -1400,6 +1651,7 @@ export class MemoryStore {
|
|
|
1400
1651
|
const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
|
|
1401
1652
|
for (const concept of enrichment.concepts)
|
|
1402
1653
|
insertConcept.run(observationId, concept);
|
|
1654
|
+
this.replaceObservationEntities(observationId, current.project, enrichment.entities);
|
|
1403
1655
|
this.reconcileExplicitPreferenceConflicts(current.project);
|
|
1404
1656
|
return this.getObservation(observationId);
|
|
1405
1657
|
})();
|
|
@@ -1971,6 +2223,74 @@ export class MemoryStore {
|
|
|
1971
2223
|
}
|
|
1972
2224
|
}
|
|
1973
2225
|
// ── Helpers ────────────────────────────────────────────
|
|
2226
|
+
function uniqueStrings(values) {
|
|
2227
|
+
const result = [];
|
|
2228
|
+
const seen = new Set();
|
|
2229
|
+
for (const value of values) {
|
|
2230
|
+
const normalized = value.normalize('NFKC').trim();
|
|
2231
|
+
const key = normalized.toLocaleLowerCase('en-US');
|
|
2232
|
+
if (!normalized || seen.has(key))
|
|
2233
|
+
continue;
|
|
2234
|
+
seen.add(key);
|
|
2235
|
+
result.push(normalized);
|
|
2236
|
+
}
|
|
2237
|
+
return result;
|
|
2238
|
+
}
|
|
2239
|
+
function semanticRichness(value) {
|
|
2240
|
+
const exactIdentifiers = value.match(/@[a-z0-9._-]+\/[a-z0-9._-]+|--[a-z0-9-]+|[a-z0-9]+(?:[-./][a-z0-9]+)+/giu)?.length ?? 0;
|
|
2241
|
+
return [...value.normalize('NFKC').trim()].length + exactIdentifiers * 40;
|
|
2242
|
+
}
|
|
2243
|
+
function semanticObservationsConflict(existing, incoming) {
|
|
2244
|
+
if (existing.semantic_key === incoming.semanticKey)
|
|
2245
|
+
return false;
|
|
2246
|
+
if (existing.semantic_polarity === 'neutral' || incoming.polarity === 'neutral')
|
|
2247
|
+
return false;
|
|
2248
|
+
const existingValue = normalizeEntityKey(existing.semantic_value);
|
|
2249
|
+
const incomingValue = normalizeEntityKey(incoming.value);
|
|
2250
|
+
if (existingValue === incomingValue)
|
|
2251
|
+
return existing.semantic_polarity !== incoming.polarity;
|
|
2252
|
+
const subject = normalizeEntityKey(incoming.subject);
|
|
2253
|
+
if (new Set(['package manager', 'test runner', 'build tool']).has(subject))
|
|
2254
|
+
return true;
|
|
2255
|
+
if (incoming.predicate === 'version')
|
|
2256
|
+
return true;
|
|
2257
|
+
return ['use', 'prefer', 'require'].includes(incoming.predicate);
|
|
2258
|
+
}
|
|
2259
|
+
function strongerEvidenceStatus(existing, incoming) {
|
|
2260
|
+
return existing === 'verified' || incoming === 'verified' ? 'verified' : 'unverified';
|
|
2261
|
+
}
|
|
2262
|
+
function mergeEvidenceDetails(existing, incoming) {
|
|
2263
|
+
let parsed = [];
|
|
2264
|
+
try {
|
|
2265
|
+
const value = JSON.parse(existing);
|
|
2266
|
+
if (Array.isArray(value)) {
|
|
2267
|
+
parsed = value.filter(item => item && typeof item === 'object' && !Array.isArray(item));
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
catch { /* malformed historical evidence is replaced by valid evidence */ }
|
|
2271
|
+
const result = [];
|
|
2272
|
+
const seen = new Set();
|
|
2273
|
+
for (const item of [...parsed, ...incoming]) {
|
|
2274
|
+
const key = JSON.stringify(item);
|
|
2275
|
+
if (seen.has(key))
|
|
2276
|
+
continue;
|
|
2277
|
+
seen.add(key);
|
|
2278
|
+
result.push(item);
|
|
2279
|
+
}
|
|
2280
|
+
return result.slice(0, 64);
|
|
2281
|
+
}
|
|
2282
|
+
function semanticFromExplicitEnrichment(enrichment) {
|
|
2283
|
+
return {
|
|
2284
|
+
subject: enrichment.subject,
|
|
2285
|
+
predicate: enrichment.predicate,
|
|
2286
|
+
value: enrichment.value,
|
|
2287
|
+
polarity: enrichment.polarity,
|
|
2288
|
+
entities: enrichment.entities,
|
|
2289
|
+
temporal: enrichment.temporal,
|
|
2290
|
+
semanticKey: enrichment.semanticKey,
|
|
2291
|
+
facetKey: enrichment.facetKey,
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
1974
2294
|
function camelToSnake(str) {
|
|
1975
2295
|
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
1976
2296
|
}
|