@jungjaehoon/mama-core 4.1.0 → 5.0.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 +3 -6
- package/dist/api/dispatch.js +12 -1
- 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/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 +11 -3
- package/dist/knowledge/index.js +9 -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/mama-api.d.ts +22 -7
- package/dist/mama-api.js +64 -15
- package/dist/memory/api.d.ts +16 -11
- package/dist/memory/api.js +376 -400
- 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/types.d.ts +13 -0
- 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,170 +708,9 @@ 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
716
|
async function retireMemoryRecord(adapter, input, access, commandId) {
|
|
@@ -1366,124 +1224,133 @@ async function recallMemory(adapter, query, options = {}) {
|
|
|
1366
1224
|
bundle.graph_context.expanded = [];
|
|
1367
1225
|
bundle.graph_context.edges = [];
|
|
1368
1226
|
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
|
-
];
|
|
1227
|
+
const candidates = matched.map((m) => ({
|
|
1228
|
+
id: m.id,
|
|
1229
|
+
topic: m.topic,
|
|
1230
|
+
decision: m.summary,
|
|
1231
|
+
confidence: m.confidence,
|
|
1232
|
+
created_at: m.created_at,
|
|
1233
|
+
similarity: m.confidence ?? 0.5,
|
|
1234
|
+
}));
|
|
1235
|
+
const expanded = await expandWithGraphInAdapter(adapter, candidates);
|
|
1236
|
+
const primaryIds = new Set(matched.map((m) => m.id));
|
|
1237
|
+
let expandedOnly = expanded.filter((e) => !primaryIds.has(e.id));
|
|
1238
|
+
let expandedScopeMap = new Map();
|
|
1239
|
+
// Re-filter expanded results: apply status and scope checks
|
|
1240
|
+
if (!options.includeHistory) {
|
|
1241
|
+
expandedOnly = expandedOnly.filter((e) => {
|
|
1242
|
+
const row = adapter.prepare(`SELECT kind, status FROM decisions WHERE id = ?`).get(e.id);
|
|
1243
|
+
const status = row?.status || '';
|
|
1244
|
+
return matchesKind(row?.kind) && (!status || !EXCLUDED_STATUSES.has(status));
|
|
1440
1245
|
});
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1246
|
+
}
|
|
1247
|
+
else if (options.kind !== undefined) {
|
|
1248
|
+
expandedOnly = expandedOnly.filter((e) => {
|
|
1249
|
+
const row = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1250
|
+
return matchesKind(row?.kind);
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (options.scopes && options.scopes.length > 0) {
|
|
1254
|
+
const expandedIds = expandedOnly.map((e) => e.id);
|
|
1255
|
+
expandedScopeMap = batchLoadScopes(adapter, expandedIds);
|
|
1256
|
+
const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
|
|
1257
|
+
expandedOnly = expandedOnly.filter((e) => {
|
|
1258
|
+
const scopes = expandedScopeMap.get(e.id) ?? [];
|
|
1259
|
+
if (scopes.length === 0)
|
|
1260
|
+
return false;
|
|
1261
|
+
return scopes.some((s) => requestedScopes.has(`${s.kind}:${s.id}`));
|
|
1262
|
+
});
|
|
1263
|
+
// A correction is stated by a record; its reason shows under the rule the records follow.
|
|
1264
|
+
const correctionScopes = batchLoadScopes(adapter, expandedOnly.flatMap((e) => correctionAuthors(e.edge_corrected_by ?? [])));
|
|
1265
|
+
const readable = (correction) => (correctionScopes.get(correction.from) ?? []).some((scope) => requestedScopes.has(`${scope.kind}:${scope.id}`));
|
|
1266
|
+
expandedOnly = expandedOnly.map((e) => e.edge_corrected_by
|
|
1267
|
+
? { ...e, edge_corrected_by: readableCorrections(e.edge_corrected_by, readable) }
|
|
1268
|
+
: e);
|
|
1269
|
+
}
|
|
1270
|
+
bundle.graph_context.expanded = expandedOnly.flatMap((e) => {
|
|
1271
|
+
const kindRow = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
|
|
1272
|
+
const expandedRecord = {
|
|
1273
|
+
id: String(e.id),
|
|
1274
|
+
topic: String(e.topic || ''),
|
|
1275
|
+
kind: (kindRow?.kind ?? 'decision'),
|
|
1276
|
+
summary: String(e.decision || ''),
|
|
1277
|
+
details: '',
|
|
1278
|
+
confidence: e.graph_rank ?? 0.5,
|
|
1279
|
+
status: 'active',
|
|
1280
|
+
scopes: expandedScopeMap.get(e.id) ?? [],
|
|
1281
|
+
source: {
|
|
1282
|
+
package: 'mama-core',
|
|
1283
|
+
source_type: String(e.graph_source || 'graph_expansion'),
|
|
1284
|
+
},
|
|
1285
|
+
created_at: e.created_at ?? Date.now(),
|
|
1286
|
+
updated_at: e.created_at ?? Date.now(),
|
|
1287
|
+
...(e.related_to
|
|
1288
|
+
? {
|
|
1289
|
+
reached_through: {
|
|
1290
|
+
from: e.related_to,
|
|
1291
|
+
relation: String(e.graph_source),
|
|
1292
|
+
reason: e.edge_reason ?? null,
|
|
1293
|
+
...(e.edge_corrected_by ? { corrected_by: e.edge_corrected_by } : {}),
|
|
1294
|
+
},
|
|
1295
|
+
}
|
|
1296
|
+
: {}),
|
|
1297
|
+
};
|
|
1298
|
+
const expandedDiagnostics = buildDiagnostics(expandedRecord, 'expanded');
|
|
1299
|
+
if (!passesStrictness(expandedDiagnostics)) {
|
|
1300
|
+
diagnostics.candidate_counts.rejected_by_strictness += 1;
|
|
1301
|
+
return [];
|
|
1483
1302
|
}
|
|
1303
|
+
return [
|
|
1304
|
+
searchOptions.diagnostics
|
|
1305
|
+
? {
|
|
1306
|
+
...expandedRecord,
|
|
1307
|
+
retrieval_diagnostics: expandedDiagnostics,
|
|
1308
|
+
}
|
|
1309
|
+
: expandedRecord,
|
|
1310
|
+
];
|
|
1311
|
+
});
|
|
1312
|
+
diagnostics.candidate_counts.graph_expanded = bundle.graph_context.expanded.length;
|
|
1313
|
+
// Graph-expanded hits are supporting context for the primary matches -
|
|
1314
|
+
// they must never OUTRANK them. The previous score ((graph_rank)*0.1,
|
|
1315
|
+
// typically 0.05-0.095) sat far above the entire RRF range (<=~0.018),
|
|
1316
|
+
// so expansion hits displaced every primary hit from the fused top-N
|
|
1317
|
+
// (delta-bench root cause: top-5 filled with related-but-wrong topics
|
|
1318
|
+
// while the queried topic's own rows were cut). Scale them into a band
|
|
1319
|
+
// strictly below the weakest primary hit, ordered by graph rank.
|
|
1320
|
+
const minPrimaryScore = fusedHits.length > 0 ? Math.min(...fusedHits.map((hit) => hit.fused_rank_score)) : 0.002;
|
|
1321
|
+
fusedHits = [
|
|
1322
|
+
...fusedHits,
|
|
1323
|
+
...bundle.graph_context.expanded.map((record) => ({
|
|
1324
|
+
source_type: 'decision',
|
|
1325
|
+
source_id: record.id,
|
|
1326
|
+
record,
|
|
1327
|
+
fused_rank_score: minPrimaryScore * 0.9 * Math.min(1, Math.max(0.1, record.confidence ?? 0.5)),
|
|
1328
|
+
retrieval_diagnostics: record.retrieval_diagnostics,
|
|
1329
|
+
})),
|
|
1330
|
+
].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
|
|
1331
|
+
// bundle.graph_context.expanded is the strictness-filtered set of
|
|
1332
|
+
// expanded nodes that will actually be returned. Use those IDs (not
|
|
1333
|
+
// expandedOnly, which still contains rejected candidates) so edges
|
|
1334
|
+
// never point at nodes that never made it into the graph payload.
|
|
1335
|
+
const acceptedExpandedIds = bundle.graph_context.expanded.map((record) => record.id);
|
|
1336
|
+
const allIds = [...matched.map((m) => m.id), ...acceptedExpandedIds];
|
|
1337
|
+
const allEdges = await loadEdgesForIds(adapter, allIds);
|
|
1338
|
+
// Filter out edges pointing to decisions with excluded statuses
|
|
1339
|
+
const activeIds = new Set(allIds);
|
|
1340
|
+
const edgesToCheck = allEdges.filter((e) => !activeIds.has(e.to_id) || !activeIds.has(e.from_id));
|
|
1341
|
+
if (edgesToCheck.length > 0) {
|
|
1342
|
+
const checkIds = [
|
|
1343
|
+
...new Set(edgesToCheck.flatMap((e) => [e.from_id, e.to_id]).filter((id) => !activeIds.has(id))),
|
|
1344
|
+
];
|
|
1345
|
+
const placeholders = checkIds.map(() => '?').join(', ');
|
|
1346
|
+
const statusRows = adapter
|
|
1347
|
+
.prepare(`SELECT id, status FROM decisions WHERE id IN (${placeholders})`)
|
|
1348
|
+
.all(...checkIds);
|
|
1349
|
+
const excludedIds = new Set(statusRows.filter((r) => r.status && EXCLUDED_STATUSES.has(r.status)).map((r) => r.id));
|
|
1350
|
+
bundle.graph_context.edges = allEdges.filter((e) => !excludedIds.has(e.from_id) && !excludedIds.has(e.to_id));
|
|
1484
1351
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1352
|
+
else {
|
|
1353
|
+
bundle.graph_context.edges = allEdges;
|
|
1487
1354
|
}
|
|
1488
1355
|
}
|
|
1489
1356
|
bundle.fused_hits = fusedHits;
|
|
@@ -1546,9 +1413,6 @@ async function ingestMemoryInternal(adapter, input) {
|
|
|
1546
1413
|
async function ingestMemory(adapter, input) {
|
|
1547
1414
|
return ingestMemoryInternal(adapter, (0, provenance_js_1.sanitizePublicIngestMemoryInput)(input));
|
|
1548
1415
|
}
|
|
1549
|
-
async function evolveMemory(input) {
|
|
1550
|
-
return (0, evolution_engine_js_1.resolveMemoryEvolution)(input);
|
|
1551
|
-
}
|
|
1552
1416
|
async function buildMemoryBootstrap(adapter, params) {
|
|
1553
1417
|
return (0, bootstrap_builder_js_1.buildMemoryAgentBootstrap)(adapter, params);
|
|
1554
1418
|
}
|
|
@@ -1661,6 +1525,7 @@ function confidenceValue(record, fallback) {
|
|
|
1661
1525
|
async function expandWithGraphInAdapter(adapter, candidates) {
|
|
1662
1526
|
const graphEnhanced = new Map(); // Use Map for deduplication by ID
|
|
1663
1527
|
const primaryIds = new Set(candidates.map((c) => c.id)); // Track primary candidates
|
|
1528
|
+
const reachedThrough = new Map(); // expanded record id -> the link's edge id
|
|
1664
1529
|
// Process each candidate
|
|
1665
1530
|
for (const candidate of candidates) {
|
|
1666
1531
|
// Add primary candidate with higher rank
|
|
@@ -1671,97 +1536,96 @@ async function expandWithGraphInAdapter(adapter, candidates) {
|
|
|
1671
1536
|
graph_rank: 1.0, // Highest rank
|
|
1672
1537
|
});
|
|
1673
1538
|
}
|
|
1674
|
-
// 1.
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1539
|
+
// 1. The records this one replaced, down its supersedes chain
|
|
1540
|
+
const chain = await (0, graph_query_js_1.queryDecisionGraph)(adapter, candidate.topic, candidate.id);
|
|
1541
|
+
for (const decision of chain) {
|
|
1542
|
+
if (!graphEnhanced.has(decision.id)) {
|
|
1543
|
+
graphEnhanced.set(decision.id, {
|
|
1544
|
+
...decision,
|
|
1545
|
+
graph_source: 'supersedes_chain',
|
|
1546
|
+
graph_rank: 0.8, // Lower rank than primary
|
|
1547
|
+
similarity: (candidate.similarity ?? 0) * 0.9, // Inherit similarity, slightly reduced
|
|
1548
|
+
related_to: candidate.id, // Track relationship
|
|
1549
|
+
});
|
|
1687
1550
|
}
|
|
1688
1551
|
}
|
|
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);
|
|
1552
|
+
// 2. The links an agent stated (refines, contradicts, builds_on, debates, synthesizes)
|
|
1553
|
+
const rawEdges = (await (0, graph_query_js_1.querySemanticEdges)(adapter, [candidate.id])) || {};
|
|
1554
|
+
const edges = {
|
|
1555
|
+
refines: rawEdges.refines || [],
|
|
1556
|
+
refined_by: rawEdges.refined_by || [],
|
|
1557
|
+
contradicts: rawEdges.contradicts || [],
|
|
1558
|
+
contradicted_by: rawEdges.contradicted_by || [],
|
|
1559
|
+
builds_on: rawEdges.builds_on || [],
|
|
1560
|
+
built_on_by: rawEdges.built_on_by || [],
|
|
1561
|
+
debates: rawEdges.debates || [],
|
|
1562
|
+
debated_by: rawEdges.debated_by || [],
|
|
1563
|
+
synthesizes: rawEdges.synthesizes || [],
|
|
1564
|
+
synthesized_by: rawEdges.synthesized_by || [],
|
|
1565
|
+
};
|
|
1566
|
+
// Helper to add edge to graph
|
|
1567
|
+
const addEdge = (edge, idField, source, rank, simFactor) => {
|
|
1568
|
+
const id = edge[idField];
|
|
1569
|
+
if (!graphEnhanced.has(id)) {
|
|
1570
|
+
graphEnhanced.set(id, {
|
|
1571
|
+
id: id,
|
|
1572
|
+
topic: edge.topic,
|
|
1573
|
+
decision: edge.decision,
|
|
1574
|
+
confidence: edge.confidence,
|
|
1575
|
+
created_at: edge.created_at,
|
|
1576
|
+
graph_source: source,
|
|
1577
|
+
graph_rank: rank,
|
|
1578
|
+
similarity: (candidate.similarity ?? 0) * simFactor,
|
|
1579
|
+
related_to: candidate.id,
|
|
1580
|
+
edge_reason: edge.reason,
|
|
1581
|
+
});
|
|
1582
|
+
if (edge.edge_id)
|
|
1583
|
+
reachedThrough.set(id, edge.edge_id);
|
|
1760
1584
|
}
|
|
1585
|
+
};
|
|
1586
|
+
// Add refines edges
|
|
1587
|
+
for (const edge of edges.refines) {
|
|
1588
|
+
addEdge(edge, 'to_id', 'refines', 0.7, 0.85);
|
|
1589
|
+
}
|
|
1590
|
+
// Add refined_by edges
|
|
1591
|
+
for (const edge of edges.refined_by) {
|
|
1592
|
+
addEdge(edge, 'from_id', 'refined_by', 0.7, 0.85);
|
|
1593
|
+
}
|
|
1594
|
+
// Add contradicts edges (lower rank, but still relevant)
|
|
1595
|
+
for (const edge of edges.contradicts) {
|
|
1596
|
+
addEdge(edge, 'to_id', 'contradicts', 0.6, 0.8);
|
|
1597
|
+
}
|
|
1598
|
+
// Story 2.1: Add builds_on edges (high relevance - extending prior work)
|
|
1599
|
+
for (const edge of edges.builds_on) {
|
|
1600
|
+
addEdge(edge, 'to_id', 'builds_on', 0.75, 0.9);
|
|
1601
|
+
}
|
|
1602
|
+
// Add built_on_by edges (someone built on this decision)
|
|
1603
|
+
for (const edge of edges.built_on_by) {
|
|
1604
|
+
addEdge(edge, 'from_id', 'built_on_by', 0.75, 0.9);
|
|
1605
|
+
}
|
|
1606
|
+
// Add debates edges (alternative view)
|
|
1607
|
+
for (const edge of edges.debates) {
|
|
1608
|
+
addEdge(edge, 'to_id', 'debates', 0.65, 0.85);
|
|
1609
|
+
}
|
|
1610
|
+
// Add debated_by edges
|
|
1611
|
+
for (const edge of edges.debated_by) {
|
|
1612
|
+
addEdge(edge, 'from_id', 'debated_by', 0.65, 0.85);
|
|
1761
1613
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1614
|
+
// Add synthesizes edges (unified approach)
|
|
1615
|
+
for (const edge of edges.synthesizes) {
|
|
1616
|
+
addEdge(edge, 'to_id', 'synthesizes', 0.7, 0.88);
|
|
1764
1617
|
}
|
|
1618
|
+
// Add synthesized_by edges
|
|
1619
|
+
for (const edge of edges.synthesized_by) {
|
|
1620
|
+
addEdge(edge, 'from_id', 'synthesized_by', 0.7, 0.88);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
// A link the agent later contradicted still leads here; the correction goes with the record.
|
|
1624
|
+
const corrections = (0, decision_links_js_1.correctionsOf)(adapter, [...reachedThrough.values()]);
|
|
1625
|
+
for (const [id, edgeId] of reachedThrough) {
|
|
1626
|
+
const correctedBy = corrections.get(edgeId);
|
|
1627
|
+
if (correctedBy)
|
|
1628
|
+
graphEnhanced.set(id, { ...graphEnhanced.get(id), edge_corrected_by: correctedBy });
|
|
1765
1629
|
}
|
|
1766
1630
|
// 3. Convert Map to Array
|
|
1767
1631
|
const allResults = Array.from(graphEnhanced.values());
|
|
@@ -1831,8 +1695,117 @@ function buildRankerMeta(applied, modelId, skippedReason) {
|
|
|
1831
1695
|
}
|
|
1832
1696
|
return meta;
|
|
1833
1697
|
}
|
|
1698
|
+
/** The relations search follows, and their names seen from the other end. */
|
|
1699
|
+
const POINTER_RELATIONS = {
|
|
1700
|
+
refines: 'refined_by',
|
|
1701
|
+
contradicts: 'contradicted_by',
|
|
1702
|
+
builds_on: 'built_on_by',
|
|
1703
|
+
debates: 'debated_by',
|
|
1704
|
+
synthesizes: 'synthesized_by',
|
|
1705
|
+
supersedes: 'superseded_by',
|
|
1706
|
+
};
|
|
1707
|
+
/**
|
|
1708
|
+
* Each direct hit names the records its stated links reach, in both directions, including a link
|
|
1709
|
+
* to another hit. Expanded rows rank below every direct hit (a measured regression when they did
|
|
1710
|
+
* not) and are cut at the usual limits, so the link comes along on its hit instead, and the
|
|
1711
|
+
* reader opens the record it names. A scoped search names only records in its scopes, and a
|
|
1712
|
+
* correction only when the record stating it is in them.
|
|
1713
|
+
*/
|
|
1714
|
+
function withLinkPointers(rows, adapter, scopes) {
|
|
1715
|
+
const hitIds = rows.filter((row) => !row.related_to).map((row) => row.id);
|
|
1716
|
+
if (hitIds.length === 0)
|
|
1717
|
+
return rows;
|
|
1718
|
+
const hits = hitIds.map(() => '?').join(', ');
|
|
1719
|
+
const relations = Object.keys(POINTER_RELATIONS);
|
|
1720
|
+
const edges = adapter
|
|
1721
|
+
.prepare(`SELECT edge_id, from_id, to_id, relationship, reason FROM ${graph_query_js_1.STATED_DECISION_EDGES} e
|
|
1722
|
+
WHERE (from_id IN (${hits}) OR to_id IN (${hits}))
|
|
1723
|
+
AND relationship IN (${relations.map(() => '?').join(', ')})
|
|
1724
|
+
AND (approved_by_user = 1 OR approved_by_user IS NULL)
|
|
1725
|
+
ORDER BY created_at`)
|
|
1726
|
+
.all(...hitIds, ...hitIds, ...relations);
|
|
1727
|
+
if (edges.length === 0)
|
|
1728
|
+
return rows;
|
|
1729
|
+
const hitSet = new Set(hitIds);
|
|
1730
|
+
const otherIds = [...new Set(edges.flatMap((edge) => [edge.from_id, edge.to_id]))];
|
|
1731
|
+
const records = new Map(adapter
|
|
1732
|
+
.prepare(`SELECT id, topic, decision, status FROM decisions
|
|
1733
|
+
WHERE id IN (${otherIds.map(() => '?').join(', ')})`)
|
|
1734
|
+
.all(...otherIds).map((record) => [record.id, record]));
|
|
1735
|
+
const corrections = (0, decision_links_js_1.correctionsOf)(adapter, edges.flatMap((edge) => (edge.edge_id ? [edge.edge_id] : [])));
|
|
1736
|
+
const requested = scopes?.length ? new Set(scopes.map((s) => `${s.kind}:${s.id}`)) : null;
|
|
1737
|
+
const scopeMap = requested
|
|
1738
|
+
? batchLoadScopes(adapter, [
|
|
1739
|
+
...otherIds,
|
|
1740
|
+
...[...corrections.values()].flatMap((list) => correctionAuthors(list)),
|
|
1741
|
+
])
|
|
1742
|
+
: new Map();
|
|
1743
|
+
const inScope = (id) => !requested ||
|
|
1744
|
+
(scopeMap.get(id) ?? []).some((scope) => requested.has(`${scope.kind}:${scope.id}`));
|
|
1745
|
+
const byHit = new Map();
|
|
1746
|
+
const point = (hitId, otherId, relation, edge) => {
|
|
1747
|
+
const record = records.get(otherId);
|
|
1748
|
+
if (!record || !inScope(otherId))
|
|
1749
|
+
return;
|
|
1750
|
+
const correctedBy = edge.edge_id ? corrections.get(edge.edge_id) : undefined;
|
|
1751
|
+
const readable = correctedBy
|
|
1752
|
+
? readableCorrections(correctedBy, (correction) => inScope(correction.from))
|
|
1753
|
+
: undefined;
|
|
1754
|
+
const list = byHit.get(hitId) ?? [];
|
|
1755
|
+
list.push({
|
|
1756
|
+
id: record.id,
|
|
1757
|
+
topic: record.topic,
|
|
1758
|
+
summary: record.decision.split('\n')[0].slice(0, 200),
|
|
1759
|
+
...(record.status && record.status !== 'active' ? { status: record.status } : {}),
|
|
1760
|
+
relation,
|
|
1761
|
+
reason: edge.reason,
|
|
1762
|
+
...(readable ? { corrected_by: readable } : {}),
|
|
1763
|
+
});
|
|
1764
|
+
byHit.set(hitId, list);
|
|
1765
|
+
};
|
|
1766
|
+
for (const edge of edges) {
|
|
1767
|
+
if (hitSet.has(edge.from_id))
|
|
1768
|
+
point(edge.from_id, edge.to_id, edge.relationship, edge);
|
|
1769
|
+
if (hitSet.has(edge.to_id))
|
|
1770
|
+
point(edge.to_id, edge.from_id, POINTER_RELATIONS[edge.relationship], edge);
|
|
1771
|
+
}
|
|
1772
|
+
return rows.map((row) => {
|
|
1773
|
+
const links = row.related_to ? undefined : byHit.get(row.id);
|
|
1774
|
+
return links ? { ...row, links } : row;
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* A hit that is one revision of a work item says which one and the item's head. Search ranks an
|
|
1779
|
+
* item's revisions by their text, so an earlier revision can rank above the one that corrected it
|
|
1780
|
+
* (on a copy of the owner's database a revision 16 of 20 ranked first and the head was not in the
|
|
1781
|
+
* top ten); the reader opens the head before answering from an earlier one.
|
|
1782
|
+
*/
|
|
1783
|
+
function withWorkRevision(rows, adapter) {
|
|
1784
|
+
if (rows.length === 0)
|
|
1785
|
+
return rows;
|
|
1786
|
+
const found = new Map(adapter
|
|
1787
|
+
.prepare(`SELECT a.record_id, a.commitment_id, a.revision, c.current_revision
|
|
1788
|
+
FROM commitment_assignments a
|
|
1789
|
+
JOIN commitments c ON c.commitment_id = a.commitment_id
|
|
1790
|
+
WHERE a.record_id IN (${rows.map(() => '?').join(', ')})`)
|
|
1791
|
+
.all(...rows.map((row) => row.id)).map((row) => [row.record_id, row]));
|
|
1792
|
+
return rows.map((row) => {
|
|
1793
|
+
const work = found.get(row.id);
|
|
1794
|
+
return work
|
|
1795
|
+
? {
|
|
1796
|
+
...row,
|
|
1797
|
+
work_item: {
|
|
1798
|
+
commitment_id: work.commitment_id,
|
|
1799
|
+
revision: work.revision,
|
|
1800
|
+
head_revision: work.current_revision,
|
|
1801
|
+
},
|
|
1802
|
+
}
|
|
1803
|
+
: row;
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1834
1806
|
function mapRolledUpResult(result) {
|
|
1835
1807
|
const record = resultRecord(result);
|
|
1808
|
+
const reached = record.reached_through;
|
|
1836
1809
|
const retrievalDiagnostics = result.retrieval_diagnostics;
|
|
1837
1810
|
const topic = stringOrNull(record.topic ?? record.title) ?? result.source_id;
|
|
1838
1811
|
// For wiki_page leaves, prefer the markdown body (`content`) in `decision` so
|
|
@@ -1863,10 +1836,13 @@ function mapRolledUpResult(result) {
|
|
|
1863
1836
|
created_at: record.created_at ?? null,
|
|
1864
1837
|
event_date: record.event_date ?? null,
|
|
1865
1838
|
event_datetime: record.event_datetime ?? null,
|
|
1866
|
-
|
|
1839
|
+
// An expanded record says which hit it came from and through which link, so the reader
|
|
1840
|
+
// can tell it from a direct hit and weigh the link's reason (and any correction of it).
|
|
1841
|
+
graph_source: reached?.relation ?? retrievalDiagnostics?.graph_source ?? 'primary',
|
|
1867
1842
|
graph_rank: 1,
|
|
1868
|
-
related_to: null,
|
|
1869
|
-
edge_reason: null,
|
|
1843
|
+
related_to: reached?.from ?? null,
|
|
1844
|
+
edge_reason: reached?.reason ?? null,
|
|
1845
|
+
...(reached?.corrected_by ? { edge_corrected_by: reached.corrected_by } : {}),
|
|
1870
1846
|
case_id: result.case_id,
|
|
1871
1847
|
source_type: result.source_type,
|
|
1872
1848
|
kind: stringOrNull(record.kind) ?? 'decision',
|
|
@@ -2026,7 +2002,7 @@ async function suggestInAdapter(adapter, userQuestion, options = {}
|
|
|
2026
2002
|
if (rolledUp.length > 0) {
|
|
2027
2003
|
const filteredResults = rolledUp.slice(0, rerankPoolLimit);
|
|
2028
2004
|
const { results: mappedResults, meta: rankerMeta } = applyLearnedRanker(filteredResults.map(mapRolledUpResult));
|
|
2029
|
-
const limitedResults = mappedResults.slice(0, limit);
|
|
2005
|
+
const limitedResults = withWorkRevision(withLinkPointers(mappedResults.slice(0, limit), adapter, options.scopes), adapter);
|
|
2030
2006
|
if (format === 'markdown') {
|
|
2031
2007
|
const context = limitedResults
|
|
2032
2008
|
.map((result, index) => `${index + 1}. [${result.topic}] ${result.decision}\n ${result.reasoning}`)
|
|
@@ -2091,7 +2067,7 @@ async function suggestInAdapter(adapter, userQuestion, options = {}
|
|
|
2091
2067
|
: {}),
|
|
2092
2068
|
}));
|
|
2093
2069
|
const { results: rankedRows, meta: rankerMeta } = applyLearnedRanker(baseRows);
|
|
2094
|
-
const limitedRows = rankedRows.slice(0, limit);
|
|
2070
|
+
const limitedRows = withWorkRevision(withLinkPointers(rankedRows.slice(0, limit), adapter, options.scopes), adapter);
|
|
2095
2071
|
if (format === 'markdown') {
|
|
2096
2072
|
const context = limitedRows
|
|
2097
2073
|
.map((row, index) => `${index + 1}. [${row.topic}] ${row.decision}\n ${row.reasoning}`)
|