@h-rig/memory-plugin 0.0.6-alpha.156
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 +1 -0
- package/dist/src/cli.d.ts +14 -0
- package/dist/src/cli.js +1500 -0
- package/dist/src/db.d.ts +8 -0
- package/dist/src/db.js +751 -0
- package/dist/src/embed.d.ts +22 -0
- package/dist/src/embed.js +284 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +1628 -0
- package/dist/src/plugin.d.ts +4 -0
- package/dist/src/plugin.js +1579 -0
- package/dist/src/query.d.ts +12 -0
- package/dist/src/query.js +291 -0
- package/dist/src/read.d.ts +12 -0
- package/dist/src/read.js +314 -0
- package/dist/src/service.d.ts +14 -0
- package/dist/src/service.js +1511 -0
- package/dist/src/write.d.ts +27 -0
- package/dist/src/write.js +1033 -0
- package/package.json +36 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MemoryDb, MemoryEmbedder, MemoryQueryResult } from "@rig/contracts";
|
|
2
|
+
import { type RuntimeMemoryRetrievalConfig } from "@rig/runtime/control-plane/runtime/context";
|
|
3
|
+
export type { MemoryQueryResult };
|
|
4
|
+
export type MemoryQueryInput = {
|
|
5
|
+
query: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
embedder?: MemoryEmbedder;
|
|
8
|
+
retrieval?: RuntimeMemoryRetrievalConfig;
|
|
9
|
+
now?: string | Date;
|
|
10
|
+
};
|
|
11
|
+
export declare function queryRelevantMemory(db: MemoryDb, input: MemoryQueryInput): Promise<MemoryQueryResult[]>;
|
|
12
|
+
export declare function formatMemoryQueryResults(results: MemoryQueryResult[]): string;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/memory-plugin/src/embed.ts
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
|
|
5
|
+
// packages/memory-plugin/src/db.ts
|
|
6
|
+
import {
|
|
7
|
+
NO_MATCH_RETRIEVAL_CANONICAL_KEY
|
|
8
|
+
} from "@rig/contracts";
|
|
9
|
+
function parseJsonRecord(value) {
|
|
10
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return JSON.parse(value);
|
|
14
|
+
}
|
|
15
|
+
function parseEmbedding(value) {
|
|
16
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(value);
|
|
20
|
+
}
|
|
21
|
+
function rowString(row, key) {
|
|
22
|
+
const value = row[key];
|
|
23
|
+
return value == null ? null : String(value);
|
|
24
|
+
}
|
|
25
|
+
function toMemoryItemRow(row) {
|
|
26
|
+
return {
|
|
27
|
+
canonicalKey: String(row.canonical_key),
|
|
28
|
+
summary: String(row.summary),
|
|
29
|
+
kind: rowString(row, "kind"),
|
|
30
|
+
category: rowString(row, "category"),
|
|
31
|
+
status: String(row.status),
|
|
32
|
+
confidence: Number(row.confidence),
|
|
33
|
+
sourceRunId: rowString(row, "source_run_id"),
|
|
34
|
+
sourceTaskId: rowString(row, "source_task_id"),
|
|
35
|
+
branch: rowString(row, "branch"),
|
|
36
|
+
details: parseJsonRecord(row.details_json),
|
|
37
|
+
createdAt: String(row.created_at),
|
|
38
|
+
updatedAt: String(row.updated_at),
|
|
39
|
+
lastEventId: String(row.last_event_id),
|
|
40
|
+
supersededBy: rowString(row, "superseded_by"),
|
|
41
|
+
embedding: parseEmbedding(row.embedding)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function listActiveMemoryItems(db) {
|
|
45
|
+
const result = await db.client.execute(`
|
|
46
|
+
SELECT
|
|
47
|
+
canonical_key,
|
|
48
|
+
summary,
|
|
49
|
+
kind,
|
|
50
|
+
category,
|
|
51
|
+
status,
|
|
52
|
+
confidence,
|
|
53
|
+
source_run_id,
|
|
54
|
+
source_task_id,
|
|
55
|
+
branch,
|
|
56
|
+
details_json,
|
|
57
|
+
created_at,
|
|
58
|
+
updated_at,
|
|
59
|
+
last_event_id,
|
|
60
|
+
superseded_by,
|
|
61
|
+
embedding
|
|
62
|
+
FROM memory_items
|
|
63
|
+
WHERE status = 'active'
|
|
64
|
+
ORDER BY canonical_key
|
|
65
|
+
`);
|
|
66
|
+
return result.rows.map((row) => toMemoryItemRow(row));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// packages/memory-plugin/src/embed.ts
|
|
70
|
+
var DEFAULT_EMBEDDING_API_BASE_URL = "https://api.openai.com/v1";
|
|
71
|
+
var DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
|
|
72
|
+
var DETERMINISTIC_EMBEDDER_MODE = "deterministic";
|
|
73
|
+
var DETERMINISTIC_VECTOR_DIMS = 32;
|
|
74
|
+
function tokenizeForDeterministicEmbedding(text) {
|
|
75
|
+
const tokens = text.toLowerCase().match(/[a-z0-9_./:-]+/g) ?? [];
|
|
76
|
+
return tokens.length > 0 ? tokens : [text.toLowerCase()];
|
|
77
|
+
}
|
|
78
|
+
function deterministicTokenVector(token) {
|
|
79
|
+
const hash = createHash("sha256").update(token).digest();
|
|
80
|
+
const vector = new Array(DETERMINISTIC_VECTOR_DIMS).fill(0);
|
|
81
|
+
for (let index = 0;index < 8; index += 1) {
|
|
82
|
+
const slot = (hash[index] ?? 0) % DETERMINISTIC_VECTOR_DIMS;
|
|
83
|
+
const sign = ((hash[index + 8] ?? 0) & 1) === 0 ? 1 : -1;
|
|
84
|
+
const magnitude = ((hash[index + 16] ?? 0) + 1) / 256;
|
|
85
|
+
vector[slot] = (vector[slot] ?? 0) + sign * magnitude;
|
|
86
|
+
}
|
|
87
|
+
return vector;
|
|
88
|
+
}
|
|
89
|
+
function normalizeVector(vector) {
|
|
90
|
+
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
|
|
91
|
+
if (norm === 0) {
|
|
92
|
+
return vector.map(() => 0);
|
|
93
|
+
}
|
|
94
|
+
return vector.map((value) => Number((value / norm).toFixed(6)));
|
|
95
|
+
}
|
|
96
|
+
function createDeterministicMemoryEmbedder() {
|
|
97
|
+
return {
|
|
98
|
+
async embed(texts) {
|
|
99
|
+
return texts.map((text) => {
|
|
100
|
+
const vector = new Array(DETERMINISTIC_VECTOR_DIMS).fill(0);
|
|
101
|
+
for (const token of tokenizeForDeterministicEmbedding(text)) {
|
|
102
|
+
const tokenVector = deterministicTokenVector(token);
|
|
103
|
+
for (let index = 0;index < tokenVector.length; index += 1) {
|
|
104
|
+
vector[index] = (vector[index] ?? 0) + (tokenVector[index] ?? 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return normalizeVector(vector);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function embeddingResponseMessage(payload, status) {
|
|
113
|
+
if (typeof payload === "object" && payload !== null) {
|
|
114
|
+
const maybeMessage = payload.error?.message;
|
|
115
|
+
if (maybeMessage) {
|
|
116
|
+
return maybeMessage;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return `memory embedding request failed with status ${status}`;
|
|
120
|
+
}
|
|
121
|
+
function createOpenAiMemoryEmbedder(options) {
|
|
122
|
+
const apiKey = options.apiKey.trim();
|
|
123
|
+
const model = options.model?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
124
|
+
const apiBaseUrl = (options.apiBaseUrl?.trim() || DEFAULT_EMBEDDING_API_BASE_URL).replace(/\/+$/, "");
|
|
125
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
126
|
+
if (!apiKey) {
|
|
127
|
+
throw new Error("memory embedding api key must be non-empty");
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
async embed(texts) {
|
|
131
|
+
if (texts.length === 0) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const response = await fetchImpl(`${apiBaseUrl}/embeddings`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: {
|
|
137
|
+
authorization: `Bearer ${apiKey}`,
|
|
138
|
+
"content-type": "application/json"
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
input: texts,
|
|
142
|
+
model
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
const payload = await response.json();
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw new Error(embeddingResponseMessage(payload, response.status));
|
|
148
|
+
}
|
|
149
|
+
if (!Array.isArray(payload.data) || payload.data.length !== texts.length) {
|
|
150
|
+
throw new Error(`memory embedding provider returned ${payload.data?.length ?? 0} embeddings for ${texts.length} inputs`);
|
|
151
|
+
}
|
|
152
|
+
const byIndex = new Map;
|
|
153
|
+
for (const row of payload.data) {
|
|
154
|
+
if (typeof row.index !== "number" || !Array.isArray(row.embedding)) {
|
|
155
|
+
throw new Error("memory embedding provider returned an invalid response payload");
|
|
156
|
+
}
|
|
157
|
+
byIndex.set(row.index, row.embedding);
|
|
158
|
+
}
|
|
159
|
+
return texts.map((_text, index) => {
|
|
160
|
+
const embedding = byIndex.get(index);
|
|
161
|
+
if (!embedding) {
|
|
162
|
+
throw new Error(`memory embedding provider omitted embedding ${index}`);
|
|
163
|
+
}
|
|
164
|
+
return embedding;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function createConfiguredMemoryEmbedder(options = {}) {
|
|
170
|
+
const env = options.env ?? process.env;
|
|
171
|
+
const mode = env.RIG_MEMORY_EMBEDDER?.trim();
|
|
172
|
+
if (mode === DETERMINISTIC_EMBEDDER_MODE) {
|
|
173
|
+
return createDeterministicMemoryEmbedder();
|
|
174
|
+
}
|
|
175
|
+
const apiKey = env.OPENAI_API_KEY?.trim();
|
|
176
|
+
if (!apiKey) {
|
|
177
|
+
throw new Error("memory embeddings require OPENAI_API_KEY or RIG_MEMORY_EMBEDDER=deterministic");
|
|
178
|
+
}
|
|
179
|
+
return createOpenAiMemoryEmbedder({
|
|
180
|
+
apiKey,
|
|
181
|
+
model: env.RIG_MEMORY_EMBEDDING_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL,
|
|
182
|
+
apiBaseUrl: options.apiBaseUrl ?? env.RIG_MEMORY_EMBEDDING_API_BASE_URL?.trim() ?? DEFAULT_EMBEDDING_API_BASE_URL,
|
|
183
|
+
fetchImpl: options.fetchImpl
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// packages/memory-plugin/src/query.ts
|
|
188
|
+
import {
|
|
189
|
+
DEFAULT_RUNTIME_MEMORY_RETRIEVAL
|
|
190
|
+
} from "@rig/runtime/control-plane/runtime/context";
|
|
191
|
+
var DEFAULT_RESULT_LIMIT = DEFAULT_RUNTIME_MEMORY_RETRIEVAL.topK;
|
|
192
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
193
|
+
var MIN_VECTOR_MATCH_SCORE = 0.2;
|
|
194
|
+
function tokenize(text) {
|
|
195
|
+
return (text.toLowerCase().match(/[a-z0-9_./:-]+/g) ?? []).flatMap((token) => {
|
|
196
|
+
const split = token.split(/[./:_-]+/).filter(Boolean);
|
|
197
|
+
return split.length > 0 ? [token, ...split] : [token];
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function lexicalScore(query, item) {
|
|
201
|
+
const queryTokens = [...new Set(tokenize(query))];
|
|
202
|
+
if (queryTokens.length === 0) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
const haystackTokens = new Set(tokenize(item.summary));
|
|
206
|
+
let matched = 0;
|
|
207
|
+
for (const token of queryTokens) {
|
|
208
|
+
if (haystackTokens.has(token)) {
|
|
209
|
+
matched += 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return matched / queryTokens.length;
|
|
213
|
+
}
|
|
214
|
+
function cosineSimilarity(left, right) {
|
|
215
|
+
if (left.length === 0 || left.length !== right.length) {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
let dot = 0;
|
|
219
|
+
let leftNorm = 0;
|
|
220
|
+
let rightNorm = 0;
|
|
221
|
+
for (let index = 0;index < left.length; index += 1) {
|
|
222
|
+
const l = left[index] ?? 0;
|
|
223
|
+
const r = right[index] ?? 0;
|
|
224
|
+
dot += l * r;
|
|
225
|
+
leftNorm += l * l;
|
|
226
|
+
rightNorm += r * r;
|
|
227
|
+
}
|
|
228
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
return dot / Math.sqrt(leftNorm * rightNorm);
|
|
232
|
+
}
|
|
233
|
+
function recencyScore(item, now) {
|
|
234
|
+
const updatedAt = Date.parse(item.updatedAt);
|
|
235
|
+
if (Number.isNaN(updatedAt)) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
const ageMs = Math.max(0, now.getTime() - updatedAt);
|
|
239
|
+
return 1 / (1 + ageMs / DAY_MS);
|
|
240
|
+
}
|
|
241
|
+
async function maybeEmbedQuery(items, query, embedder) {
|
|
242
|
+
if (!items.some((item) => Array.isArray(item.embedding) && item.embedding.length > 0)) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const resolvedEmbedder = embedder ?? createConfiguredMemoryEmbedder();
|
|
247
|
+
const [queryEmbedding] = await resolvedEmbedder.embed([query]);
|
|
248
|
+
return queryEmbedding ?? null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function queryRelevantMemory(db, input) {
|
|
254
|
+
const retrieval = input.retrieval ?? DEFAULT_RUNTIME_MEMORY_RETRIEVAL;
|
|
255
|
+
const now = input.now instanceof Date ? input.now : new Date(input.now ?? Date.now());
|
|
256
|
+
const items = await listActiveMemoryItems(db);
|
|
257
|
+
if (items.length === 0) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
const queryEmbedding = await maybeEmbedQuery(items, input.query, input.embedder);
|
|
261
|
+
const scored = items.map((item) => {
|
|
262
|
+
const lexical = lexicalScore(input.query, item);
|
|
263
|
+
const vector = queryEmbedding && item.embedding ? cosineSimilarity(queryEmbedding, item.embedding) : 0;
|
|
264
|
+
const recency = recencyScore(item, now);
|
|
265
|
+
const score = retrieval.lexicalWeight * lexical + retrieval.vectorWeight * vector + retrieval.recencyWeight * recency + retrieval.confidenceWeight * item.confidence;
|
|
266
|
+
return {
|
|
267
|
+
canonicalKey: item.canonicalKey,
|
|
268
|
+
summary: item.summary,
|
|
269
|
+
kind: item.kind,
|
|
270
|
+
category: item.category,
|
|
271
|
+
confidence: item.confidence,
|
|
272
|
+
updatedAt: item.updatedAt,
|
|
273
|
+
score,
|
|
274
|
+
lexicalScore: lexical,
|
|
275
|
+
vectorScore: vector,
|
|
276
|
+
recencyScore: recency
|
|
277
|
+
};
|
|
278
|
+
}).filter((item) => item.lexicalScore > 0 || item.vectorScore >= MIN_VECTOR_MATCH_SCORE).sort((left, right) => right.score - left.score || Date.parse(right.updatedAt) - Date.parse(left.updatedAt) || right.confidence - left.confidence || left.canonicalKey.localeCompare(right.canonicalKey));
|
|
279
|
+
return scored.slice(0, input.limit ?? retrieval.topK ?? DEFAULT_RESULT_LIMIT);
|
|
280
|
+
}
|
|
281
|
+
function formatMemoryQueryResults(results) {
|
|
282
|
+
if (results.length === 0) {
|
|
283
|
+
return "No shared memories matched.";
|
|
284
|
+
}
|
|
285
|
+
return results.map((result, index) => `${index + 1}. [${result.canonicalKey}] ${result.summary} (confidence ${result.confidence.toFixed(2)})`).join(`
|
|
286
|
+
`);
|
|
287
|
+
}
|
|
288
|
+
export {
|
|
289
|
+
queryRelevantMemory,
|
|
290
|
+
formatMemoryQueryResults
|
|
291
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { nativeFetchRef, nativeReadBlobBytesAtRef } from "@rig/runtime/control-plane/native/git-native";
|
|
2
|
+
import { openMemoryDb } from "./db";
|
|
3
|
+
import type { CanonicalMemorySnapshot } from "@rig/contracts";
|
|
4
|
+
export type { CanonicalMemorySnapshot };
|
|
5
|
+
type CanonicalMemoryReadDeps = {
|
|
6
|
+
fetchRef: typeof nativeFetchRef;
|
|
7
|
+
readBlobBytesAtRef: typeof nativeReadBlobBytesAtRef;
|
|
8
|
+
openMemoryDb: typeof openMemoryDb;
|
|
9
|
+
makeTempDir: () => string;
|
|
10
|
+
removeDir: (path: string) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function readCanonicalMemoryDb(projectRoot: string, deps?: Partial<CanonicalMemoryReadDeps>): Promise<CanonicalMemorySnapshot>;
|
package/dist/src/read.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/memory-plugin/src/read.ts
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { nativeFetchRef, nativeReadBlobBytesAtRef } from "@rig/runtime/control-plane/native/git-native";
|
|
7
|
+
import { resolveMonorepoRoot } from "@rig/runtime/control-plane/native/utils";
|
|
8
|
+
|
|
9
|
+
// packages/memory-plugin/src/db.ts
|
|
10
|
+
import { Database } from "bun:sqlite";
|
|
11
|
+
import { mkdirSync } from "fs";
|
|
12
|
+
import { dirname } from "path";
|
|
13
|
+
import {
|
|
14
|
+
NO_MATCH_RETRIEVAL_CANONICAL_KEY
|
|
15
|
+
} from "@rig/contracts";
|
|
16
|
+
var SCHEMA_STATEMENTS = [
|
|
17
|
+
`CREATE TABLE IF NOT EXISTS memory_events (
|
|
18
|
+
event_id TEXT PRIMARY KEY,
|
|
19
|
+
event_type TEXT NOT NULL,
|
|
20
|
+
canonical_key TEXT NOT NULL,
|
|
21
|
+
summary TEXT,
|
|
22
|
+
kind TEXT,
|
|
23
|
+
category TEXT,
|
|
24
|
+
confidence REAL,
|
|
25
|
+
source_run_id TEXT,
|
|
26
|
+
source_task_id TEXT,
|
|
27
|
+
branch TEXT,
|
|
28
|
+
source_canonical_key TEXT,
|
|
29
|
+
replacement_canonical_key TEXT,
|
|
30
|
+
retrieval_query TEXT,
|
|
31
|
+
retrieval_rank INTEGER,
|
|
32
|
+
feedback_outcome TEXT,
|
|
33
|
+
details_json TEXT,
|
|
34
|
+
created_at TEXT NOT NULL
|
|
35
|
+
)`,
|
|
36
|
+
`CREATE TABLE IF NOT EXISTS memory_items (
|
|
37
|
+
canonical_key TEXT PRIMARY KEY,
|
|
38
|
+
summary TEXT NOT NULL,
|
|
39
|
+
kind TEXT,
|
|
40
|
+
category TEXT,
|
|
41
|
+
status TEXT NOT NULL,
|
|
42
|
+
confidence REAL NOT NULL,
|
|
43
|
+
source_run_id TEXT,
|
|
44
|
+
source_task_id TEXT,
|
|
45
|
+
branch TEXT,
|
|
46
|
+
details_json TEXT,
|
|
47
|
+
created_at TEXT NOT NULL,
|
|
48
|
+
updated_at TEXT NOT NULL,
|
|
49
|
+
last_event_id TEXT NOT NULL,
|
|
50
|
+
superseded_by TEXT,
|
|
51
|
+
embedding TEXT
|
|
52
|
+
)`,
|
|
53
|
+
`CREATE TABLE IF NOT EXISTS memory_links (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
from_key TEXT NOT NULL,
|
|
56
|
+
to_key TEXT NOT NULL,
|
|
57
|
+
relation TEXT NOT NULL,
|
|
58
|
+
source_event_id TEXT NOT NULL,
|
|
59
|
+
created_at TEXT NOT NULL
|
|
60
|
+
)`,
|
|
61
|
+
`CREATE TABLE IF NOT EXISTS task_runs (
|
|
62
|
+
run_id TEXT PRIMARY KEY,
|
|
63
|
+
task_id TEXT,
|
|
64
|
+
branch TEXT,
|
|
65
|
+
first_event_id TEXT,
|
|
66
|
+
last_event_id TEXT,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
updated_at TEXT NOT NULL
|
|
69
|
+
)`,
|
|
70
|
+
`CREATE TABLE IF NOT EXISTS retrieval_feedback (
|
|
71
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
canonical_key TEXT NOT NULL,
|
|
73
|
+
outcome TEXT NOT NULL,
|
|
74
|
+
retrieval_rank INTEGER,
|
|
75
|
+
source_run_id TEXT,
|
|
76
|
+
source_task_id TEXT,
|
|
77
|
+
query_text TEXT,
|
|
78
|
+
source_event_id TEXT NOT NULL,
|
|
79
|
+
created_at TEXT NOT NULL
|
|
80
|
+
)`
|
|
81
|
+
];
|
|
82
|
+
var MEMORY_EVENT_COLUMNS = [
|
|
83
|
+
["summary", "TEXT"],
|
|
84
|
+
["kind", "TEXT"],
|
|
85
|
+
["category", "TEXT"],
|
|
86
|
+
["confidence", "REAL"],
|
|
87
|
+
["source_run_id", "TEXT"],
|
|
88
|
+
["source_task_id", "TEXT"],
|
|
89
|
+
["branch", "TEXT"],
|
|
90
|
+
["source_canonical_key", "TEXT"],
|
|
91
|
+
["replacement_canonical_key", "TEXT"],
|
|
92
|
+
["retrieval_query", "TEXT"],
|
|
93
|
+
["retrieval_rank", "INTEGER"],
|
|
94
|
+
["feedback_outcome", "TEXT"],
|
|
95
|
+
["details_json", "TEXT"]
|
|
96
|
+
];
|
|
97
|
+
var MEMORY_ITEM_COLUMNS = [
|
|
98
|
+
["kind", "TEXT"],
|
|
99
|
+
["category", "TEXT"],
|
|
100
|
+
["branch", "TEXT"],
|
|
101
|
+
["details_json", "TEXT"],
|
|
102
|
+
["superseded_by", "TEXT"],
|
|
103
|
+
["embedding", "TEXT"]
|
|
104
|
+
];
|
|
105
|
+
function normalizeStatement(statement, args) {
|
|
106
|
+
if (typeof statement === "string") {
|
|
107
|
+
return { sql: statement, args };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
sql: statement.sql,
|
|
111
|
+
args: statement.args ?? args
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function normalizeBindings(bindings) {
|
|
115
|
+
return (bindings ?? []).map((binding) => binding === undefined ? null : binding);
|
|
116
|
+
}
|
|
117
|
+
function statementVerb(sql) {
|
|
118
|
+
const match = sql.trim().match(/^[A-Za-z]+/);
|
|
119
|
+
return match ? match[0].toUpperCase() : "";
|
|
120
|
+
}
|
|
121
|
+
function isMutationStatement(sql) {
|
|
122
|
+
return new Set([
|
|
123
|
+
"ALTER",
|
|
124
|
+
"ATTACH",
|
|
125
|
+
"BEGIN",
|
|
126
|
+
"COMMIT",
|
|
127
|
+
"CREATE",
|
|
128
|
+
"DELETE",
|
|
129
|
+
"DETACH",
|
|
130
|
+
"DROP",
|
|
131
|
+
"END",
|
|
132
|
+
"INSERT",
|
|
133
|
+
"REINDEX",
|
|
134
|
+
"RELEASE",
|
|
135
|
+
"REPLACE",
|
|
136
|
+
"ROLLBACK",
|
|
137
|
+
"SAVEPOINT",
|
|
138
|
+
"UPDATE",
|
|
139
|
+
"VACUUM"
|
|
140
|
+
]).has(statementVerb(sql));
|
|
141
|
+
}
|
|
142
|
+
function executeSqlite(sqlite, statement, args) {
|
|
143
|
+
const normalized = normalizeStatement(statement, args);
|
|
144
|
+
const bindings = normalizeBindings(normalized.args);
|
|
145
|
+
if (isMutationStatement(normalized.sql)) {
|
|
146
|
+
const result = bindings.length > 0 ? sqlite.run(normalized.sql, ...bindings) : sqlite.run(normalized.sql);
|
|
147
|
+
return {
|
|
148
|
+
rows: [],
|
|
149
|
+
rowsAffected: Number(result.changes)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const query = sqlite.query(normalized.sql);
|
|
153
|
+
const rows = bindings.length > 0 ? query.all(...bindings) : query.all();
|
|
154
|
+
return {
|
|
155
|
+
rows,
|
|
156
|
+
rowsAffected: 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function createTransaction(sqlite, mode) {
|
|
160
|
+
sqlite.run(mode === "write" ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
161
|
+
let active = true;
|
|
162
|
+
return {
|
|
163
|
+
async execute(statement, args) {
|
|
164
|
+
if (!active) {
|
|
165
|
+
throw new Error("memory transaction is closed");
|
|
166
|
+
}
|
|
167
|
+
return executeSqlite(sqlite, statement, args);
|
|
168
|
+
},
|
|
169
|
+
async commit() {
|
|
170
|
+
if (!active) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
sqlite.run("COMMIT");
|
|
174
|
+
active = false;
|
|
175
|
+
},
|
|
176
|
+
async rollback() {
|
|
177
|
+
if (!active) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
sqlite.run("ROLLBACK");
|
|
181
|
+
active = false;
|
|
182
|
+
},
|
|
183
|
+
close() {
|
|
184
|
+
if (!active) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
sqlite.run("ROLLBACK");
|
|
189
|
+
} catch {} finally {
|
|
190
|
+
active = false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function createMemoryDbClient(sqlite) {
|
|
196
|
+
return {
|
|
197
|
+
async execute(statement, args) {
|
|
198
|
+
return executeSqlite(sqlite, statement, args);
|
|
199
|
+
},
|
|
200
|
+
async transaction(mode) {
|
|
201
|
+
return createTransaction(sqlite, mode);
|
|
202
|
+
},
|
|
203
|
+
close() {
|
|
204
|
+
sqlite.close();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function listColumns(executor, tableName) {
|
|
209
|
+
const result = await executor.execute(`PRAGMA table_info(${tableName})`);
|
|
210
|
+
return new Set(result.rows.map((row) => String(row.name)));
|
|
211
|
+
}
|
|
212
|
+
async function ensureColumns(executor, tableName, columns) {
|
|
213
|
+
const existing = await listColumns(executor, tableName);
|
|
214
|
+
for (const [columnName, definition] of columns) {
|
|
215
|
+
if (!existing.has(columnName)) {
|
|
216
|
+
await executor.execute(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function ensureSchema(db) {
|
|
221
|
+
for (const statement of SCHEMA_STATEMENTS) {
|
|
222
|
+
await db.client.execute(statement);
|
|
223
|
+
}
|
|
224
|
+
await ensureColumns(db.client, "memory_events", MEMORY_EVENT_COLUMNS);
|
|
225
|
+
await ensureColumns(db.client, "memory_items", MEMORY_ITEM_COLUMNS);
|
|
226
|
+
}
|
|
227
|
+
async function openMemoryDb(dbPath) {
|
|
228
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
229
|
+
const sqlite = new Database(dbPath, { create: true, strict: true });
|
|
230
|
+
const client = createMemoryDbClient(sqlite);
|
|
231
|
+
const db = {
|
|
232
|
+
path: dbPath,
|
|
233
|
+
client,
|
|
234
|
+
async close() {
|
|
235
|
+
client.close();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
await ensureSchema(db);
|
|
239
|
+
return db;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// packages/memory-plugin/src/read.ts
|
|
243
|
+
var CANONICAL_MEMORY_DB_PATH = "rig/memory/project-memory.db";
|
|
244
|
+
var DEFAULT_READ_DEPS = {
|
|
245
|
+
fetchRef: nativeFetchRef,
|
|
246
|
+
readBlobBytesAtRef: nativeReadBlobBytesAtRef,
|
|
247
|
+
openMemoryDb,
|
|
248
|
+
makeTempDir: () => mkdtempSync(join(tmpdir(), "memory-sync-read-")),
|
|
249
|
+
removeDir: (path) => rmSync(path, { recursive: true, force: true })
|
|
250
|
+
};
|
|
251
|
+
function isMissingCanonicalMemoryBlobError(error) {
|
|
252
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
253
|
+
return message.includes(`path '${CANONICAL_MEMORY_DB_PATH}' does not exist in`) || message.includes(`path '${CANONICAL_MEMORY_DB_PATH}' exists on disk, but not in`) || message.includes(`pathspec '${CANONICAL_MEMORY_DB_PATH}' did not match any file(s) known to git`);
|
|
254
|
+
}
|
|
255
|
+
async function validateReadableDatabase(dbPath, open) {
|
|
256
|
+
const db = await open(dbPath);
|
|
257
|
+
await db.close();
|
|
258
|
+
}
|
|
259
|
+
async function readCanonicalMemoryDb(projectRoot, deps = {}) {
|
|
260
|
+
const readDeps = { ...DEFAULT_READ_DEPS, ...deps };
|
|
261
|
+
const repoPath = resolveMonorepoRoot(projectRoot);
|
|
262
|
+
let baseOid;
|
|
263
|
+
let remoteUnavailable = false;
|
|
264
|
+
try {
|
|
265
|
+
baseOid = readDeps.fetchRef(repoPath, "origin", "main");
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
268
|
+
const originAbsent = /no such remote|remote (?:'|")?origin(?:'|")? not found/i.test(message);
|
|
269
|
+
if (!originAbsent) {
|
|
270
|
+
console.warn(`[rig-memory] canonical project memory unavailable (origin/main): ${message.split(`
|
|
271
|
+
`)[0]} \u2014 starting this run with empty project memory.`);
|
|
272
|
+
}
|
|
273
|
+
baseOid = "";
|
|
274
|
+
remoteUnavailable = true;
|
|
275
|
+
}
|
|
276
|
+
const tempDir = readDeps.makeTempDir();
|
|
277
|
+
const dbPath = join(tempDir, "project-memory.db");
|
|
278
|
+
let createdFresh = false;
|
|
279
|
+
try {
|
|
280
|
+
if (remoteUnavailable) {
|
|
281
|
+
const db = await readDeps.openMemoryDb(dbPath);
|
|
282
|
+
await db.close();
|
|
283
|
+
createdFresh = true;
|
|
284
|
+
} else {
|
|
285
|
+
try {
|
|
286
|
+
const bytes = readDeps.readBlobBytesAtRef(repoPath, baseOid, CANONICAL_MEMORY_DB_PATH);
|
|
287
|
+
writeFileSync(dbPath, bytes);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!isMissingCanonicalMemoryBlobError(error)) {
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
const db = await readDeps.openMemoryDb(dbPath);
|
|
293
|
+
await db.close();
|
|
294
|
+
createdFresh = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
await validateReadableDatabase(dbPath, readDeps.openMemoryDb);
|
|
298
|
+
return {
|
|
299
|
+
repoPath,
|
|
300
|
+
baseOid,
|
|
301
|
+
dbPath,
|
|
302
|
+
createdFresh,
|
|
303
|
+
async cleanup() {
|
|
304
|
+
readDeps.removeDir(tempDir);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
} catch (error) {
|
|
308
|
+
readDeps.removeDir(tempDir);
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export {
|
|
313
|
+
readCanonicalMemoryDb
|
|
314
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* service.ts — the concrete shared-memory implementation the runtime port
|
|
3
|
+
* resolves and consumes.
|
|
4
|
+
*
|
|
5
|
+
* CONFIG-LIGHT: this module top-level-imports the heavy impl (db.ts pulls
|
|
6
|
+
* bun:sqlite, query/read/cli pull fs + embed). It is loaded LAZILY by the
|
|
7
|
+
* capability `run()` in plugin.ts (`(await import("./service")).svc`), so merely
|
|
8
|
+
* evaluating rig.config.ts never drags the memory impl into scope.
|
|
9
|
+
*/
|
|
10
|
+
import type { MemoryService } from "@rig/runtime/control-plane/memory-service-port";
|
|
11
|
+
/** The concrete shared-memory service the runtime port resolves and consumes. */
|
|
12
|
+
export declare const svc: MemoryService;
|
|
13
|
+
/** Back-compat alias. */
|
|
14
|
+
export declare const memoryService: MemoryService;
|