@jungjaehoon/mama-core 4.1.1 → 5.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/README.md +10 -10
- package/db/migrations/099-commitment-revision-graph.sql +3 -0
- package/dist/api/catalog.js +4 -7
- package/dist/db-adapter/node-sqlite-adapter.d.ts +5 -1
- package/dist/db-adapter/node-sqlite-adapter.js +27 -24
- package/dist/db-manager.d.ts +2 -13
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -6
- package/dist/knowledge/access.js +1 -1
- package/dist/knowledge/commitments.d.ts +2 -0
- package/dist/knowledge/commitments.js +1 -0
- package/dist/knowledge/graph-query.d.ts +10 -1
- package/dist/knowledge/graph-query.js +118 -25
- package/dist/knowledge/index.d.ts +12 -4
- package/dist/knowledge/index.js +10 -10
- package/dist/knowledge/judgments.d.ts +3 -1
- package/dist/knowledge/judgments.js +46 -26
- package/dist/knowledge/links.d.ts +50 -0
- package/dist/knowledge/links.js +152 -0
- package/dist/knowledge/search.d.ts +6 -1
- package/dist/knowledge/search.js +17 -11
- package/dist/mama-api.d.ts +22 -7
- package/dist/mama-api.js +64 -15
- package/dist/memory/api.d.ts +17 -12
- package/dist/memory/api.js +436 -414
- package/dist/memory/decision-links.d.ts +54 -0
- package/dist/memory/decision-links.js +195 -0
- package/dist/memory/graph-read.d.ts +0 -16
- package/dist/memory/graph-read.js +10 -51
- package/dist/memory/judgment-types.d.ts +1 -20
- package/dist/memory/provenance-live.js +1 -1
- package/dist/memory/types.d.ts +14 -1
- package/dist/memory/types.js +1 -7
- package/dist/memory/write-adapters.d.ts +1 -39
- package/dist/memory/write-adapters.js +0 -98
- package/package.json +1 -1
- package/dist/knowledge/commitment-revision-migration.d.ts +0 -4
- package/dist/knowledge/commitment-revision-migration.js +0 -50
- package/dist/knowledge/decision-edges.d.ts +0 -70
- package/dist/knowledge/decision-edges.js +0 -123
- package/dist/memory/evolution-engine.d.ts +0 -22
- package/dist/memory/evolution-engine.js +0 -133
package/dist/memory/api.js
CHANGED
|
@@ -14,12 +14,10 @@ exports.saveMemory = saveMemory;
|
|
|
14
14
|
exports.saveJudgmentRecord = saveJudgmentRecord;
|
|
15
15
|
exports.boundReadScopesFor = boundReadScopesFor;
|
|
16
16
|
exports.saveLegacyMemory = saveLegacyMemory;
|
|
17
|
-
exports.promoteMemoryStatus = promoteMemoryStatus;
|
|
18
17
|
exports.retireMemoryRecord = retireMemoryRecord;
|
|
19
18
|
exports.buildProfile = buildProfile;
|
|
20
19
|
exports.recallMemory = recallMemory;
|
|
21
20
|
exports.ingestMemory = ingestMemory;
|
|
22
|
-
exports.evolveMemory = evolveMemory;
|
|
23
21
|
exports.buildMemoryBootstrap = buildMemoryBootstrap;
|
|
24
22
|
exports.createAuditAck = createAuditAck;
|
|
25
23
|
exports.recordMemoryAudit = recordMemoryAudit;
|
|
@@ -39,6 +37,7 @@ const write_adapters_js_1 = require("./write-adapters.js");
|
|
|
39
37
|
const decision_formatter_js_1 = require("../decision-formatter.js");
|
|
40
38
|
const debug_logger_js_1 = require("../debug-logger.js");
|
|
41
39
|
const graph_query_js_1 = require("../knowledge/graph-query.js");
|
|
40
|
+
const decision_links_js_1 = require("./decision-links.js");
|
|
42
41
|
const case_search_rollup_js_1 = require("../knowledge/case-search-rollup.js");
|
|
43
42
|
const ranker_rescore_js_1 = require("../knowledge/ranker-rescore.js");
|
|
44
43
|
const ranker_features_js_1 = require("../knowledge/ranker-features.js");
|
|
@@ -48,7 +47,6 @@ const index_js_1 = require("../knowledge/index.js");
|
|
|
48
47
|
const write_adapters_js_2 = require("./write-adapters.js");
|
|
49
48
|
const profile_builder_js_1 = require("./profile-builder.js");
|
|
50
49
|
const bootstrap_builder_js_1 = require("./bootstrap-builder.js");
|
|
51
|
-
const evolution_engine_js_1 = require("./evolution-engine.js");
|
|
52
50
|
const channel_summary_state_store_js_1 = require("./channel-summary-state-store.js");
|
|
53
51
|
const debug_logger_js_2 = require("../debug-logger.js");
|
|
54
52
|
const secret_filter_js_1 = require("./secret-filter.js");
|
|
@@ -200,6 +198,24 @@ async function readMemoryRecordById(adapter, memoryId, scopes) {
|
|
|
200
198
|
const recordScopes = batchLoadScopes(adapter, [id]).get(id) ?? [];
|
|
201
199
|
return toMemoryRecord(row, recordScopes, { package: 'mama-core', source_type: 'db' });
|
|
202
200
|
}
|
|
201
|
+
/** The records that state these corrections, down their chains. */
|
|
202
|
+
function correctionAuthors(corrections) {
|
|
203
|
+
return corrections.flatMap((correction) => [
|
|
204
|
+
correction.from,
|
|
205
|
+
...correctionAuthors(correction.correctedBy ?? []),
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
/** The corrections a reader may see; a hidden correction takes the corrections of it along. */
|
|
209
|
+
function readableCorrections(corrections, readable) {
|
|
210
|
+
const kept = corrections.filter(readable).map((correction) => {
|
|
211
|
+
const further = correction.correctedBy
|
|
212
|
+
? readableCorrections(correction.correctedBy, readable)
|
|
213
|
+
: undefined;
|
|
214
|
+
const { correctedBy: _drop, ...rest } = correction;
|
|
215
|
+
return further ? { ...rest, correctedBy: further } : rest;
|
|
216
|
+
});
|
|
217
|
+
return kept.length > 0 ? kept : undefined;
|
|
218
|
+
}
|
|
203
219
|
function batchLoadScopes(adapter, memoryIds) {
|
|
204
220
|
const scopeMap = new Map();
|
|
205
221
|
if (memoryIds.length === 0)
|
|
@@ -467,7 +483,7 @@ async function loadEdgesForIds(adapter, ids) {
|
|
|
467
483
|
const placeholders = ids.map(() => '?').join(', ');
|
|
468
484
|
const rows = adapter
|
|
469
485
|
.prepare(`SELECT from_id, to_id, relationship AS type, reason
|
|
470
|
-
FROM
|
|
486
|
+
FROM ${graph_query_js_1.STATED_DECISION_EDGES} e
|
|
471
487
|
WHERE (from_id IN (${placeholders}) OR to_id IN (${placeholders}))
|
|
472
488
|
AND (approved_by_user != 0 OR approved_by_user IS NULL)`)
|
|
473
489
|
.all(...ids, ...ids);
|
|
@@ -514,7 +530,7 @@ function guidancePayloadFor(input) {
|
|
|
514
530
|
}
|
|
515
531
|
return { guidance };
|
|
516
532
|
}
|
|
517
|
-
async function saveMemoryInternal(adapter, input, provenance, access,
|
|
533
|
+
async function saveMemoryInternal(adapter, input, provenance, access, legacy, commandIdOverride) {
|
|
518
534
|
const targetStatus = input.status ?? 'active';
|
|
519
535
|
const requestedScopes = input.scopes ?? [];
|
|
520
536
|
const payload = guidancePayloadFor(input);
|
|
@@ -528,23 +544,10 @@ async function saveMemoryInternal(adapter, input, provenance, access, trustedEnv
|
|
|
528
544
|
});
|
|
529
545
|
const commandId = commandIdOverride ?? `save:${buildDecisionId(input.topic)}`;
|
|
530
546
|
const recordId = (0, index_js_1.judgmentRecordId)(commandId);
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
`${relationship.type}:${targetId}`,
|
|
536
|
-
{ type: relationship.type, targetId },
|
|
537
|
-
]))).values());
|
|
538
|
-
(0, write_adapters_js_2.assertRelationshipTargetsVisible)(adapter, explicitRelationships.map((relationship) => relationship.targetId), access.scopes, trustedEnvelope);
|
|
539
|
-
const { links, replaces, decisionEdges, supersedeTargets } = (0, write_adapters_js_2.relationshipsToCommandFields)(explicitRelationships, { trusted: trustedEnvelope });
|
|
540
|
-
const authoredLinks = [...links, ...(input.links ?? [])];
|
|
541
|
-
const authoredReplacements = [...replaces, ...(input.replaces ?? [])];
|
|
542
|
-
const allSupersedeTargets = [
|
|
543
|
-
...new Set([
|
|
544
|
-
...supersedeTargets,
|
|
545
|
-
...(input.replaces ?? []).map((replacement) => replacement.id),
|
|
546
|
-
]),
|
|
547
|
-
];
|
|
547
|
+
// A relation is written only when the caller names its target and reason (`links`,
|
|
548
|
+
// `replaces`); matching topic text or vector similarity is evidence for retrieval, not a link.
|
|
549
|
+
const authoredLinks = input.links ?? [];
|
|
550
|
+
const authoredReplacements = input.replaces ?? [];
|
|
548
551
|
const embeddingDecision = {
|
|
549
552
|
id: recordId,
|
|
550
553
|
topic: input.topic,
|
|
@@ -591,8 +594,6 @@ async function saveMemoryInternal(adapter, input, provenance, access, trustedEnv
|
|
|
591
594
|
reason: buildSaveEventReason(provenance.tool_name, provenance.gateway_call_id),
|
|
592
595
|
},
|
|
593
596
|
projections: {
|
|
594
|
-
decisionEdges,
|
|
595
|
-
...(allSupersedeTargets.length > 0 ? { supersedeTargets: allSupersedeTargets } : {}),
|
|
596
597
|
...(input.itemId !== undefined || input.actors !== undefined
|
|
597
598
|
? {
|
|
598
599
|
recordIdentity: { itemId: input.itemId ?? null, actors: input.actors ?? [] },
|
|
@@ -621,8 +622,26 @@ async function saveMemoryInternal(adapter, input, provenance, access, trustedEnv
|
|
|
621
622
|
async function saveMemory(adapter, input) {
|
|
622
623
|
const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)(input);
|
|
623
624
|
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
|
|
624
|
-
const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, clean.scopes ?? []);
|
|
625
|
-
return saveMemoryInternal(adapter, clean, provenance, access
|
|
625
|
+
const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, uniqueScopes([...(clean.scopes ?? []), ...namedTargetScopes(adapter, clean)]));
|
|
626
|
+
return saveMemoryInternal(adapter, clean, provenance, access);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* The scopes of the records a direct save names in `links` and `replaces`. A direct caller has no
|
|
630
|
+
* grant of its own, so naming a record admits its partition for that link, as `mama.link` does;
|
|
631
|
+
* the new record is still bound only to its own scopes.
|
|
632
|
+
*/
|
|
633
|
+
function namedTargetScopes(adapter, input) {
|
|
634
|
+
const ids = [
|
|
635
|
+
...(input.links ?? [])
|
|
636
|
+
.filter((link) => link.target.kind === 'memory')
|
|
637
|
+
.map((link) => link.target.id),
|
|
638
|
+
...(input.replaces ?? []).map((replacement) => replacement.id),
|
|
639
|
+
];
|
|
640
|
+
return ids.flatMap((id) => (0, write_adapters_js_2.boundScopesOf)(adapter, id));
|
|
641
|
+
}
|
|
642
|
+
/** Access scopes must be unique; the save's own scope and a target's are often the same. */
|
|
643
|
+
function uniqueScopes(scopes) {
|
|
644
|
+
return [...new Map(scopes.map((scope) => [`${scope.kind}\0${scope.id}`, scope])).values()];
|
|
626
645
|
}
|
|
627
646
|
/**
|
|
628
647
|
* The unified `memory.save` action path. Access IS the call authority — the
|
|
@@ -650,7 +669,7 @@ async function saveJudgmentRecord(adapter, input, access, commandId, session) {
|
|
|
650
669
|
source_message_ref: session?.sourceMessageRef,
|
|
651
670
|
source_refs: session?.sourceRefs ? [...session.sourceRefs] : undefined,
|
|
652
671
|
});
|
|
653
|
-
return saveMemoryInternal(adapter, clean, provenance, access,
|
|
672
|
+
return saveMemoryInternal(adapter, clean, provenance, access, undefined, commandId);
|
|
654
673
|
}
|
|
655
674
|
/**
|
|
656
675
|
* The read-side scope rule for memory actions — the same bound `appendJudgment`
|
|
@@ -689,173 +708,12 @@ function boundReadScopesFor(access, requested) {
|
|
|
689
708
|
async function saveLegacyMemory(adapter, input, legacy, access) {
|
|
690
709
|
const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)(input);
|
|
691
710
|
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
|
|
692
|
-
const effectiveAccess = access ??
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
async function promoteMemoryStatus(adapter, input) {
|
|
696
|
-
const memoryId = input.memoryId;
|
|
697
|
-
const now = input.nowMs ?? Date.now();
|
|
698
|
-
const targetStatus = input.status;
|
|
699
|
-
const row = adapter
|
|
700
|
-
.prepare(`
|
|
701
|
-
SELECT id, topic, decision, confidence, kind, summary, supersedes
|
|
702
|
-
FROM decisions
|
|
703
|
-
WHERE id = ?
|
|
704
|
-
`)
|
|
705
|
-
.get(memoryId);
|
|
706
|
-
if (!row) {
|
|
707
|
-
throw new Error(`Cannot promote missing memory ${memoryId}`);
|
|
708
|
-
}
|
|
709
|
-
const topic = String(row.topic);
|
|
710
|
-
const summary = String(row.summary ?? row.decision ?? '');
|
|
711
|
-
const kind = (row.kind ?? 'fact') || 'fact';
|
|
712
|
-
const scopes = batchLoadScopes(adapter, [memoryId]).get(memoryId) ?? [];
|
|
713
|
-
let evolution = { edges: [] };
|
|
714
|
-
if (targetStatus === 'active') {
|
|
715
|
-
const primaryScope = scopes[0] ?? null;
|
|
716
|
-
let existingCandidates;
|
|
717
|
-
if (primaryScope) {
|
|
718
|
-
const scopeId = (0, db_manager_js_1.ensureMemoryScope)(adapter, primaryScope.kind, primaryScope.id);
|
|
719
|
-
existingCandidates = adapter
|
|
720
|
-
.prepare(`
|
|
721
|
-
SELECT d.id, d.topic, d.summary, d.kind
|
|
722
|
-
FROM decisions d
|
|
723
|
-
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
724
|
-
WHERE d.topic = ? AND msb.scope_id = ? AND d.id <> ?
|
|
725
|
-
AND (d.status = 'active' OR d.status IS NULL)
|
|
726
|
-
AND d.superseded_by IS NULL
|
|
727
|
-
ORDER BY d.created_at DESC
|
|
728
|
-
LIMIT 5
|
|
729
|
-
`)
|
|
730
|
-
.all(topic, scopeId, memoryId);
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
existingCandidates = adapter
|
|
734
|
-
.prepare(`
|
|
735
|
-
SELECT id, topic, summary, kind
|
|
736
|
-
FROM decisions
|
|
737
|
-
WHERE topic = ? AND id <> ?
|
|
738
|
-
AND (status = 'active' OR status IS NULL)
|
|
739
|
-
AND superseded_by IS NULL
|
|
740
|
-
ORDER BY created_at DESC
|
|
741
|
-
LIMIT 5
|
|
742
|
-
`)
|
|
743
|
-
.all(topic, memoryId);
|
|
744
|
-
}
|
|
745
|
-
if (existingCandidates.length === 0) {
|
|
746
|
-
try {
|
|
747
|
-
const queryText = `${topic} ${summary}`;
|
|
748
|
-
const embedding = await (0, embedder_js_1.generateEmbedding)(queryText, 'query');
|
|
749
|
-
// Same exclusion as saveMemoryInternal's fallback: superseded history must
|
|
750
|
-
// not crowd out the prior ACTIVE decision from the 3 candidate slots.
|
|
751
|
-
const semanticResults = await (0, search_js_1.vectorSearch)(adapter, embedding, 3, 0.82, undefined, Array.from(EXCLUDED_STATUSES));
|
|
752
|
-
let scopeFiltered = semanticResults;
|
|
753
|
-
if (primaryScope) {
|
|
754
|
-
const semIds = semanticResults.map((result) => String(result.id));
|
|
755
|
-
const semScopeMap = batchLoadScopes(adapter, semIds);
|
|
756
|
-
const scopeKey = `${primaryScope.kind}:${primaryScope.id}`;
|
|
757
|
-
scopeFiltered = semanticResults.filter((result) => {
|
|
758
|
-
const resultScopes = semScopeMap.get(String(result.id)) ?? [];
|
|
759
|
-
return (resultScopes.length === 0 ||
|
|
760
|
-
resultScopes.some((scope) => `${scope.kind}:${scope.id}` === scopeKey));
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
existingCandidates = scopeFiltered
|
|
764
|
-
.filter((result) => String(result.id) !== memoryId)
|
|
765
|
-
.filter((result) => {
|
|
766
|
-
const status = String(result.status || '');
|
|
767
|
-
return !status || status === 'active' || status === '';
|
|
768
|
-
})
|
|
769
|
-
.map((result) => ({
|
|
770
|
-
id: String(result.id),
|
|
771
|
-
topic: String(result.topic || ''),
|
|
772
|
-
summary: String(result.decision || ''),
|
|
773
|
-
kind: 'fact',
|
|
774
|
-
_semanticMatch: true,
|
|
775
|
-
}));
|
|
776
|
-
}
|
|
777
|
-
catch {
|
|
778
|
-
// Semantic search unavailable — proceed with exact-match candidates only.
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
evolution = (0, evolution_engine_js_1.resolveMemoryEvolution)({
|
|
782
|
-
incoming: { topic, summary, kind },
|
|
783
|
-
existing: existingCandidates.map((candidate) => ({
|
|
784
|
-
...candidate,
|
|
785
|
-
kind: (candidate.kind || 'fact'),
|
|
786
|
-
})),
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
const existingSupersedesTarget = typeof row.supersedes === 'string' && row.supersedes.length > 0 ? row.supersedes : null;
|
|
790
|
-
const supersedesTarget = evolution.edges.find((edge) => edge.type === 'supersedes')?.to_id ??
|
|
791
|
-
(targetStatus === 'active' ? existingSupersedesTarget : null);
|
|
792
|
-
// The status change is an authored amendment: one append-only judgment record
|
|
793
|
-
// carries it, and the target row's projection columns move in the same
|
|
794
|
-
// transaction. A replayed command (same id, same payload) returns the stored
|
|
795
|
-
// receipt instead of rewriting.
|
|
796
|
-
const commandId = `promote:${memoryId}:${node_crypto_1.default
|
|
797
|
-
.createHash('sha256')
|
|
798
|
-
.update((0, canonicalize_js_1.canonicalizeJSON)({
|
|
799
|
-
status: targetStatus,
|
|
800
|
-
supersedes: supersedesTarget,
|
|
801
|
-
edges: evolution.edges.map((edge) => `${edge.type}:${edge.to_id}`),
|
|
802
|
-
now,
|
|
803
|
-
}))
|
|
804
|
-
.digest('hex')
|
|
805
|
-
.slice(0, 16)}`;
|
|
806
|
-
const supersedesEdges = evolution.edges.filter((edge) => edge.type === 'supersedes');
|
|
807
|
-
const command = {
|
|
808
|
-
commandId,
|
|
809
|
-
// Audit records link to the amended memory instead of sharing its topic,
|
|
810
|
-
// keeping them out of the topic's evolution candidate pool.
|
|
811
|
-
topic: `judgment/${memoryId}`,
|
|
812
|
-
summary: `Status '${targetStatus}' applied to ${memoryId}`,
|
|
813
|
-
recordKind: 'judgment',
|
|
814
|
-
payload: {
|
|
815
|
-
amended: memoryId,
|
|
816
|
-
status: targetStatus,
|
|
817
|
-
supersedes: supersedesTarget,
|
|
818
|
-
edges: evolution.edges.map((edge) => ({ type: edge.type, to_id: edge.to_id })),
|
|
819
|
-
},
|
|
820
|
-
scopes,
|
|
821
|
-
agentId: null,
|
|
822
|
-
links: [{ relation: 'amends', target: { kind: 'memory', id: memoryId } }],
|
|
823
|
-
amends: [
|
|
824
|
-
// supersedes is included only when a target resolved: applyAmendment
|
|
825
|
-
// writes a column for every key present, so a null here would clear the
|
|
826
|
-
// target's predecessor pointer on non-active promotions.
|
|
827
|
-
{
|
|
828
|
-
target: { kind: 'memory', id: memoryId },
|
|
829
|
-
status: targetStatus,
|
|
830
|
-
...(supersedesTarget !== null ? { supersedes: supersedesTarget } : {}),
|
|
831
|
-
},
|
|
832
|
-
...supersedesEdges.map((edge) => ({
|
|
833
|
-
target: { kind: 'memory', id: edge.to_id },
|
|
834
|
-
supersededBy: memoryId,
|
|
835
|
-
status: 'superseded',
|
|
836
|
-
})),
|
|
837
|
-
],
|
|
838
|
-
projections: {
|
|
839
|
-
decisionEdges: evolution.edges.map((edge) => ({
|
|
840
|
-
fromId: memoryId,
|
|
841
|
-
targetId: edge.to_id,
|
|
842
|
-
relationship: edge.type,
|
|
843
|
-
reason: edge.reason ?? null,
|
|
844
|
-
weight: 1,
|
|
845
|
-
})),
|
|
846
|
-
},
|
|
847
|
-
record: {
|
|
848
|
-
kind: 'fact',
|
|
849
|
-
status: 'active',
|
|
850
|
-
summary: `Status '${targetStatus}' applied to ${memoryId}`,
|
|
851
|
-
},
|
|
852
|
-
recordedAt: now,
|
|
853
|
-
event: { reason: `promote ${memoryId} to '${targetStatus}'` },
|
|
854
|
-
};
|
|
855
|
-
await (0, index_js_1.appendJudgment)(command, (0, write_adapters_js_2.unsignedWriteAccess)(scopes), { adapter });
|
|
711
|
+
const effectiveAccess = access ??
|
|
712
|
+
(0, write_adapters_js_2.writeAccessForProvenance)(provenance, uniqueScopes([...(clean.scopes ?? []), ...namedTargetScopes(adapter, clean)]));
|
|
713
|
+
return saveMemoryInternal(adapter, clean, provenance, effectiveAccess, legacy);
|
|
856
714
|
}
|
|
857
715
|
/** Append an access-checked status amendment for one stored memory record. */
|
|
858
|
-
async function retireMemoryRecord(adapter, input, access, commandId) {
|
|
716
|
+
async function retireMemoryRecord(adapter, input, access, commandId, session) {
|
|
859
717
|
const id = input.memoryId.trim();
|
|
860
718
|
const reason = input.reason.trim();
|
|
861
719
|
if (!id)
|
|
@@ -871,11 +729,30 @@ async function retireMemoryRecord(adapter, input, access, commandId) {
|
|
|
871
729
|
}
|
|
872
730
|
const admittedScopes = record.scopes.filter((recordScope) => access.scopes.some((scope) => scope.kind === recordScope.kind && scope.id === recordScope.id));
|
|
873
731
|
const summary = `Status '${input.status}' applied to ${id}: ${reason}`;
|
|
732
|
+
// Who retired it, stated by the host as for a save, so a retirement can be traced to its turn.
|
|
733
|
+
const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)({
|
|
734
|
+
actor: session?.actor ?? 'main_agent',
|
|
735
|
+
agent_id: access.agentId,
|
|
736
|
+
model_run_id: session?.modelRunId,
|
|
737
|
+
envelope_hash: session?.envelopeHash,
|
|
738
|
+
tool_name: session?.toolName,
|
|
739
|
+
gateway_call_id: session?.gatewayCallId,
|
|
740
|
+
context_packet_id: session?.contextPacketId,
|
|
741
|
+
source_turn_id: session?.sourceTurnId,
|
|
742
|
+
source_message_ref: session?.sourceMessageRef,
|
|
743
|
+
source_refs: session?.sourceRefs ? [...session.sourceRefs] : undefined,
|
|
744
|
+
});
|
|
874
745
|
const receipt = await (0, index_js_1.appendJudgment)({
|
|
875
746
|
commandId,
|
|
876
747
|
topic: `judgment/${id}`,
|
|
877
748
|
summary,
|
|
878
749
|
recordKind: 'judgment',
|
|
750
|
+
sourceRefs: provenance.source_refs,
|
|
751
|
+
provenance: provenance.provenance,
|
|
752
|
+
agentId: provenance.agent_id,
|
|
753
|
+
modelRunId: provenance.model_run_id,
|
|
754
|
+
envelopeHash: provenance.envelope_hash,
|
|
755
|
+
gatewayCallId: provenance.gateway_call_id,
|
|
879
756
|
payload: { amended: id, status: input.status, reason },
|
|
880
757
|
scopes: admittedScopes,
|
|
881
758
|
links: [{ relation: 'amends', target: { kind: 'memory', id } }],
|
|
@@ -889,12 +766,15 @@ async function buildProfile(adapter, scopes) {
|
|
|
889
766
|
const records = await loadScopedMemories(adapter, scopes);
|
|
890
767
|
return (0, profile_builder_js_1.classifyProfileEntries)(records);
|
|
891
768
|
}
|
|
892
|
-
const EXCLUDED_STATUSES = new Set(
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
769
|
+
const EXCLUDED_STATUSES = new Set(search_js_1.RECALL_EXCLUDED_STATUSES);
|
|
770
|
+
/** The ids among these that only amend another record (a retirement or an outcome change). */
|
|
771
|
+
function amendmentIds(adapter, ids) {
|
|
772
|
+
const rows = adapter
|
|
773
|
+
.prepare(`SELECT id FROM decisions WHERE id IN (${ids.map(() => '?').join(',')})
|
|
774
|
+
AND json_extract(payload_json, '$.amended') IS NOT NULL`)
|
|
775
|
+
.all(...ids);
|
|
776
|
+
return new Set(rows.map((row) => row.id));
|
|
777
|
+
}
|
|
898
778
|
async function recallMemory(adapter, query, options = {}) {
|
|
899
779
|
const bundle = (0, types_js_1.createEmptyRecallBundle)(query);
|
|
900
780
|
const searchOptions = (0, search_quality_js_1.normalizeSearchQualityOptions)(options);
|
|
@@ -1072,7 +952,7 @@ async function recallMemory(adapter, query, options = {}) {
|
|
|
1072
952
|
.map((t) => stemToken(t))
|
|
1073
953
|
.filter((t) => !FTS5_NOISE_WORDS.has(t));
|
|
1074
954
|
const ftsQuery = ftsTokens.length > 0 ? ftsTokens.join(' OR ') : query;
|
|
1075
|
-
const ftsResults = await (0, search_js_1.fts5Search)(searchAdapter, ftsQuery, lexicalLimit, options.kind);
|
|
955
|
+
const ftsResults = await (0, search_js_1.fts5Search)(searchAdapter, ftsQuery, lexicalLimit, options.kind, options.includeHistory ? undefined : { statuses: [...EXCLUDED_STATUSES], amendments: true });
|
|
1076
956
|
if (ftsResults.length > 0) {
|
|
1077
957
|
const adapter = searchAdapter;
|
|
1078
958
|
const fallbackSource = {
|
|
@@ -1154,6 +1034,10 @@ async function recallMemory(adapter, query, options = {}) {
|
|
|
1154
1034
|
if (options.kind !== undefined) {
|
|
1155
1035
|
lexicalRecords = lexicalRecords.filter((r) => matchesKind(r.kind));
|
|
1156
1036
|
}
|
|
1037
|
+
if (!options.includeHistory && lexicalRecords.length > 0) {
|
|
1038
|
+
const amendments = amendmentIds(adapter, lexicalRecords.map((r) => r.id));
|
|
1039
|
+
lexicalRecords = lexicalRecords.filter((r) => !amendments.has(r.id));
|
|
1040
|
+
}
|
|
1157
1041
|
lexicalCandidates = buildLexicalCandidates(lexicalRecords, query);
|
|
1158
1042
|
if (subQueries.length > 1) {
|
|
1159
1043
|
for (const sq of subQueries.slice(1)) {
|
|
@@ -1329,6 +1213,15 @@ async function recallMemory(adapter, query, options = {}) {
|
|
|
1329
1213
|
];
|
|
1330
1214
|
});
|
|
1331
1215
|
fusedHits = fusedHits.filter((hit) => hit.source_type !== 'decision' || acceptedPrimaryIds.has(hit.source_id));
|
|
1216
|
+
// A retirement or an outcome change audits another record; it is not a belief. Default recall
|
|
1217
|
+
// leaves it out and history shows it. Only the host writers put `amended` in a payload.
|
|
1218
|
+
if (!options.includeHistory && matched.length > 0) {
|
|
1219
|
+
const amendments = amendmentIds(adapter, matched.map((record) => record.id));
|
|
1220
|
+
if (amendments.size > 0) {
|
|
1221
|
+
matched = matched.filter((record) => !amendments.has(record.id));
|
|
1222
|
+
fusedHits = fusedHits.filter((hit) => hit.source_type !== 'decision' || !amendments.has(hit.source_id));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1332
1225
|
// Honor options.limit on the final memories (matched is RRF-rank-sorted, canonical
|
|
1333
1226
|
// dual-write appends last). Without this cap the full fusion set (hundreds of records)
|
|
1334
1227
|
// flowed into bundle.memories AND the per-record enrichment SQL loops below.
|
|
@@ -1340,14 +1233,20 @@ async function recallMemory(adapter, query, options = {}) {
|
|
|
1340
1233
|
const cappedIds = new Set(matched.map((record) => record.id));
|
|
1341
1234
|
fusedHits = fusedHits.filter((hit) => hit.source_type !== 'decision' || cappedIds.has(hit.source_id));
|
|
1342
1235
|
}
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
// excluded from search. This recovers their key information so it is not lost.
|
|
1236
|
+
// Superseded records are excluded from search, so an active record carries what it replaced:
|
|
1237
|
+
// the reader sees the correction and what it corrected. A predecessor is shown only inside the
|
|
1238
|
+
// reader's scopes, and marked as replaced.
|
|
1347
1239
|
if (matched.length > 0) {
|
|
1348
1240
|
const stmtChain = adapter.prepare(`SELECT id, summary, decision FROM decisions WHERE superseded_by = ?`);
|
|
1241
|
+
const readerScopes = options.scopes && options.scopes.length > 0
|
|
1242
|
+
? new Set(options.scopes.map((scope) => `${scope.kind}:${scope.id}`))
|
|
1243
|
+
: null;
|
|
1349
1244
|
for (const record of matched) {
|
|
1350
|
-
|
|
1245
|
+
let predecessors = stmtChain.all(record.id);
|
|
1246
|
+
if (readerScopes && predecessors.length > 0) {
|
|
1247
|
+
const scopeMap = batchLoadScopes(adapter, predecessors.map((predecessor) => predecessor.id));
|
|
1248
|
+
predecessors = predecessors.filter((predecessor) => (scopeMap.get(predecessor.id) ?? []).some((scope) => readerScopes.has(`${scope.kind}:${scope.id}`)));
|
|
1249
|
+
}
|
|
1351
1250
|
if (predecessors.length > 0) {
|
|
1352
1251
|
const extra = predecessors
|
|
1353
1252
|
.map((p) => String(p.summary ?? p.decision ?? ''))
|
|
@@ -1355,8 +1254,8 @@ async function recallMemory(adapter, query, options = {}) {
|
|
|
1355
1254
|
.join(' | ');
|
|
1356
1255
|
if (extra) {
|
|
1357
1256
|
record.details = record.details
|
|
1358
|
-
? `${record.details}\n[
|
|
1359
|
-
: `[
|
|
1257
|
+
? `${record.details}\n[Replaced by this record] ${extra}`
|
|
1258
|
+
: `[Replaced by this record] ${extra}`;
|
|
1360
1259
|
}
|
|
1361
1260
|
}
|
|
1362
1261
|
}
|
|
@@ -1366,124 +1265,138 @@ async function recallMemory(adapter, query, options = {}) {
|
|
|
1366
1265
|
bundle.graph_context.expanded = [];
|
|
1367
1266
|
bundle.graph_context.edges = [];
|
|
1368
1267
|
if (matched.length > 0 && !options.skipGraphExpansion && searchOptions.includeRelated) {
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
.get(e.id);
|
|
1388
|
-
const status = row?.status || '';
|
|
1389
|
-
return matchesKind(row?.kind) && (!status || !EXCLUDED_STATUSES.has(status));
|
|
1390
|
-
});
|
|
1391
|
-
}
|
|
1392
|
-
else if (options.kind !== undefined) {
|
|
1393
|
-
expandedOnly = expandedOnly.filter((e) => {
|
|
1394
|
-
const row = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1395
|
-
return matchesKind(row?.kind);
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
if (options.scopes && options.scopes.length > 0) {
|
|
1399
|
-
const expandedIds = expandedOnly.map((e) => e.id);
|
|
1400
|
-
expandedScopeMap = batchLoadScopes(adapter, expandedIds);
|
|
1401
|
-
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
1402
|
-
expandedOnly = expandedOnly.filter((e) => {
|
|
1403
|
-
const scopes = expandedScopeMap.get(e.id) ?? [];
|
|
1404
|
-
if (scopes.length === 0)
|
|
1405
|
-
return false;
|
|
1406
|
-
return scopes.some((s) => requestedScopes.has(`${s.kind}:${s.id}`));
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
bundle.graph_context.expanded = expandedOnly.flatMap((e) => {
|
|
1410
|
-
const kindRow = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1411
|
-
const expandedRecord = {
|
|
1412
|
-
id: String(e.id),
|
|
1413
|
-
topic: String(e.topic || ''),
|
|
1414
|
-
kind: (kindRow?.kind ?? 'decision'),
|
|
1415
|
-
summary: String(e.decision || ''),
|
|
1416
|
-
details: '',
|
|
1417
|
-
confidence: e.graph_rank ?? 0.5,
|
|
1418
|
-
status: 'active',
|
|
1419
|
-
scopes: expandedScopeMap.get(e.id) ?? [],
|
|
1420
|
-
source: {
|
|
1421
|
-
package: 'mama-core',
|
|
1422
|
-
source_type: String(e.graph_source || 'graph_expansion'),
|
|
1423
|
-
},
|
|
1424
|
-
created_at: e.created_at ?? Date.now(),
|
|
1425
|
-
updated_at: e.created_at ?? Date.now(),
|
|
1426
|
-
};
|
|
1427
|
-
const expandedDiagnostics = buildDiagnostics(expandedRecord, 'expanded');
|
|
1428
|
-
if (!passesStrictness(expandedDiagnostics)) {
|
|
1429
|
-
diagnostics.candidate_counts.rejected_by_strictness += 1;
|
|
1430
|
-
return [];
|
|
1431
|
-
}
|
|
1432
|
-
return [
|
|
1433
|
-
searchOptions.diagnostics
|
|
1434
|
-
? {
|
|
1435
|
-
...expandedRecord,
|
|
1436
|
-
retrieval_diagnostics: expandedDiagnostics,
|
|
1437
|
-
}
|
|
1438
|
-
: expandedRecord,
|
|
1439
|
-
];
|
|
1268
|
+
const candidates = matched.map((m) => ({
|
|
1269
|
+
id: m.id,
|
|
1270
|
+
topic: m.topic,
|
|
1271
|
+
decision: m.summary,
|
|
1272
|
+
confidence: m.confidence,
|
|
1273
|
+
created_at: m.created_at,
|
|
1274
|
+
similarity: m.confidence ?? 0.5,
|
|
1275
|
+
}));
|
|
1276
|
+
const expanded = await expandWithGraphInAdapter(adapter, candidates);
|
|
1277
|
+
const primaryIds = new Set(matched.map((m) => m.id));
|
|
1278
|
+
let expandedOnly = expanded.filter((e) => !primaryIds.has(e.id));
|
|
1279
|
+
let expandedScopeMap = new Map();
|
|
1280
|
+
// Re-filter expanded results: apply status and scope checks
|
|
1281
|
+
if (!options.includeHistory) {
|
|
1282
|
+
expandedOnly = expandedOnly.filter((e) => {
|
|
1283
|
+
const row = adapter.prepare(`SELECT kind, status FROM decisions WHERE id = ?`).get(e.id);
|
|
1284
|
+
const status = row?.status || '';
|
|
1285
|
+
return matchesKind(row?.kind) && (!status || !EXCLUDED_STATUSES.has(status));
|
|
1440
1286
|
});
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
// so expansion hits displaced every primary hit from the fused top-N
|
|
1446
|
-
// (delta-bench root cause: top-5 filled with related-but-wrong topics
|
|
1447
|
-
// while the queried topic's own rows were cut). Scale them into a band
|
|
1448
|
-
// strictly below the weakest primary hit, ordered by graph rank.
|
|
1449
|
-
const minPrimaryScore = fusedHits.length > 0 ? Math.min(...fusedHits.map((hit) => hit.fused_rank_score)) : 0.002;
|
|
1450
|
-
fusedHits = [
|
|
1451
|
-
...fusedHits,
|
|
1452
|
-
...bundle.graph_context.expanded.map((record) => ({
|
|
1453
|
-
source_type: 'decision',
|
|
1454
|
-
source_id: record.id,
|
|
1455
|
-
record,
|
|
1456
|
-
fused_rank_score: minPrimaryScore * 0.9 * Math.min(1, Math.max(0.1, record.confidence ?? 0.5)),
|
|
1457
|
-
retrieval_diagnostics: record.retrieval_diagnostics,
|
|
1458
|
-
})),
|
|
1459
|
-
].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
|
|
1460
|
-
// bundle.graph_context.expanded is the strictness-filtered set of
|
|
1461
|
-
// expanded nodes that will actually be returned. Use those IDs (not
|
|
1462
|
-
// expandedOnly, which still contains rejected candidates) so edges
|
|
1463
|
-
// never point at nodes that never made it into the graph payload.
|
|
1464
|
-
const acceptedExpandedIds = bundle.graph_context.expanded.map((record) => record.id);
|
|
1465
|
-
const allIds = [...matched.map((m) => m.id), ...acceptedExpandedIds];
|
|
1466
|
-
const allEdges = await loadEdgesForIds(adapter, allIds);
|
|
1467
|
-
// Filter out edges pointing to decisions with excluded statuses
|
|
1468
|
-
const activeIds = new Set(allIds);
|
|
1469
|
-
const edgesToCheck = allEdges.filter((e) => !activeIds.has(e.to_id) || !activeIds.has(e.from_id));
|
|
1470
|
-
if (edgesToCheck.length > 0) {
|
|
1471
|
-
const checkIds = [
|
|
1472
|
-
...new Set(edgesToCheck.flatMap((e) => [e.from_id, e.to_id]).filter((id) => !activeIds.has(id))),
|
|
1473
|
-
];
|
|
1474
|
-
const placeholders = checkIds.map(() => '?').join(', ');
|
|
1475
|
-
const statusRows = adapter
|
|
1476
|
-
.prepare(`SELECT id, status FROM decisions WHERE id IN (${placeholders})`)
|
|
1477
|
-
.all(...checkIds);
|
|
1478
|
-
const excludedIds = new Set(statusRows.filter((r) => r.status && EXCLUDED_STATUSES.has(r.status)).map((r) => r.id));
|
|
1479
|
-
bundle.graph_context.edges = allEdges.filter((e) => !excludedIds.has(e.from_id) && !excludedIds.has(e.to_id));
|
|
1287
|
+
// A link an agent stated can point at a retirement or an outcome change; it stays out too.
|
|
1288
|
+
if (expandedOnly.length > 0) {
|
|
1289
|
+
const amendments = amendmentIds(adapter, expandedOnly.map((e) => e.id));
|
|
1290
|
+
expandedOnly = expandedOnly.filter((e) => !amendments.has(e.id));
|
|
1480
1291
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1292
|
+
}
|
|
1293
|
+
else if (options.kind !== undefined) {
|
|
1294
|
+
expandedOnly = expandedOnly.filter((e) => {
|
|
1295
|
+
const row = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1296
|
+
return matchesKind(row?.kind);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
if (options.scopes && options.scopes.length > 0) {
|
|
1300
|
+
const expandedIds = expandedOnly.map((e) => e.id);
|
|
1301
|
+
expandedScopeMap = batchLoadScopes(adapter, expandedIds);
|
|
1302
|
+
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
1303
|
+
expandedOnly = expandedOnly.filter((e) => {
|
|
1304
|
+
const scopes = expandedScopeMap.get(e.id) ?? [];
|
|
1305
|
+
if (scopes.length === 0)
|
|
1306
|
+
return false;
|
|
1307
|
+
return scopes.some((s) => requestedScopes.has(`${s.kind}:${s.id}`));
|
|
1308
|
+
});
|
|
1309
|
+
// A correction is stated by a record; its reason shows under the rule the records follow.
|
|
1310
|
+
const correctionScopes = batchLoadScopes(adapter, expandedOnly.flatMap((e) => correctionAuthors(e.edge_corrected_by ?? [])));
|
|
1311
|
+
const readable = (correction) => (correctionScopes.get(correction.from) ?? []).some((scope) => requestedScopes.has(`${scope.kind}:${scope.id}`));
|
|
1312
|
+
expandedOnly = expandedOnly.map((e) => e.edge_corrected_by
|
|
1313
|
+
? { ...e, edge_corrected_by: readableCorrections(e.edge_corrected_by, readable) }
|
|
1314
|
+
: e);
|
|
1315
|
+
}
|
|
1316
|
+
bundle.graph_context.expanded = expandedOnly.flatMap((e) => {
|
|
1317
|
+
const kindRow = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1318
|
+
const expandedRecord = {
|
|
1319
|
+
id: String(e.id),
|
|
1320
|
+
topic: String(e.topic || ''),
|
|
1321
|
+
kind: (kindRow?.kind ?? 'decision'),
|
|
1322
|
+
summary: String(e.decision || ''),
|
|
1323
|
+
details: '',
|
|
1324
|
+
confidence: e.graph_rank ?? 0.5,
|
|
1325
|
+
status: 'active',
|
|
1326
|
+
scopes: expandedScopeMap.get(e.id) ?? [],
|
|
1327
|
+
source: {
|
|
1328
|
+
package: 'mama-core',
|
|
1329
|
+
source_type: String(e.graph_source || 'graph_expansion'),
|
|
1330
|
+
},
|
|
1331
|
+
created_at: e.created_at ?? Date.now(),
|
|
1332
|
+
updated_at: e.created_at ?? Date.now(),
|
|
1333
|
+
...(e.related_to
|
|
1334
|
+
? {
|
|
1335
|
+
reached_through: {
|
|
1336
|
+
from: e.related_to,
|
|
1337
|
+
relation: String(e.graph_source),
|
|
1338
|
+
reason: e.edge_reason ?? null,
|
|
1339
|
+
...(e.edge_corrected_by ? { corrected_by: e.edge_corrected_by } : {}),
|
|
1340
|
+
},
|
|
1341
|
+
}
|
|
1342
|
+
: {}),
|
|
1343
|
+
};
|
|
1344
|
+
const expandedDiagnostics = buildDiagnostics(expandedRecord, 'expanded');
|
|
1345
|
+
if (!passesStrictness(expandedDiagnostics)) {
|
|
1346
|
+
diagnostics.candidate_counts.rejected_by_strictness += 1;
|
|
1347
|
+
return [];
|
|
1483
1348
|
}
|
|
1349
|
+
return [
|
|
1350
|
+
searchOptions.diagnostics
|
|
1351
|
+
? {
|
|
1352
|
+
...expandedRecord,
|
|
1353
|
+
retrieval_diagnostics: expandedDiagnostics,
|
|
1354
|
+
}
|
|
1355
|
+
: expandedRecord,
|
|
1356
|
+
];
|
|
1357
|
+
});
|
|
1358
|
+
diagnostics.candidate_counts.graph_expanded = bundle.graph_context.expanded.length;
|
|
1359
|
+
// Graph-expanded hits are supporting context for the primary matches -
|
|
1360
|
+
// they must never OUTRANK them. The previous score ((graph_rank)*0.1,
|
|
1361
|
+
// typically 0.05-0.095) sat far above the entire RRF range (<=~0.018),
|
|
1362
|
+
// so expansion hits displaced every primary hit from the fused top-N
|
|
1363
|
+
// (delta-bench root cause: top-5 filled with related-but-wrong topics
|
|
1364
|
+
// while the queried topic's own rows were cut). Scale them into a band
|
|
1365
|
+
// strictly below the weakest primary hit, ordered by graph rank.
|
|
1366
|
+
const minPrimaryScore = fusedHits.length > 0 ? Math.min(...fusedHits.map((hit) => hit.fused_rank_score)) : 0.002;
|
|
1367
|
+
fusedHits = [
|
|
1368
|
+
...fusedHits,
|
|
1369
|
+
...bundle.graph_context.expanded.map((record) => ({
|
|
1370
|
+
source_type: 'decision',
|
|
1371
|
+
source_id: record.id,
|
|
1372
|
+
record,
|
|
1373
|
+
fused_rank_score: minPrimaryScore * 0.9 * Math.min(1, Math.max(0.1, record.confidence ?? 0.5)),
|
|
1374
|
+
retrieval_diagnostics: record.retrieval_diagnostics,
|
|
1375
|
+
})),
|
|
1376
|
+
].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
|
|
1377
|
+
// bundle.graph_context.expanded is the strictness-filtered set of
|
|
1378
|
+
// expanded nodes that will actually be returned. Use those IDs (not
|
|
1379
|
+
// expandedOnly, which still contains rejected candidates) so edges
|
|
1380
|
+
// never point at nodes that never made it into the graph payload.
|
|
1381
|
+
const acceptedExpandedIds = bundle.graph_context.expanded.map((record) => record.id);
|
|
1382
|
+
const allIds = [...matched.map((m) => m.id), ...acceptedExpandedIds];
|
|
1383
|
+
const allEdges = await loadEdgesForIds(adapter, allIds);
|
|
1384
|
+
// Filter out edges pointing to decisions with excluded statuses
|
|
1385
|
+
const activeIds = new Set(allIds);
|
|
1386
|
+
const edgesToCheck = allEdges.filter((e) => !activeIds.has(e.to_id) || !activeIds.has(e.from_id));
|
|
1387
|
+
if (edgesToCheck.length > 0) {
|
|
1388
|
+
const checkIds = [
|
|
1389
|
+
...new Set(edgesToCheck.flatMap((e) => [e.from_id, e.to_id]).filter((id) => !activeIds.has(id))),
|
|
1390
|
+
];
|
|
1391
|
+
const placeholders = checkIds.map(() => '?').join(', ');
|
|
1392
|
+
const statusRows = adapter
|
|
1393
|
+
.prepare(`SELECT id, status FROM decisions WHERE id IN (${placeholders})`)
|
|
1394
|
+
.all(...checkIds);
|
|
1395
|
+
const excludedIds = new Set(statusRows.filter((r) => r.status && EXCLUDED_STATUSES.has(r.status)).map((r) => r.id));
|
|
1396
|
+
bundle.graph_context.edges = allEdges.filter((e) => !excludedIds.has(e.from_id) && !excludedIds.has(e.to_id));
|
|
1484
1397
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1398
|
+
else {
|
|
1399
|
+
bundle.graph_context.edges = allEdges;
|
|
1487
1400
|
}
|
|
1488
1401
|
}
|
|
1489
1402
|
bundle.fused_hits = fusedHits;
|
|
@@ -1546,9 +1459,6 @@ async function ingestMemoryInternal(adapter, input) {
|
|
|
1546
1459
|
async function ingestMemory(adapter, input) {
|
|
1547
1460
|
return ingestMemoryInternal(adapter, (0, provenance_js_1.sanitizePublicIngestMemoryInput)(input));
|
|
1548
1461
|
}
|
|
1549
|
-
async function evolveMemory(input) {
|
|
1550
|
-
return (0, evolution_engine_js_1.resolveMemoryEvolution)(input);
|
|
1551
|
-
}
|
|
1552
1462
|
async function buildMemoryBootstrap(adapter, params) {
|
|
1553
1463
|
return (0, bootstrap_builder_js_1.buildMemoryAgentBootstrap)(adapter, params);
|
|
1554
1464
|
}
|
|
@@ -1661,6 +1571,7 @@ function confidenceValue(record, fallback) {
|
|
|
1661
1571
|
async function expandWithGraphInAdapter(adapter, candidates) {
|
|
1662
1572
|
const graphEnhanced = new Map(); // Use Map for deduplication by ID
|
|
1663
1573
|
const primaryIds = new Set(candidates.map((c) => c.id)); // Track primary candidates
|
|
1574
|
+
const reachedThrough = new Map(); // expanded record id -> the link's edge id
|
|
1664
1575
|
// Process each candidate
|
|
1665
1576
|
for (const candidate of candidates) {
|
|
1666
1577
|
// Add primary candidate with higher rank
|
|
@@ -1671,97 +1582,96 @@ async function expandWithGraphInAdapter(adapter, candidates) {
|
|
|
1671
1582
|
graph_rank: 1.0, // Highest rank
|
|
1672
1583
|
});
|
|
1673
1584
|
}
|
|
1674
|
-
// 1.
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1585
|
+
// 1. The records this one replaced, down its supersedes chain
|
|
1586
|
+
const chain = await (0, graph_query_js_1.queryDecisionGraph)(adapter, candidate.topic, candidate.id);
|
|
1587
|
+
for (const decision of chain) {
|
|
1588
|
+
if (!graphEnhanced.has(decision.id)) {
|
|
1589
|
+
graphEnhanced.set(decision.id, {
|
|
1590
|
+
...decision,
|
|
1591
|
+
graph_source: 'supersedes_chain',
|
|
1592
|
+
graph_rank: 0.8, // Lower rank than primary
|
|
1593
|
+
similarity: (candidate.similarity ?? 0) * 0.9, // Inherit similarity, slightly reduced
|
|
1594
|
+
related_to: candidate.id, // Track relationship
|
|
1595
|
+
});
|
|
1687
1596
|
}
|
|
1688
1597
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
edge_reason: edge.reason,
|
|
1722
|
-
});
|
|
1723
|
-
}
|
|
1724
|
-
};
|
|
1725
|
-
// Add refines edges
|
|
1726
|
-
for (const edge of edges.refines) {
|
|
1727
|
-
addEdge(edge, 'to_id', 'refines', 0.7, 0.85);
|
|
1728
|
-
}
|
|
1729
|
-
// Add refined_by edges
|
|
1730
|
-
for (const edge of edges.refined_by) {
|
|
1731
|
-
addEdge(edge, 'from_id', 'refined_by', 0.7, 0.85);
|
|
1732
|
-
}
|
|
1733
|
-
// Add contradicts edges (lower rank, but still relevant)
|
|
1734
|
-
for (const edge of edges.contradicts) {
|
|
1735
|
-
addEdge(edge, 'to_id', 'contradicts', 0.6, 0.8);
|
|
1736
|
-
}
|
|
1737
|
-
// Story 2.1: Add builds_on edges (high relevance - extending prior work)
|
|
1738
|
-
for (const edge of edges.builds_on) {
|
|
1739
|
-
addEdge(edge, 'to_id', 'builds_on', 0.75, 0.9);
|
|
1740
|
-
}
|
|
1741
|
-
// Add built_on_by edges (someone built on this decision)
|
|
1742
|
-
for (const edge of edges.built_on_by) {
|
|
1743
|
-
addEdge(edge, 'from_id', 'built_on_by', 0.75, 0.9);
|
|
1744
|
-
}
|
|
1745
|
-
// Add debates edges (alternative view)
|
|
1746
|
-
for (const edge of edges.debates) {
|
|
1747
|
-
addEdge(edge, 'to_id', 'debates', 0.65, 0.85);
|
|
1748
|
-
}
|
|
1749
|
-
// Add debated_by edges
|
|
1750
|
-
for (const edge of edges.debated_by) {
|
|
1751
|
-
addEdge(edge, 'from_id', 'debated_by', 0.65, 0.85);
|
|
1752
|
-
}
|
|
1753
|
-
// Add synthesizes edges (unified approach)
|
|
1754
|
-
for (const edge of edges.synthesizes) {
|
|
1755
|
-
addEdge(edge, 'to_id', 'synthesizes', 0.7, 0.88);
|
|
1756
|
-
}
|
|
1757
|
-
// Add synthesized_by edges
|
|
1758
|
-
for (const edge of edges.synthesized_by) {
|
|
1759
|
-
addEdge(edge, 'from_id', 'synthesized_by', 0.7, 0.88);
|
|
1598
|
+
// 2. The links an agent stated (refines, contradicts, builds_on, debates, synthesizes)
|
|
1599
|
+
const rawEdges = (await (0, graph_query_js_1.querySemanticEdges)(adapter, [candidate.id])) || {};
|
|
1600
|
+
const edges = {
|
|
1601
|
+
refines: rawEdges.refines || [],
|
|
1602
|
+
refined_by: rawEdges.refined_by || [],
|
|
1603
|
+
contradicts: rawEdges.contradicts || [],
|
|
1604
|
+
contradicted_by: rawEdges.contradicted_by || [],
|
|
1605
|
+
builds_on: rawEdges.builds_on || [],
|
|
1606
|
+
built_on_by: rawEdges.built_on_by || [],
|
|
1607
|
+
debates: rawEdges.debates || [],
|
|
1608
|
+
debated_by: rawEdges.debated_by || [],
|
|
1609
|
+
synthesizes: rawEdges.synthesizes || [],
|
|
1610
|
+
synthesized_by: rawEdges.synthesized_by || [],
|
|
1611
|
+
};
|
|
1612
|
+
// Helper to add edge to graph
|
|
1613
|
+
const addEdge = (edge, idField, source, rank, simFactor) => {
|
|
1614
|
+
const id = edge[idField];
|
|
1615
|
+
if (!graphEnhanced.has(id)) {
|
|
1616
|
+
graphEnhanced.set(id, {
|
|
1617
|
+
id: id,
|
|
1618
|
+
topic: edge.topic,
|
|
1619
|
+
decision: edge.decision,
|
|
1620
|
+
confidence: edge.confidence,
|
|
1621
|
+
created_at: edge.created_at,
|
|
1622
|
+
graph_source: source,
|
|
1623
|
+
graph_rank: rank,
|
|
1624
|
+
similarity: (candidate.similarity ?? 0) * simFactor,
|
|
1625
|
+
related_to: candidate.id,
|
|
1626
|
+
edge_reason: edge.reason,
|
|
1627
|
+
});
|
|
1628
|
+
if (edge.edge_id)
|
|
1629
|
+
reachedThrough.set(id, edge.edge_id);
|
|
1760
1630
|
}
|
|
1631
|
+
};
|
|
1632
|
+
// Add refines edges
|
|
1633
|
+
for (const edge of edges.refines) {
|
|
1634
|
+
addEdge(edge, 'to_id', 'refines', 0.7, 0.85);
|
|
1635
|
+
}
|
|
1636
|
+
// Add refined_by edges
|
|
1637
|
+
for (const edge of edges.refined_by) {
|
|
1638
|
+
addEdge(edge, 'from_id', 'refined_by', 0.7, 0.85);
|
|
1639
|
+
}
|
|
1640
|
+
// Add contradicts edges (lower rank, but still relevant)
|
|
1641
|
+
for (const edge of edges.contradicts) {
|
|
1642
|
+
addEdge(edge, 'to_id', 'contradicts', 0.6, 0.8);
|
|
1643
|
+
}
|
|
1644
|
+
// Story 2.1: Add builds_on edges (high relevance - extending prior work)
|
|
1645
|
+
for (const edge of edges.builds_on) {
|
|
1646
|
+
addEdge(edge, 'to_id', 'builds_on', 0.75, 0.9);
|
|
1761
1647
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1648
|
+
// Add built_on_by edges (someone built on this decision)
|
|
1649
|
+
for (const edge of edges.built_on_by) {
|
|
1650
|
+
addEdge(edge, 'from_id', 'built_on_by', 0.75, 0.9);
|
|
1764
1651
|
}
|
|
1652
|
+
// Add debates edges (alternative view)
|
|
1653
|
+
for (const edge of edges.debates) {
|
|
1654
|
+
addEdge(edge, 'to_id', 'debates', 0.65, 0.85);
|
|
1655
|
+
}
|
|
1656
|
+
// Add debated_by edges
|
|
1657
|
+
for (const edge of edges.debated_by) {
|
|
1658
|
+
addEdge(edge, 'from_id', 'debated_by', 0.65, 0.85);
|
|
1659
|
+
}
|
|
1660
|
+
// Add synthesizes edges (unified approach)
|
|
1661
|
+
for (const edge of edges.synthesizes) {
|
|
1662
|
+
addEdge(edge, 'to_id', 'synthesizes', 0.7, 0.88);
|
|
1663
|
+
}
|
|
1664
|
+
// Add synthesized_by edges
|
|
1665
|
+
for (const edge of edges.synthesized_by) {
|
|
1666
|
+
addEdge(edge, 'from_id', 'synthesized_by', 0.7, 0.88);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
// A link the agent later contradicted still leads here; the correction goes with the record.
|
|
1670
|
+
const corrections = (0, decision_links_js_1.correctionsOf)(adapter, [...reachedThrough.values()]);
|
|
1671
|
+
for (const [id, edgeId] of reachedThrough) {
|
|
1672
|
+
const correctedBy = corrections.get(edgeId);
|
|
1673
|
+
if (correctedBy)
|
|
1674
|
+
graphEnhanced.set(id, { ...graphEnhanced.get(id), edge_corrected_by: correctedBy });
|
|
1765
1675
|
}
|
|
1766
1676
|
// 3. Convert Map to Array
|
|
1767
1677
|
const allResults = Array.from(graphEnhanced.values());
|
|
@@ -1831,8 +1741,117 @@ function buildRankerMeta(applied, modelId, skippedReason) {
|
|
|
1831
1741
|
}
|
|
1832
1742
|
return meta;
|
|
1833
1743
|
}
|
|
1744
|
+
/** The relations search follows, and their names seen from the other end. */
|
|
1745
|
+
const POINTER_RELATIONS = {
|
|
1746
|
+
refines: 'refined_by',
|
|
1747
|
+
contradicts: 'contradicted_by',
|
|
1748
|
+
builds_on: 'built_on_by',
|
|
1749
|
+
debates: 'debated_by',
|
|
1750
|
+
synthesizes: 'synthesized_by',
|
|
1751
|
+
supersedes: 'superseded_by',
|
|
1752
|
+
};
|
|
1753
|
+
/**
|
|
1754
|
+
* Each direct hit names the records its stated links reach, in both directions, including a link
|
|
1755
|
+
* to another hit. Expanded rows rank below every direct hit (a measured regression when they did
|
|
1756
|
+
* not) and are cut at the usual limits, so the link comes along on its hit instead, and the
|
|
1757
|
+
* reader opens the record it names. A scoped search names only records in its scopes, and a
|
|
1758
|
+
* correction only when the record stating it is in them.
|
|
1759
|
+
*/
|
|
1760
|
+
function withLinkPointers(rows, adapter, scopes) {
|
|
1761
|
+
const hitIds = rows.filter((row) => !row.related_to).map((row) => row.id);
|
|
1762
|
+
if (hitIds.length === 0)
|
|
1763
|
+
return rows;
|
|
1764
|
+
const hits = hitIds.map(() => '?').join(', ');
|
|
1765
|
+
const relations = Object.keys(POINTER_RELATIONS);
|
|
1766
|
+
const edges = adapter
|
|
1767
|
+
.prepare(`SELECT edge_id, from_id, to_id, relationship, reason FROM ${graph_query_js_1.STATED_DECISION_EDGES} e
|
|
1768
|
+
WHERE (from_id IN (${hits}) OR to_id IN (${hits}))
|
|
1769
|
+
AND relationship IN (${relations.map(() => '?').join(', ')})
|
|
1770
|
+
AND (approved_by_user = 1 OR approved_by_user IS NULL)
|
|
1771
|
+
ORDER BY created_at`)
|
|
1772
|
+
.all(...hitIds, ...hitIds, ...relations);
|
|
1773
|
+
if (edges.length === 0)
|
|
1774
|
+
return rows;
|
|
1775
|
+
const hitSet = new Set(hitIds);
|
|
1776
|
+
const otherIds = [...new Set(edges.flatMap((edge) => [edge.from_id, edge.to_id]))];
|
|
1777
|
+
const records = new Map(adapter
|
|
1778
|
+
.prepare(`SELECT id, topic, decision, status FROM decisions
|
|
1779
|
+
WHERE id IN (${otherIds.map(() => '?').join(', ')})`)
|
|
1780
|
+
.all(...otherIds).map((record) => [record.id, record]));
|
|
1781
|
+
const corrections = (0, decision_links_js_1.correctionsOf)(adapter, edges.flatMap((edge) => (edge.edge_id ? [edge.edge_id] : [])));
|
|
1782
|
+
const requested = scopes?.length ? new Set(scopes.map((s) => `${s.kind}:${s.id}`)) : null;
|
|
1783
|
+
const scopeMap = requested
|
|
1784
|
+
? batchLoadScopes(adapter, [
|
|
1785
|
+
...otherIds,
|
|
1786
|
+
...[...corrections.values()].flatMap((list) => correctionAuthors(list)),
|
|
1787
|
+
])
|
|
1788
|
+
: new Map();
|
|
1789
|
+
const inScope = (id) => !requested ||
|
|
1790
|
+
(scopeMap.get(id) ?? []).some((scope) => requested.has(`${scope.kind}:${scope.id}`));
|
|
1791
|
+
const byHit = new Map();
|
|
1792
|
+
const point = (hitId, otherId, relation, edge) => {
|
|
1793
|
+
const record = records.get(otherId);
|
|
1794
|
+
if (!record || !inScope(otherId))
|
|
1795
|
+
return;
|
|
1796
|
+
const correctedBy = edge.edge_id ? corrections.get(edge.edge_id) : undefined;
|
|
1797
|
+
const readable = correctedBy
|
|
1798
|
+
? readableCorrections(correctedBy, (correction) => inScope(correction.from))
|
|
1799
|
+
: undefined;
|
|
1800
|
+
const list = byHit.get(hitId) ?? [];
|
|
1801
|
+
list.push({
|
|
1802
|
+
id: record.id,
|
|
1803
|
+
topic: record.topic,
|
|
1804
|
+
summary: record.decision.split('\n')[0].slice(0, 200),
|
|
1805
|
+
...(record.status && record.status !== 'active' ? { status: record.status } : {}),
|
|
1806
|
+
relation,
|
|
1807
|
+
reason: edge.reason,
|
|
1808
|
+
...(readable ? { corrected_by: readable } : {}),
|
|
1809
|
+
});
|
|
1810
|
+
byHit.set(hitId, list);
|
|
1811
|
+
};
|
|
1812
|
+
for (const edge of edges) {
|
|
1813
|
+
if (hitSet.has(edge.from_id))
|
|
1814
|
+
point(edge.from_id, edge.to_id, edge.relationship, edge);
|
|
1815
|
+
if (hitSet.has(edge.to_id))
|
|
1816
|
+
point(edge.to_id, edge.from_id, POINTER_RELATIONS[edge.relationship], edge);
|
|
1817
|
+
}
|
|
1818
|
+
return rows.map((row) => {
|
|
1819
|
+
const links = row.related_to ? undefined : byHit.get(row.id);
|
|
1820
|
+
return links ? { ...row, links } : row;
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* A hit that is one revision of a work item says which one and the item's head. Search ranks an
|
|
1825
|
+
* item's revisions by their text, so an earlier revision can rank above the one that corrected it
|
|
1826
|
+
* (on a copy of the owner's database a revision 16 of 20 ranked first and the head was not in the
|
|
1827
|
+
* top ten); the reader opens the head before answering from an earlier one.
|
|
1828
|
+
*/
|
|
1829
|
+
function withWorkRevision(rows, adapter) {
|
|
1830
|
+
if (rows.length === 0)
|
|
1831
|
+
return rows;
|
|
1832
|
+
const found = new Map(adapter
|
|
1833
|
+
.prepare(`SELECT a.record_id, a.commitment_id, a.revision, c.current_revision
|
|
1834
|
+
FROM commitment_assignments a
|
|
1835
|
+
JOIN commitments c ON c.commitment_id = a.commitment_id
|
|
1836
|
+
WHERE a.record_id IN (${rows.map(() => '?').join(', ')})`)
|
|
1837
|
+
.all(...rows.map((row) => row.id)).map((row) => [row.record_id, row]));
|
|
1838
|
+
return rows.map((row) => {
|
|
1839
|
+
const work = found.get(row.id);
|
|
1840
|
+
return work
|
|
1841
|
+
? {
|
|
1842
|
+
...row,
|
|
1843
|
+
work_item: {
|
|
1844
|
+
commitment_id: work.commitment_id,
|
|
1845
|
+
revision: work.revision,
|
|
1846
|
+
head_revision: work.current_revision,
|
|
1847
|
+
},
|
|
1848
|
+
}
|
|
1849
|
+
: row;
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1834
1852
|
function mapRolledUpResult(result) {
|
|
1835
1853
|
const record = resultRecord(result);
|
|
1854
|
+
const reached = record.reached_through;
|
|
1836
1855
|
const retrievalDiagnostics = result.retrieval_diagnostics;
|
|
1837
1856
|
const topic = stringOrNull(record.topic ?? record.title) ?? result.source_id;
|
|
1838
1857
|
// For wiki_page leaves, prefer the markdown body (`content`) in `decision` so
|
|
@@ -1863,10 +1882,13 @@ function mapRolledUpResult(result) {
|
|
|
1863
1882
|
created_at: record.created_at ?? null,
|
|
1864
1883
|
event_date: record.event_date ?? null,
|
|
1865
1884
|
event_datetime: record.event_datetime ?? null,
|
|
1866
|
-
|
|
1885
|
+
// An expanded record says which hit it came from and through which link, so the reader
|
|
1886
|
+
// can tell it from a direct hit and weigh the link's reason (and any correction of it).
|
|
1887
|
+
graph_source: reached?.relation ?? retrievalDiagnostics?.graph_source ?? 'primary',
|
|
1867
1888
|
graph_rank: 1,
|
|
1868
|
-
related_to: null,
|
|
1869
|
-
edge_reason: null,
|
|
1889
|
+
related_to: reached?.from ?? null,
|
|
1890
|
+
edge_reason: reached?.reason ?? null,
|
|
1891
|
+
...(reached?.corrected_by ? { edge_corrected_by: reached.corrected_by } : {}),
|
|
1870
1892
|
case_id: result.case_id,
|
|
1871
1893
|
source_type: result.source_type,
|
|
1872
1894
|
kind: stringOrNull(record.kind) ?? 'decision',
|
|
@@ -2026,7 +2048,7 @@ async function suggestInAdapter(adapter, userQuestion, options = {}
|
|
|
2026
2048
|
if (rolledUp.length > 0) {
|
|
2027
2049
|
const filteredResults = rolledUp.slice(0, rerankPoolLimit);
|
|
2028
2050
|
const { results: mappedResults, meta: rankerMeta } = applyLearnedRanker(filteredResults.map(mapRolledUpResult));
|
|
2029
|
-
const limitedResults = mappedResults.slice(0, limit);
|
|
2051
|
+
const limitedResults = withWorkRevision(withLinkPointers(mappedResults.slice(0, limit), adapter, options.scopes), adapter);
|
|
2030
2052
|
if (format === 'markdown') {
|
|
2031
2053
|
const context = limitedResults
|
|
2032
2054
|
.map((result, index) => `${index + 1}. [${result.topic}] ${result.decision}\n ${result.reasoning}`)
|
|
@@ -2091,7 +2113,7 @@ async function suggestInAdapter(adapter, userQuestion, options = {}
|
|
|
2091
2113
|
: {}),
|
|
2092
2114
|
}));
|
|
2093
2115
|
const { results: rankedRows, meta: rankerMeta } = applyLearnedRanker(baseRows);
|
|
2094
|
-
const limitedRows = rankedRows.slice(0, limit);
|
|
2116
|
+
const limitedRows = withWorkRevision(withLinkPointers(rankedRows.slice(0, limit), adapter, options.scopes), adapter);
|
|
2095
2117
|
if (format === 'markdown') {
|
|
2096
2118
|
const context = limitedRows
|
|
2097
2119
|
.map((row, index) => `${index + 1}. [${row.topic}] ${row.decision}\n ${row.reasoning}`)
|