@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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../db-manager.js';
|
|
2
|
+
import { type LinkReceipt } from '../knowledge/links.js';
|
|
3
|
+
import type { RecordLink } from './judgment-types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Decision links for the direct memory API (the Claude Code MCP): one decision linked to another,
|
|
6
|
+
* or to a link it corrects, with the reason the caller judged. Nothing is edited.
|
|
7
|
+
*/
|
|
8
|
+
export interface DecisionLinkInput {
|
|
9
|
+
from: string;
|
|
10
|
+
/** A decision id, or the id of a link this one contradicts. */
|
|
11
|
+
to: string;
|
|
12
|
+
relation: RecordLink['relation'];
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function appendDecisionLink(adapter: Pick<DatabaseAdapter, 'prepare' | 'transaction' | 'transactionImmediate'>, input: DecisionLinkInput): LinkReceipt;
|
|
16
|
+
export interface DecisionEdgeView {
|
|
17
|
+
relation: string;
|
|
18
|
+
direction: 'out' | 'in';
|
|
19
|
+
otherId: string;
|
|
20
|
+
otherTopic: string | null;
|
|
21
|
+
otherSummary: string | null;
|
|
22
|
+
reason: string | null;
|
|
23
|
+
/** agent: stated through a link; agent_text: parsed from reasoning text; host: written by code. */
|
|
24
|
+
source: 'agent' | 'agent_text' | 'host' | 'user';
|
|
25
|
+
edgeId?: string;
|
|
26
|
+
correctedBy?: DecisionCorrection[];
|
|
27
|
+
/** On an incoming amends edge: the values the amending record replaced on this decision. */
|
|
28
|
+
replacedValues?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export interface DecisionCorrection {
|
|
31
|
+
edgeId: string;
|
|
32
|
+
/** The record that states the correction. */
|
|
33
|
+
from: string;
|
|
34
|
+
reason: string | null;
|
|
35
|
+
at: number;
|
|
36
|
+
correctedBy?: DecisionCorrection[];
|
|
37
|
+
}
|
|
38
|
+
export interface DecisionWithEdges {
|
|
39
|
+
id: string;
|
|
40
|
+
topic: string;
|
|
41
|
+
decision: string;
|
|
42
|
+
reasoning: string | null;
|
|
43
|
+
outcome: string | null;
|
|
44
|
+
status: string | null;
|
|
45
|
+
createdAt: number;
|
|
46
|
+
supersedes: string | null;
|
|
47
|
+
supersededBy: string | null;
|
|
48
|
+
edges: DecisionEdgeView[];
|
|
49
|
+
}
|
|
50
|
+
/** Links that contradict these edges, and the links that contradict those, in two queries per level. */
|
|
51
|
+
export declare function correctionsOf(adapter: Pick<DatabaseAdapter, 'prepare'>, edgeIds: readonly string[]): Map<string, DecisionCorrection[]>;
|
|
52
|
+
/** One decision with every edge in and out, each with its reason and who wrote it. */
|
|
53
|
+
export declare function readDecisionWithEdges(adapter: Pick<DatabaseAdapter, 'prepare'>, id: string): DecisionWithEdges | null;
|
|
54
|
+
//# sourceMappingURL=decision-links.d.ts.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.appendDecisionLink = appendDecisionLink;
|
|
7
|
+
exports.correctionsOf = correctionsOf;
|
|
8
|
+
exports.readDecisionWithEdges = readDecisionWithEdges;
|
|
9
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
|
+
const canonicalize_js_1 = require("../canonicalize.js");
|
|
11
|
+
const graph_query_js_1 = require("../knowledge/graph-query.js");
|
|
12
|
+
const links_js_1 = require("../knowledge/links.js");
|
|
13
|
+
const secret_filter_js_1 = require("./secret-filter.js");
|
|
14
|
+
const write_adapters_js_1 = require("./write-adapters.js");
|
|
15
|
+
function isEdgeId(id) {
|
|
16
|
+
return id.startsWith('link_') || id.startsWith('edge_');
|
|
17
|
+
}
|
|
18
|
+
function appendDecisionLink(adapter, input) {
|
|
19
|
+
// The reason is stored text, like a checkpoint's, so it passes the same secret scan.
|
|
20
|
+
const scan = (0, secret_filter_js_1.scanMemoryWriteInput)({ reason: input.reason });
|
|
21
|
+
if (!scan.clean)
|
|
22
|
+
throw new secret_filter_js_1.SecretMaterialRefusedError(scan.matches);
|
|
23
|
+
const to = isEdgeId(input.to)
|
|
24
|
+
? { kind: 'edge', id: input.to }
|
|
25
|
+
: { kind: 'memory', id: input.to };
|
|
26
|
+
const scopes = new Map();
|
|
27
|
+
const memoryEnds = [input.from, ...(to.kind === 'memory' ? [to.id] : [])];
|
|
28
|
+
// A correction reads the corrected link, so its ends must be admitted too, down the chain when
|
|
29
|
+
// it corrects a correction. A link names only an edge that already exists, so the chain ends.
|
|
30
|
+
const endsOf = adapter.prepare(`SELECT subject_kind, subject_id, object_kind, object_id FROM twin_edges WHERE edge_id = ?`);
|
|
31
|
+
let edgeIds = to.kind === 'edge' ? [to.id] : [];
|
|
32
|
+
while (edgeIds.length > 0) {
|
|
33
|
+
const next = [];
|
|
34
|
+
for (const edgeId of edgeIds) {
|
|
35
|
+
const ends = endsOf.get(edgeId);
|
|
36
|
+
// An unknown edge is refused by appendLink, which names the caller's own id.
|
|
37
|
+
if (!ends)
|
|
38
|
+
continue;
|
|
39
|
+
for (const [kind, id] of [
|
|
40
|
+
[ends.subject_kind, ends.subject_id],
|
|
41
|
+
[ends.object_kind, ends.object_id],
|
|
42
|
+
]) {
|
|
43
|
+
if (kind === 'memory')
|
|
44
|
+
memoryEnds.push(id);
|
|
45
|
+
else if (kind === 'edge')
|
|
46
|
+
next.push(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
edgeIds = next;
|
|
50
|
+
}
|
|
51
|
+
for (const scope of memoryEnds.flatMap((id) => (0, write_adapters_js_1.boundScopesOf)(adapter, id))) {
|
|
52
|
+
scopes.set(`${scope.kind}\0${scope.id}`, scope);
|
|
53
|
+
}
|
|
54
|
+
// The same statement retried is the same link; a different reason is a different statement.
|
|
55
|
+
const commandId = `link:${node_crypto_1.default
|
|
56
|
+
.createHash('sha256')
|
|
57
|
+
.update((0, canonicalize_js_1.canonicalizeJSON)(input))
|
|
58
|
+
.digest('hex')
|
|
59
|
+
.slice(0, 24)}`;
|
|
60
|
+
return (0, links_js_1.appendLink)(adapter, {
|
|
61
|
+
commandId,
|
|
62
|
+
from: { kind: 'memory', id: input.from },
|
|
63
|
+
to,
|
|
64
|
+
relation: input.relation,
|
|
65
|
+
reason: input.reason,
|
|
66
|
+
}, (0, write_adapters_js_1.unsignedWriteAccess)([...scopes.values()]));
|
|
67
|
+
}
|
|
68
|
+
function legacySource(reason, createdBy) {
|
|
69
|
+
if (reason && graph_query_js_1.HOST_EDGE_REASON_PREFIXES.some((prefix) => reason.startsWith(prefix)))
|
|
70
|
+
return 'host';
|
|
71
|
+
if (createdBy === 'llm' || reason?.startsWith('Auto-detected from reasoning'))
|
|
72
|
+
return 'agent_text';
|
|
73
|
+
return 'user';
|
|
74
|
+
}
|
|
75
|
+
/** Links that contradict these edges, and the links that contradict those, in two queries per level. */
|
|
76
|
+
function correctionsOf(adapter, edgeIds) {
|
|
77
|
+
const byTarget = new Map();
|
|
78
|
+
let frontier = [...edgeIds];
|
|
79
|
+
while (frontier.length > 0) {
|
|
80
|
+
const placeholders = frontier.map(() => '?').join(', ');
|
|
81
|
+
const rows = adapter
|
|
82
|
+
.prepare(`SELECT edge_id, subject_id, object_id, reason_text, created_at FROM twin_edges
|
|
83
|
+
WHERE object_kind = 'edge' AND edge_type = 'contradicts' AND object_id IN (${placeholders})
|
|
84
|
+
ORDER BY created_at, rowid`)
|
|
85
|
+
.all(...frontier);
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
const list = byTarget.get(row.object_id) ?? [];
|
|
88
|
+
list.push({
|
|
89
|
+
edgeId: row.edge_id,
|
|
90
|
+
from: row.subject_id,
|
|
91
|
+
reason: row.reason_text,
|
|
92
|
+
at: row.created_at,
|
|
93
|
+
});
|
|
94
|
+
byTarget.set(row.object_id, list);
|
|
95
|
+
}
|
|
96
|
+
// A link names only an edge that already exists, so the chain has no cycle.
|
|
97
|
+
frontier = rows.map((row) => row.edge_id);
|
|
98
|
+
}
|
|
99
|
+
const nest = (edgeId) => byTarget.get(edgeId)?.map((correction) => {
|
|
100
|
+
const further = nest(correction.edgeId);
|
|
101
|
+
return further ? { ...correction, correctedBy: further } : correction;
|
|
102
|
+
});
|
|
103
|
+
const nested = new Map();
|
|
104
|
+
for (const edgeId of edgeIds) {
|
|
105
|
+
const list = nest(edgeId);
|
|
106
|
+
if (list)
|
|
107
|
+
nested.set(edgeId, list);
|
|
108
|
+
}
|
|
109
|
+
return nested;
|
|
110
|
+
}
|
|
111
|
+
function replacedValuesFor(payloadOf, amendingId, targetId) {
|
|
112
|
+
const row = payloadOf.get(amendingId);
|
|
113
|
+
if (!row?.payload_json)
|
|
114
|
+
return undefined;
|
|
115
|
+
const payload = JSON.parse(row.payload_json);
|
|
116
|
+
return payload.replacedValues?.find((entry) => entry.target === targetId)?.values;
|
|
117
|
+
}
|
|
118
|
+
/** One decision with every edge in and out, each with its reason and who wrote it. */
|
|
119
|
+
function readDecisionWithEdges(adapter, id) {
|
|
120
|
+
const row = adapter
|
|
121
|
+
.prepare(`SELECT id, topic, decision, reasoning, outcome, status, created_at, supersedes, superseded_by
|
|
122
|
+
FROM decisions WHERE id = ?`)
|
|
123
|
+
.get(id);
|
|
124
|
+
if (!row)
|
|
125
|
+
return null;
|
|
126
|
+
const other = adapter.prepare('SELECT topic, decision FROM decisions WHERE id = ?');
|
|
127
|
+
const describe = (otherId) => {
|
|
128
|
+
const found = other.get(otherId);
|
|
129
|
+
return {
|
|
130
|
+
otherTopic: found?.topic ?? null,
|
|
131
|
+
otherSummary: found ? found.decision.split('\n')[0].slice(0, 200) : null,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const edges = [];
|
|
135
|
+
const twin = adapter
|
|
136
|
+
.prepare(`SELECT edge_id, edge_type, subject_id, object_id, reason_text, relation_attrs_json, source
|
|
137
|
+
FROM twin_edges
|
|
138
|
+
WHERE edge_type <> 'derived_from'
|
|
139
|
+
AND ((subject_kind = 'memory' AND subject_id = ? AND object_kind = 'memory')
|
|
140
|
+
OR (object_kind = 'memory' AND object_id = ? AND subject_kind = 'memory'))
|
|
141
|
+
ORDER BY created_at, rowid`)
|
|
142
|
+
.all(id, id);
|
|
143
|
+
const corrections = correctionsOf(adapter, twin.map((edge) => edge.edge_id));
|
|
144
|
+
const payloadOf = adapter.prepare('SELECT payload_json FROM decisions WHERE id = ?');
|
|
145
|
+
for (const edge of twin) {
|
|
146
|
+
const out = edge.subject_id === id;
|
|
147
|
+
const otherId = out ? edge.object_id : edge.subject_id;
|
|
148
|
+
const attrs = edge.relation_attrs_json
|
|
149
|
+
? JSON.parse(edge.relation_attrs_json)
|
|
150
|
+
: {};
|
|
151
|
+
const corrected = corrections.get(edge.edge_id);
|
|
152
|
+
const replaced = !out && edge.edge_type === 'amends' ? replacedValuesFor(payloadOf, otherId, id) : undefined;
|
|
153
|
+
edges.push({
|
|
154
|
+
relation: edge.edge_type,
|
|
155
|
+
direction: out ? 'out' : 'in',
|
|
156
|
+
otherId,
|
|
157
|
+
...describe(otherId),
|
|
158
|
+
reason: typeof attrs.reason === 'string' ? attrs.reason : edge.reason_text,
|
|
159
|
+
source: edge.source === 'code' ? 'host' : 'agent',
|
|
160
|
+
edgeId: edge.edge_id,
|
|
161
|
+
...(corrected ? { correctedBy: corrected } : {}),
|
|
162
|
+
...(replaced ? { replacedValues: replaced } : {}),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const legacy = adapter
|
|
166
|
+
.prepare(`SELECT from_id, to_id, relationship, reason, created_by FROM decision_edges
|
|
167
|
+
WHERE from_id = ? OR to_id = ?
|
|
168
|
+
ORDER BY created_at`)
|
|
169
|
+
.all(id, id);
|
|
170
|
+
for (const edge of legacy) {
|
|
171
|
+
const out = edge.from_id === id;
|
|
172
|
+
const otherId = out ? edge.to_id : edge.from_id;
|
|
173
|
+
edges.push({
|
|
174
|
+
relation: edge.relationship,
|
|
175
|
+
direction: out ? 'out' : 'in',
|
|
176
|
+
otherId,
|
|
177
|
+
...describe(otherId),
|
|
178
|
+
reason: edge.reason,
|
|
179
|
+
source: legacySource(edge.reason, edge.created_by),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
id: row.id,
|
|
184
|
+
topic: row.topic,
|
|
185
|
+
decision: row.decision,
|
|
186
|
+
reasoning: row.reasoning,
|
|
187
|
+
outcome: row.outcome,
|
|
188
|
+
status: row.status,
|
|
189
|
+
createdAt: row.created_at,
|
|
190
|
+
supersedes: row.supersedes,
|
|
191
|
+
supersededBy: row.superseded_by,
|
|
192
|
+
edges,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=decision-links.js.map
|
|
@@ -23,9 +23,6 @@ export interface GraphReadEdge {
|
|
|
23
23
|
relationship: string;
|
|
24
24
|
reason: string | null;
|
|
25
25
|
}
|
|
26
|
-
export interface GraphSimilarityEdge extends GraphReadEdge {
|
|
27
|
-
similarity: number;
|
|
28
|
-
}
|
|
29
26
|
export interface GraphScope {
|
|
30
27
|
kind: string;
|
|
31
28
|
id: string;
|
|
@@ -36,17 +33,4 @@ export declare function readGraphNodes(adapter: DatabaseAdapter, scopes: readonl
|
|
|
36
33
|
}): Promise<GraphReadNode[]>;
|
|
37
34
|
export declare function readGraphEdges(adapter: DatabaseAdapter, scopes: readonly GraphScope[]): Promise<GraphReadEdge[]>;
|
|
38
35
|
export declare function countGraphNodes(adapter: DatabaseAdapter, scopes: readonly GraphScope[]): Promise<number>;
|
|
39
|
-
/**
|
|
40
|
-
* Similarity edges over the admitted set.
|
|
41
|
-
*
|
|
42
|
-
* This embeds a bounded window of admitted nodes and asks the vector index what
|
|
43
|
-
* each is near. It lives here rather than in an HTTP handler because that is
|
|
44
|
-
* where the embedder is — a door that embeds is a door that has opinions about
|
|
45
|
-
* what memory means.
|
|
46
|
-
*/
|
|
47
|
-
export declare function readGraphSimilarityEdges(adapter: DatabaseAdapter, scopes: readonly GraphScope[], options?: {
|
|
48
|
-
window?: number;
|
|
49
|
-
neighbors?: number;
|
|
50
|
-
threshold?: number;
|
|
51
|
-
}): Promise<GraphSimilarityEdge[]>;
|
|
52
36
|
//# sourceMappingURL=graph-read.d.ts.map
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.readGraphNodes = readGraphNodes;
|
|
4
4
|
exports.readGraphEdges = readGraphEdges;
|
|
5
5
|
exports.countGraphNodes = countGraphNodes;
|
|
6
|
-
exports.readGraphSimilarityEdges = readGraphSimilarityEdges;
|
|
7
6
|
/**
|
|
8
7
|
* The decision graph, as a caller may see it.
|
|
9
8
|
*
|
|
@@ -14,8 +13,6 @@ exports.readGraphSimilarityEdges = readGraphSimilarityEdges;
|
|
|
14
13
|
* A principal that admits nothing sees an empty graph rather than the file.
|
|
15
14
|
*/
|
|
16
15
|
const db_manager_js_1 = require("../db-manager.js");
|
|
17
|
-
const embedder_js_1 = require("../embedding/embedder.js");
|
|
18
|
-
const search_js_1 = require("../knowledge/search.js");
|
|
19
16
|
/** The admitted-node clause both a node read and an edge read stand on. */
|
|
20
17
|
async function admittedScopeIds(adapter, scopes) {
|
|
21
18
|
return Promise.all(scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(adapter, scope.kind, scope.id)));
|
|
@@ -48,9 +45,18 @@ async function readGraphEdges(adapter, scopes) {
|
|
|
48
45
|
const placeholders = scopeIds.map(() => '?').join(', ');
|
|
49
46
|
// Both ends must be admitted: an edge to a memory this caller cannot read
|
|
50
47
|
// would disclose that it exists, which is the read the join is here to bound.
|
|
48
|
+
// Links between memories live in twin_edges; decision_edges holds the rows
|
|
49
|
+
// written before links moved there.
|
|
51
50
|
const rows = (await adapter
|
|
52
51
|
.prepare(`SELECT e.from_id, e.to_id, e.relationship, e.reason
|
|
53
|
-
FROM
|
|
52
|
+
FROM (
|
|
53
|
+
SELECT from_id, to_id, relationship, reason FROM decision_edges
|
|
54
|
+
UNION ALL
|
|
55
|
+
SELECT subject_id, object_id, edge_type,
|
|
56
|
+
COALESCE(json_extract(relation_attrs_json, '$.reason'), reason_text)
|
|
57
|
+
FROM twin_edges
|
|
58
|
+
WHERE subject_kind = 'memory' AND object_kind = 'memory' AND edge_type <> 'derived_from'
|
|
59
|
+
) e
|
|
54
60
|
WHERE EXISTS (
|
|
55
61
|
SELECT 1 FROM memory_scope_bindings b
|
|
56
62
|
WHERE b.memory_id = e.from_id AND b.scope_id IN (${placeholders})
|
|
@@ -80,51 +86,4 @@ async function countGraphNodes(adapter, scopes) {
|
|
|
80
86
|
.get(...scopeIds));
|
|
81
87
|
return row?.count ?? 0;
|
|
82
88
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Similarity edges over the admitted set.
|
|
85
|
-
*
|
|
86
|
-
* This embeds a bounded window of admitted nodes and asks the vector index what
|
|
87
|
-
* each is near. It lives here rather than in an HTTP handler because that is
|
|
88
|
-
* where the embedder is — a door that embeds is a door that has opinions about
|
|
89
|
-
* what memory means.
|
|
90
|
-
*/
|
|
91
|
-
async function readGraphSimilarityEdges(adapter, scopes, options = {}) {
|
|
92
|
-
const window = options.window ?? 50;
|
|
93
|
-
const neighbors = options.neighbors ?? 3;
|
|
94
|
-
const threshold = options.threshold ?? 0.7;
|
|
95
|
-
const nodes = await readGraphNodes(adapter, scopes, { limit: Math.max(window * 2, window) });
|
|
96
|
-
if (nodes.length < 2) {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
const admitted = new Set(nodes.map((node) => node.id));
|
|
100
|
-
const seen = new Set();
|
|
101
|
-
const edges = [];
|
|
102
|
-
for (const node of nodes.slice(0, window)) {
|
|
103
|
-
const embedding = await (0, embedder_js_1.generateEmbedding)(`${node.topic} ${node.decision}`, 'query');
|
|
104
|
-
const similar = (await (0, search_js_1.vectorSearch)(adapter, embedding, neighbors, threshold));
|
|
105
|
-
for (const match of similar) {
|
|
106
|
-
// A neighbour the caller may not read is not an edge it may see.
|
|
107
|
-
if (match.id === node.id || !admitted.has(match.id)) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const similarity = match.similarity ?? threshold;
|
|
111
|
-
if (similarity <= threshold) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
const key = [node.id, match.id].sort().join('|');
|
|
115
|
-
if (seen.has(key)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
seen.add(key);
|
|
119
|
-
edges.push({
|
|
120
|
-
from: node.id,
|
|
121
|
-
to: match.id,
|
|
122
|
-
relationship: 'similar',
|
|
123
|
-
reason: null,
|
|
124
|
-
similarity,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return edges;
|
|
129
|
-
}
|
|
130
89
|
//# sourceMappingURL=graph-read.js.map
|
|
@@ -87,8 +87,7 @@ export interface JudgmentRecordFields {
|
|
|
87
87
|
refinedFrom?: string[] | null;
|
|
88
88
|
/**
|
|
89
89
|
* Legacy `decisions.supersedes` column for records whose predecessor was
|
|
90
|
-
* declared
|
|
91
|
-
* scope-checked `replaces` command field.
|
|
90
|
+
* declared before the scope-checked `replaces` command field.
|
|
92
91
|
*/
|
|
93
92
|
supersedes?: string | null;
|
|
94
93
|
}
|
|
@@ -125,24 +124,6 @@ export interface JudgmentEventMeta {
|
|
|
125
124
|
* command itself; they mirror what the legacy writers used to persist inline.
|
|
126
125
|
*/
|
|
127
126
|
export interface JudgmentProjections {
|
|
128
|
-
/** Legacy decision_edges rows kept in sync for recall readers. `fromId`
|
|
129
|
-
* defaults to the appended record; amendment commands may name another row. */
|
|
130
|
-
decisionEdges?: Array<{
|
|
131
|
-
fromId?: string;
|
|
132
|
-
targetId: string;
|
|
133
|
-
relationship: string;
|
|
134
|
-
reason?: string | null;
|
|
135
|
-
weight?: number;
|
|
136
|
-
createdBy?: string;
|
|
137
|
-
approvedByUser?: number | null;
|
|
138
|
-
}>;
|
|
139
|
-
/**
|
|
140
|
-
* Rows the command marks as superseded by the appended record. This mirrors
|
|
141
|
-
* the legacy save surface where an unsigned caller's explicit `supersedes`
|
|
142
|
-
* relationship moved the named target out of current truth; the command
|
|
143
|
-
* boundary keeps `replaces` for scope-admitted supersession instead.
|
|
144
|
-
*/
|
|
145
|
-
supersedeTargets?: string[];
|
|
146
127
|
/** Registry record identity binding (item + actors). */
|
|
147
128
|
recordIdentity?: {
|
|
148
129
|
itemId?: string | null;
|
package/dist/memory/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RecordActor } from '../registry/types.js';
|
|
2
|
+
import type { DecisionCorrection } from './decision-links.js';
|
|
2
3
|
import type { RecordLink } from './judgment-types.js';
|
|
3
4
|
import type { SearchHitDiagnostics, SearchQualityOptions, SearchStrictness } from '../knowledge/search-quality.js';
|
|
4
5
|
/**
|
|
@@ -78,8 +79,20 @@ export interface MemoryRecord {
|
|
|
78
79
|
event_datetime?: number | null;
|
|
79
80
|
/** Maintained outcome projection (SUCCESS | FAILED | PARTIAL | pending). Null when unset. */
|
|
80
81
|
outcome?: string | null;
|
|
82
|
+
/** On a record search expansion added: the hit it came from and the link it followed there. */
|
|
83
|
+
reached_through?: MemoryReachedThrough;
|
|
81
84
|
retrieval_diagnostics?: SearchHitDiagnostics;
|
|
82
85
|
}
|
|
86
|
+
export interface MemoryReachedThrough {
|
|
87
|
+
/** The search hit the link starts from. */
|
|
88
|
+
from: string;
|
|
89
|
+
/** The link's relation as seen from that hit (`builds_on`, `built_on_by`, `supersedes_chain`, ...). */
|
|
90
|
+
relation: string;
|
|
91
|
+
/** The reason the agent gave for the link; null for the supersedes chain. */
|
|
92
|
+
reason: string | null;
|
|
93
|
+
/** Later links that contradict this one, each with its reason. */
|
|
94
|
+
corrected_by?: DecisionCorrection[];
|
|
95
|
+
}
|
|
83
96
|
export type RecallMemoryOptions = SearchQualityOptions & {
|
|
84
97
|
kind?: MemoryKindFilter;
|
|
85
98
|
scopes?: MemoryScopeRef[];
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { DatabaseAdapter, DatabaseInstance, DecisionInput } from '../db-manager.js';
|
|
11
11
|
import type { JudgmentAccess } from '../knowledge/judgments.js';
|
|
12
|
-
import type {
|
|
12
|
+
import type { JsonValue } from './judgment-types.js';
|
|
13
13
|
import type { MemoryScopeRef } from './types.js';
|
|
14
14
|
import type { NormalizedMemoryProvenance } from './provenance.js';
|
|
15
15
|
/** Access for a caller with no signed identity: it is admitted to exactly the
|
|
@@ -25,44 +25,6 @@ export declare function writeAccessForProvenance(provenance: NormalizedMemoryPro
|
|
|
25
25
|
/** Scope refs currently bound to a memory row — the partition a write into
|
|
26
26
|
* that row must be admitted to. */
|
|
27
27
|
export declare function boundScopesOf(adapter: Pick<DatabaseAdapter, 'prepare'>, memoryId: string): MemoryScopeRef[];
|
|
28
|
-
/**
|
|
29
|
-
* Fail-first visibility check for explicitly authored relationship targets,
|
|
30
|
-
* preserving the legacy error contract.
|
|
31
|
-
*
|
|
32
|
-
* Trusted envelopes check visibility strictly inside admitted scopes (unbound
|
|
33
|
-
* legacy rows are not reachable — same as the pre-boundary envelope check).
|
|
34
|
-
* Unsigned callers keep the legacy existence check for the original error
|
|
35
|
-
* text; scope admission is then enforced by the command boundary itself.
|
|
36
|
-
*/
|
|
37
|
-
export declare function assertRelationshipTargetsVisible(adapter: Pick<DatabaseAdapter, 'prepare'>, targetIds: readonly string[], admittedScopes: readonly MemoryScopeRef[], trustedEnvelope: boolean): void;
|
|
38
|
-
export interface ExplicitRelationship {
|
|
39
|
-
type: string;
|
|
40
|
-
targetId: string;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Map deduplicated legacy relationships onto command fields.
|
|
44
|
-
*
|
|
45
|
-
* Trusted envelopes get the authored form: `supersedes` targets become
|
|
46
|
-
* scope-checked `replaces`, and link-able relations also enter the twin edge
|
|
47
|
-
* graph as explicit `links` — both fail closed inside the boundary.
|
|
48
|
-
*
|
|
49
|
-
* Unsigned callers keep the legacy contract: the adapter already checked that
|
|
50
|
-
* each target exists, so relationships are written only as `decision_edges`
|
|
51
|
-
* projection rows and `supersedes` targets are moved out of current truth
|
|
52
|
-
* through the `supersedeTargets` projection — never as scope-checked authored
|
|
53
|
-
* links the caller was not admitted to.
|
|
54
|
-
*/
|
|
55
|
-
export declare function relationshipsToCommandFields(relationships: readonly ExplicitRelationship[], options?: {
|
|
56
|
-
trusted?: boolean;
|
|
57
|
-
}): {
|
|
58
|
-
links: RecordLink[];
|
|
59
|
-
replaces: Array<{
|
|
60
|
-
id: string;
|
|
61
|
-
reason: string;
|
|
62
|
-
}>;
|
|
63
|
-
decisionEdges: NonNullable<JudgmentProjections['decisionEdges']>;
|
|
64
|
-
supersedeTargets: string[];
|
|
65
|
-
};
|
|
66
28
|
/**
|
|
67
29
|
* Embedder for the command boundary that preserves the legacy enhanced
|
|
68
30
|
* embedding input.
|
|
@@ -15,8 +15,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.unsignedWriteAccess = unsignedWriteAccess;
|
|
16
16
|
exports.writeAccessForProvenance = writeAccessForProvenance;
|
|
17
17
|
exports.boundScopesOf = boundScopesOf;
|
|
18
|
-
exports.assertRelationshipTargetsVisible = assertRelationshipTargetsVisible;
|
|
19
|
-
exports.relationshipsToCommandFields = relationshipsToCommandFields;
|
|
20
18
|
exports.commandEmbedder = commandEmbedder;
|
|
21
19
|
exports.appendOutcomeAmendment = appendOutcomeAmendment;
|
|
22
20
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
@@ -69,102 +67,6 @@ function boundScopesOf(adapter, memoryId) {
|
|
|
69
67
|
return { kind: record.kind, id: record.external_id };
|
|
70
68
|
});
|
|
71
69
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Fail-first visibility check for explicitly authored relationship targets,
|
|
74
|
-
* preserving the legacy error contract.
|
|
75
|
-
*
|
|
76
|
-
* Trusted envelopes check visibility strictly inside admitted scopes (unbound
|
|
77
|
-
* legacy rows are not reachable — same as the pre-boundary envelope check).
|
|
78
|
-
* Unsigned callers keep the legacy existence check for the original error
|
|
79
|
-
* text; scope admission is then enforced by the command boundary itself.
|
|
80
|
-
*/
|
|
81
|
-
function assertRelationshipTargetsVisible(adapter, targetIds, admittedScopes, trustedEnvelope) {
|
|
82
|
-
for (const targetId of targetIds) {
|
|
83
|
-
if (trustedEnvelope) {
|
|
84
|
-
const scopeIds = admittedScopes.map((scope) => (0, db_manager_js_1.buildMemoryScopeId)(scope.kind, scope.id));
|
|
85
|
-
const placeholders = scopeIds.map(() => '?').join(', ');
|
|
86
|
-
const visible = scopeIds.length > 0 &&
|
|
87
|
-
adapter
|
|
88
|
-
.prepare(`SELECT 1 FROM decisions d
|
|
89
|
-
JOIN memory_scope_bindings b ON b.memory_id = d.id
|
|
90
|
-
WHERE d.id = ? AND b.scope_id IN (${placeholders}) LIMIT 1`)
|
|
91
|
-
.get(targetId, ...scopeIds) !== undefined;
|
|
92
|
-
if (!visible) {
|
|
93
|
-
const denied = new Error('Relationship target is unavailable');
|
|
94
|
-
denied.code = 'relationship_target_unavailable';
|
|
95
|
-
throw denied;
|
|
96
|
-
}
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const target = adapter.prepare('SELECT id FROM decisions WHERE id = ?').get(targetId);
|
|
100
|
-
if (!target) {
|
|
101
|
-
throw new Error(`mama.save() relationship target does not exist: ${targetId}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const LINK_RELATIONS = new Set([
|
|
106
|
-
'supersedes',
|
|
107
|
-
'refines',
|
|
108
|
-
'contradicts',
|
|
109
|
-
'mentions',
|
|
110
|
-
'derived_from',
|
|
111
|
-
'builds_on',
|
|
112
|
-
'debates',
|
|
113
|
-
'synthesizes',
|
|
114
|
-
'blocks',
|
|
115
|
-
'next_action_for',
|
|
116
|
-
'case_member',
|
|
117
|
-
]);
|
|
118
|
-
/**
|
|
119
|
-
* Map deduplicated legacy relationships onto command fields.
|
|
120
|
-
*
|
|
121
|
-
* Trusted envelopes get the authored form: `supersedes` targets become
|
|
122
|
-
* scope-checked `replaces`, and link-able relations also enter the twin edge
|
|
123
|
-
* graph as explicit `links` — both fail closed inside the boundary.
|
|
124
|
-
*
|
|
125
|
-
* Unsigned callers keep the legacy contract: the adapter already checked that
|
|
126
|
-
* each target exists, so relationships are written only as `decision_edges`
|
|
127
|
-
* projection rows and `supersedes` targets are moved out of current truth
|
|
128
|
-
* through the `supersedeTargets` projection — never as scope-checked authored
|
|
129
|
-
* links the caller was not admitted to.
|
|
130
|
-
*/
|
|
131
|
-
function relationshipsToCommandFields(relationships, options) {
|
|
132
|
-
const trusted = options?.trusted === true;
|
|
133
|
-
const links = [];
|
|
134
|
-
const replaces = [];
|
|
135
|
-
const supersedeTargets = [];
|
|
136
|
-
const decisionEdges = [];
|
|
137
|
-
const seen = new Set();
|
|
138
|
-
for (const relationship of relationships) {
|
|
139
|
-
const key = `${relationship.type}:${relationship.targetId}`;
|
|
140
|
-
if (seen.has(key))
|
|
141
|
-
continue;
|
|
142
|
-
seen.add(key);
|
|
143
|
-
const reason = `Explicit ${relationship.type} reference in reasoning`;
|
|
144
|
-
decisionEdges.push({
|
|
145
|
-
targetId: relationship.targetId,
|
|
146
|
-
relationship: relationship.type,
|
|
147
|
-
reason,
|
|
148
|
-
weight: 1,
|
|
149
|
-
});
|
|
150
|
-
if (relationship.type === 'supersedes') {
|
|
151
|
-
if (trusted) {
|
|
152
|
-
replaces.push({ id: relationship.targetId, reason });
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
supersedeTargets.push(relationship.targetId);
|
|
156
|
-
}
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (trusted && LINK_RELATIONS.has(relationship.type)) {
|
|
160
|
-
links.push({
|
|
161
|
-
relation: relationship.type,
|
|
162
|
-
target: { kind: 'memory', id: relationship.targetId },
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return { links, replaces, decisionEdges, supersedeTargets };
|
|
167
|
-
}
|
|
168
70
|
/**
|
|
169
71
|
* Embedder for the command boundary that preserves the legacy enhanced
|
|
170
72
|
* embedding input.
|
package/package.json
CHANGED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { DatabaseAdapter } from '../db-manager.js';
|
|
2
|
-
/** Link every stored commitment revision to the revision it follows. */
|
|
3
|
-
export declare function backfillCommitmentRevisionGraph(adapter: DatabaseAdapter): void;
|
|
4
|
-
//# sourceMappingURL=commitment-revision-migration.d.ts.map
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.backfillCommitmentRevisionGraph = backfillCommitmentRevisionGraph;
|
|
4
|
-
const judgment_edge_js_1 = require("./judgment-edge.js");
|
|
5
|
-
/** Link every stored commitment revision to the revision it follows. */
|
|
6
|
-
function backfillCommitmentRevisionGraph(adapter) {
|
|
7
|
-
const assignments = adapter
|
|
8
|
-
.prepare(`SELECT assignment.commitment_id, assignment.revision, assignment.record_id, assignment.operation,
|
|
9
|
-
assignment.agent_id, assignment.model_run_id, assignment.created_at
|
|
10
|
-
FROM commitment_assignments AS assignment
|
|
11
|
-
JOIN commitments AS commitment ON commitment.commitment_id = assignment.commitment_id
|
|
12
|
-
ORDER BY assignment.commitment_id, assignment.revision`)
|
|
13
|
-
.all();
|
|
14
|
-
if (assignments.length === 0)
|
|
15
|
-
return;
|
|
16
|
-
const byCommitment = new Map();
|
|
17
|
-
for (const assignment of assignments) {
|
|
18
|
-
const rows = byCommitment.get(assignment.commitment_id) ?? [];
|
|
19
|
-
rows.push(assignment);
|
|
20
|
-
byCommitment.set(assignment.commitment_id, rows);
|
|
21
|
-
}
|
|
22
|
-
const hasEdge = adapter.prepare(`SELECT 1 FROM twin_edges
|
|
23
|
-
WHERE edge_type = 'builds_on'
|
|
24
|
-
AND subject_kind = 'memory' AND subject_id = ?
|
|
25
|
-
AND object_kind = 'memory' AND object_id = ?`);
|
|
26
|
-
const insertEdge = adapter.prepare(`INSERT OR IGNORE INTO twin_edges (
|
|
27
|
-
edge_id, edge_type, subject_kind, subject_id, object_kind, object_id,
|
|
28
|
-
relation_attrs_json, confidence, source, agent_id, model_run_id, content_hash, created_at
|
|
29
|
-
) VALUES (?, 'builds_on', 'memory', ?, 'memory', ?, '{}', 1.0, 'code', ?, ?, ?, ?)`);
|
|
30
|
-
for (const [commitmentId, revisions] of byCommitment) {
|
|
31
|
-
for (let index = 0; index < revisions.length; index += 1) {
|
|
32
|
-
const current = revisions[index];
|
|
33
|
-
if (current.operation === 'create')
|
|
34
|
-
continue;
|
|
35
|
-
const previous = revisions[index - 1];
|
|
36
|
-
if (!previous) {
|
|
37
|
-
throw new Error(`Commitment ${commitmentId} revision ${current.revision} has no predecessor`);
|
|
38
|
-
}
|
|
39
|
-
if (hasEdge.get(current.record_id, previous.record_id))
|
|
40
|
-
continue;
|
|
41
|
-
const link = {
|
|
42
|
-
relation: 'builds_on',
|
|
43
|
-
target: { kind: 'memory', id: previous.record_id },
|
|
44
|
-
};
|
|
45
|
-
const id = (0, judgment_edge_js_1.judgmentEdgeId)(`migration:099:${commitmentId}:${current.revision}`, 0, link.relation, link.target);
|
|
46
|
-
insertEdge.run(id, current.record_id, previous.record_id, current.agent_id, current.model_run_id, (0, judgment_edge_js_1.judgmentEdgeContentHash)(id, current.record_id, link), current.created_at);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
//# sourceMappingURL=commitment-revision-migration.js.map
|