@yemi33/minions 0.1.2369 → 0.1.2371

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.
Files changed (79) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/memory-search.js +112 -0
  3. package/dashboard/js/render-kb.js +15 -0
  4. package/dashboard/js/render-other.js +2 -30
  5. package/dashboard/js/render-work-items.js +0 -34
  6. package/dashboard/js/settings.js +27 -27
  7. package/dashboard/pages/inbox.html +1 -1
  8. package/dashboard/pages/tools.html +1 -1
  9. package/dashboard.js +148 -258
  10. package/docs/README.md +2 -3
  11. package/docs/architecture.excalidraw +2 -2
  12. package/docs/auto-discovery.md +3 -3
  13. package/docs/completion-reports.md +0 -121
  14. package/docs/copilot-cli-schema.md +9 -17
  15. package/docs/deprecated.json +0 -51
  16. package/docs/design-state-storage.md +4 -4
  17. package/docs/harness-propagation.md +33 -263
  18. package/docs/human-vs-automated.md +1 -1
  19. package/docs/named-agents.md +0 -2
  20. package/docs/plan-lifecycle.md +5 -6
  21. package/docs/qa-runbook-lifecycle.md +10 -25
  22. package/docs/shared-lifecycle-module-map.md +2 -10
  23. package/docs/team-memory.md +18 -4
  24. package/engine/ado-comment.js +5 -11
  25. package/engine/ado.js +10 -5
  26. package/engine/cleanup.js +16 -36
  27. package/engine/cli.js +13 -175
  28. package/engine/comment-format.js +8 -182
  29. package/engine/consolidation.js +72 -1
  30. package/engine/db/index.js +60 -10
  31. package/engine/db/migrations/017-agent-memory.js +208 -0
  32. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  33. package/engine/dispatch-store.js +0 -114
  34. package/engine/dispatch.js +1 -6
  35. package/engine/features.js +6 -0
  36. package/engine/gh-comment.js +2 -10
  37. package/engine/github.js +8 -6
  38. package/engine/lifecycle.js +141 -173
  39. package/engine/llm.js +9 -11
  40. package/engine/managed-spawn.js +1 -6
  41. package/engine/memory-retrieval.js +165 -0
  42. package/engine/memory-store.js +367 -0
  43. package/engine/metrics-store.js +4 -128
  44. package/engine/pipeline.js +4 -7
  45. package/engine/playbook.js +64 -198
  46. package/engine/prd-store.js +115 -141
  47. package/engine/preflight.js +8 -55
  48. package/engine/projects.js +34 -41
  49. package/engine/pull-requests-store.js +9 -234
  50. package/engine/qa-runs.js +4 -15
  51. package/engine/qa-sessions.js +5 -17
  52. package/engine/queries.js +23 -103
  53. package/engine/routing.js +1 -1
  54. package/engine/runtimes/claude.js +1 -2
  55. package/engine/runtimes/codex.js +0 -2
  56. package/engine/runtimes/copilot.js +1 -4
  57. package/engine/shared-branch-pr-reconcile.js +2 -5
  58. package/engine/shared.js +179 -533
  59. package/engine/small-state-store.js +12 -647
  60. package/engine/spawn-agent.js +5 -96
  61. package/engine/state-operations.js +166 -0
  62. package/engine/supervisor.js +0 -1
  63. package/engine/watch-actions.js +2 -3
  64. package/engine/watches-store.js +2 -128
  65. package/engine/work-items-store.js +3 -254
  66. package/engine.js +102 -334
  67. package/package.json +2 -2
  68. package/playbooks/build-fix-complex.md +0 -4
  69. package/playbooks/fix.md +0 -4
  70. package/playbooks/implement-shared.md +0 -4
  71. package/playbooks/implement.md +0 -4
  72. package/playbooks/plan-to-prd.md +16 -11
  73. package/playbooks/plan.md +0 -4
  74. package/playbooks/review.md +0 -6
  75. package/playbooks/shared-rules.md +0 -65
  76. package/docs/harness-transparency.md +0 -199
  77. package/docs/project-skills.md +0 -193
  78. package/engine/discover-project-skills.js +0 -673
  79. package/engine/playbook-intents.js +0 -76
@@ -0,0 +1,165 @@
1
+ // engine/memory-retrieval.js — deterministic task-aware memory selection.
2
+
3
+ const memoryStore = require('./memory-store');
4
+
5
+ const DEFAULT_TOP_K = 8;
6
+ const DEFAULT_MAX_BYTES = 8 * 1024;
7
+ const DEFAULT_CANDIDATES = 50;
8
+
9
+ function buildTaskMemoryQuery(context = {}) {
10
+ const references = Array.isArray(context.references)
11
+ ? context.references.map(ref => {
12
+ if (typeof ref === 'string') return ref;
13
+ if (!ref || typeof ref !== 'object') return '';
14
+ return [ref.path, ref.url, ref.title, ref.type].filter(Boolean).join(' ');
15
+ }).join(' ')
16
+ : String(context.references || '');
17
+ return [
18
+ context.title,
19
+ context.description,
20
+ context.type,
21
+ context.failureClass,
22
+ context.sourcePlan,
23
+ references,
24
+ context.prTitle,
25
+ context.prBranch,
26
+ ].filter(Boolean).join('\n').trim();
27
+ }
28
+
29
+ function _trustBoost(trust) {
30
+ if (trust === 'human') return 12;
31
+ if (trust === 'system') return 8;
32
+ if (trust === 'agent') return 3;
33
+ return 0;
34
+ }
35
+
36
+ function _scopeBoost(record, context) {
37
+ if (record.scopeType === 'project' && record.scopeKey === context.project) return 10;
38
+ if (record.scopeType === 'agent' && record.scopeKey === context.agent) return 7;
39
+ return 2;
40
+ }
41
+
42
+ function _pathBoost(record, query) {
43
+ const source = String(record.sourcePath || '').toLowerCase();
44
+ if (!source) return 0;
45
+ const pathTokens = String(query || '').toLowerCase().match(/[a-z0-9_.-]+[\\/][a-z0-9_./\\-]+/g) || [];
46
+ return pathTokens.some(token => source.includes(token.replace(/\\/g, '/'))) ? 15 : 0;
47
+ }
48
+
49
+ function _recencyBoost(updatedAt, now) {
50
+ const ageDays = Math.max(0, (now - Number(updatedAt || 0)) / 86400000);
51
+ if (ageDays <= 7) return 4;
52
+ if (ageDays <= 30) return 2;
53
+ if (ageDays <= 90) return 1;
54
+ return 0;
55
+ }
56
+
57
+ function rankMemoryCandidates(records, context = {}, query = '') {
58
+ const now = Date.now();
59
+ return records.map((record, index) => ({
60
+ ...record,
61
+ score:
62
+ ((records.length - index) * 10) +
63
+ _scopeBoost(record, context) +
64
+ _trustBoost(record.trust) +
65
+ _pathBoost(record, query) +
66
+ (_recencyBoost(record.updatedAt, now)) +
67
+ (Number(record.importance || 0) * 5) +
68
+ (Number(record.confidence || 0) * 3),
69
+ })).sort((a, b) =>
70
+ b.score - a.score ||
71
+ Number(b.updatedAt || 0) - Number(a.updatedAt || 0) ||
72
+ String(a.id).localeCompare(String(b.id))
73
+ );
74
+ }
75
+
76
+ function dedupeMemoryCandidates(records) {
77
+ const seen = new Set();
78
+ return records.filter(record => {
79
+ const key = record.contentHash || `${record.sourceType}\0${record.sourceRef}`;
80
+ if (seen.has(key)) return false;
81
+ seen.add(key);
82
+ return true;
83
+ });
84
+ }
85
+
86
+ function _truncateUtf8(text, maxBytes) {
87
+ const input = String(text || '');
88
+ if (Buffer.byteLength(input, 'utf8') <= maxBytes) return input;
89
+ let low = 0;
90
+ let high = input.length;
91
+ while (low < high) {
92
+ const mid = Math.ceil((low + high) / 2);
93
+ if (Buffer.byteLength(input.slice(0, mid), 'utf8') <= maxBytes) low = mid;
94
+ else high = mid - 1;
95
+ }
96
+ return input.slice(0, low);
97
+ }
98
+
99
+ function _formatRecord(record, bodyBytes) {
100
+ const provenance = [
101
+ `id=${record.id}`,
102
+ `type=${record.memoryType}`,
103
+ `scope=${record.scopeType}${record.scopeKey ? `:${record.scopeKey}` : ''}`,
104
+ `date=${new Date(record.validFrom || record.createdAt).toISOString().slice(0, 10)}`,
105
+ `source=${record.sourcePath || record.sourceRef}`,
106
+ ].join(' | ');
107
+ const body = _truncateUtf8(record.body, bodyBytes);
108
+ return `### ${record.title}\n_${provenance}_\n\n${body}${body.length < record.body.length ? '\n\n_[excerpt truncated]_' : ''}`;
109
+ }
110
+
111
+ function packMemoryResults(records, options = {}) {
112
+ const topK = Math.max(1, Math.min(30, Number(options.topK) || DEFAULT_TOP_K));
113
+ const maxBytes = Math.max(1024, Math.min(64 * 1024, Number(options.maxBytes) || DEFAULT_MAX_BYTES));
114
+ const selected = [];
115
+ const blocks = [];
116
+ let used = 0;
117
+ for (const record of records.slice(0, topK)) {
118
+ const remaining = maxBytes - used;
119
+ if (remaining < 256) break;
120
+ const block = _formatRecord(record, Math.max(128, remaining - 256));
121
+ const separator = blocks.length ? '\n\n---\n\n' : '';
122
+ const bytes = Buffer.byteLength(separator + block, 'utf8');
123
+ if (bytes > remaining) continue;
124
+ blocks.push(block);
125
+ selected.push(record);
126
+ used += bytes;
127
+ }
128
+ return { text: blocks.join('\n\n---\n\n'), selected, bytes: used };
129
+ }
130
+
131
+ function retrieveRelevantMemory(context = {}, options = {}) {
132
+ const started = process.hrtime.bigint();
133
+ const query = buildTaskMemoryQuery(context);
134
+ if (!query) {
135
+ return { query: '', candidates: [], selected: [], text: '', bytes: 0, durationMs: 0 };
136
+ }
137
+ const candidates = memoryStore.searchMemoryRecords(query, {
138
+ project: context.project,
139
+ agent: context.agent,
140
+ limit: Number(options.candidateLimit) || DEFAULT_CANDIDATES,
141
+ });
142
+ const ranked = dedupeMemoryCandidates(rankMemoryCandidates(candidates, context, query));
143
+ const packed = packMemoryResults(ranked, options);
144
+ const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
145
+ return {
146
+ query,
147
+ candidates: ranked,
148
+ selected: packed.selected,
149
+ text: packed.text,
150
+ bytes: packed.bytes,
151
+ durationMs,
152
+ };
153
+ }
154
+
155
+ module.exports = {
156
+ DEFAULT_TOP_K,
157
+ DEFAULT_MAX_BYTES,
158
+ DEFAULT_CANDIDATES,
159
+ buildTaskMemoryQuery,
160
+ rankMemoryCandidates,
161
+ dedupeMemoryCandidates,
162
+ packMemoryResults,
163
+ retrieveRelevantMemory,
164
+ _truncateUtf8,
165
+ };
@@ -0,0 +1,367 @@
1
+ // engine/memory-store.js — SQL-backed long-term memory records and retrieval.
2
+
3
+ const crypto = require('crypto');
4
+ const { getDb, withTransaction, prepareCached } = require('./db');
5
+ const { emitStateEvent } = require('./db-events');
6
+
7
+ const MEMORY_TYPES = new Set(['semantic', 'episodic', 'procedural']);
8
+ const SCOPE_TYPES = new Set(['global', 'project', 'agent']);
9
+ const STATUSES = new Set(['active', 'superseded', 'retracted', 'expired']);
10
+ const TRUST_LEVELS = new Set(['human', 'system', 'agent', 'external']);
11
+ const MAX_RETRIEVAL_RUNS = 5000;
12
+ const QUERY_STOP_WORDS = new Set([
13
+ 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'in', 'is',
14
+ 'it', 'of', 'on', 'or', 'that', 'the', 'this', 'to', 'with',
15
+ 'add', 'change', 'code', 'file', 'fix', 'implement', 'issue', 'project',
16
+ 'task', 'test', 'update', 'work', 'item', 'agent',
17
+ ]);
18
+
19
+ function _clamp(value, fallback) {
20
+ const n = Number(value);
21
+ return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : fallback;
22
+ }
23
+
24
+ function _json(value, fallback) {
25
+ try { return JSON.stringify(value == null ? fallback : value); }
26
+ catch { return JSON.stringify(fallback); }
27
+ }
28
+
29
+ function _parseJson(value, fallback) {
30
+ try { return JSON.parse(value); } catch { return fallback; }
31
+ }
32
+
33
+ function _hash(value) {
34
+ return crypto.createHash('sha256').update(String(value || '')).digest('hex');
35
+ }
36
+
37
+ function _idFor(record, contentHash) {
38
+ if (record.id) return String(record.id);
39
+ return `M-${_hash(`${record.sourceType}\0${record.sourceRef}\0${contentHash}`).slice(0, 24)}`;
40
+ }
41
+
42
+ function _normalize(record) {
43
+ if (!record || typeof record !== 'object') throw new TypeError('memory record is required');
44
+ const memoryType = MEMORY_TYPES.has(record.memoryType) ? record.memoryType : 'semantic';
45
+ const scopeType = SCOPE_TYPES.has(record.scopeType) ? record.scopeType : 'global';
46
+ const scopeKey = scopeType === 'global' ? '' : String(record.scopeKey || '').trim();
47
+ if (scopeType !== 'global' && !scopeKey) throw new TypeError(`memory scopeKey required for ${scopeType} scope`);
48
+ const title = String(record.title || '').trim();
49
+ const body = String(record.body || '').trim();
50
+ if (!title) throw new TypeError('memory title is required');
51
+ if (!body) throw new TypeError('memory body is required');
52
+ const sourceType = String(record.sourceType || '').trim();
53
+ const sourceRef = String(record.sourceRef || '').trim();
54
+ if (!sourceType || !sourceRef) throw new TypeError('memory sourceType and sourceRef are required');
55
+ const contentHash = _hash(body);
56
+ const now = Date.now();
57
+ return {
58
+ id: _idFor(record, contentHash),
59
+ memoryType,
60
+ scopeType,
61
+ scopeKey,
62
+ title,
63
+ body,
64
+ tags: Array.isArray(record.tags) ? record.tags.map(String).filter(Boolean) : [],
65
+ sourceType,
66
+ sourcePath: record.sourcePath ? String(record.sourcePath) : null,
67
+ sourceRef,
68
+ trust: TRUST_LEVELS.has(record.trust) ? record.trust : 'agent',
69
+ importance: _clamp(record.importance, 0.5),
70
+ confidence: _clamp(record.confidence, 0.5),
71
+ status: STATUSES.has(record.status) ? record.status : 'active',
72
+ validFrom: Number.isFinite(Number(record.validFrom)) ? Number(record.validFrom) : now,
73
+ validTo: Number.isFinite(Number(record.validTo)) ? Number(record.validTo) : null,
74
+ supersedesId: record.supersedesId ? String(record.supersedesId) : null,
75
+ supersededBy: record.supersededBy ? String(record.supersededBy) : null,
76
+ contentHash,
77
+ metadata: record.metadata && typeof record.metadata === 'object' ? record.metadata : {},
78
+ createdAt: Number.isFinite(Number(record.createdAt)) ? Number(record.createdAt) : now,
79
+ updatedAt: now,
80
+ };
81
+ }
82
+
83
+ function _row(row) {
84
+ if (!row) return null;
85
+ return {
86
+ id: row.id,
87
+ memoryType: row.memory_type,
88
+ scopeType: row.scope_type,
89
+ scopeKey: row.scope_key,
90
+ title: row.title,
91
+ body: row.body,
92
+ tags: _parseJson(row.tags, []),
93
+ sourceType: row.source_type,
94
+ sourcePath: row.source_path,
95
+ sourceRef: row.source_ref,
96
+ trust: row.trust,
97
+ importance: row.importance,
98
+ confidence: row.confidence,
99
+ status: row.status,
100
+ validFrom: row.valid_from,
101
+ validTo: row.valid_to,
102
+ supersedesId: row.supersedes_id,
103
+ supersededBy: row.superseded_by,
104
+ contentHash: row.content_hash,
105
+ metadata: _parseJson(row.metadata, {}),
106
+ createdAt: row.created_at,
107
+ updatedAt: row.updated_at,
108
+ ftsRank: row.fts_rank,
109
+ };
110
+ }
111
+
112
+ function upsertMemoryRecord(input) {
113
+ const record = _normalize(input);
114
+ const db = getDb();
115
+ withTransaction(db, () => {
116
+ if (!record.supersedesId) {
117
+ const prior = db.prepare(`
118
+ SELECT id FROM memory_records
119
+ WHERE source_type=? AND source_ref=? AND status='active' AND content_hash<>?
120
+ ORDER BY updated_at DESC LIMIT 1
121
+ `).get(record.sourceType, record.sourceRef, record.contentHash);
122
+ if (prior) record.supersedesId = prior.id;
123
+ }
124
+ if (record.supersedesId) {
125
+ db.prepare(`
126
+ UPDATE memory_records
127
+ SET status='superseded', valid_to=?, superseded_by=?, updated_at=?
128
+ WHERE id=? AND status='active'
129
+ `).run(record.validFrom, record.id, record.updatedAt, record.supersedesId);
130
+ }
131
+ db.prepare(`
132
+ INSERT INTO memory_records (
133
+ id, memory_type, scope_type, scope_key, title, body, tags,
134
+ source_type, source_path, source_ref, trust, importance, confidence,
135
+ status, valid_from, valid_to, supersedes_id, superseded_by,
136
+ content_hash, metadata, created_at, updated_at
137
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
138
+ ON CONFLICT(id) DO UPDATE SET
139
+ memory_type=excluded.memory_type,
140
+ scope_type=excluded.scope_type,
141
+ scope_key=excluded.scope_key,
142
+ title=excluded.title,
143
+ body=excluded.body,
144
+ tags=excluded.tags,
145
+ source_path=excluded.source_path,
146
+ trust=excluded.trust,
147
+ importance=excluded.importance,
148
+ confidence=excluded.confidence,
149
+ status=excluded.status,
150
+ valid_from=excluded.valid_from,
151
+ valid_to=excluded.valid_to,
152
+ supersedes_id=excluded.supersedes_id,
153
+ superseded_by=excluded.superseded_by,
154
+ metadata=excluded.metadata,
155
+ updated_at=excluded.updated_at
156
+ `).run(
157
+ record.id, record.memoryType, record.scopeType, record.scopeKey,
158
+ record.title, record.body, _json(record.tags, []),
159
+ record.sourceType, record.sourcePath, record.sourceRef, record.trust,
160
+ record.importance, record.confidence, record.status, record.validFrom,
161
+ record.validTo, record.supersedesId, record.supersededBy,
162
+ record.contentHash, _json(record.metadata, {}), record.createdAt, record.updatedAt,
163
+ );
164
+ });
165
+ emitStateEvent('memory_records', { id: record.id, status: record.status });
166
+ return getMemoryRecord(record.id);
167
+ }
168
+
169
+ function getMemoryRecord(id) {
170
+ const db = getDb();
171
+ return _row(prepareCached(db, 'SELECT * FROM memory_records WHERE id=?').get(String(id)));
172
+ }
173
+
174
+ function updateMemoryRecordState(id, action) {
175
+ const recordId = String(id || '').trim();
176
+ if (!recordId) throw new TypeError('memory record id is required');
177
+ if (!['pin', 'retract', 'restore'].includes(action)) {
178
+ throw new TypeError(`unsupported memory action: ${action}`);
179
+ }
180
+ if (!getMemoryRecord(recordId)) return null;
181
+
182
+ const db = getDb();
183
+ const now = Date.now();
184
+ if (action === 'pin') {
185
+ db.prepare(`
186
+ UPDATE memory_records
187
+ SET trust='human', importance=1, confidence=MAX(confidence, 0.9),
188
+ status='active', valid_to=NULL, superseded_by=NULL, updated_at=?
189
+ WHERE id=?
190
+ `).run(now, recordId);
191
+ } else if (action === 'restore') {
192
+ db.prepare(`
193
+ UPDATE memory_records
194
+ SET status='active', valid_to=NULL, superseded_by=NULL, updated_at=?
195
+ WHERE id=?
196
+ `).run(now, recordId);
197
+ } else {
198
+ db.prepare(`
199
+ UPDATE memory_records
200
+ SET status='retracted', valid_to=?, updated_at=?
201
+ WHERE id=?
202
+ `).run(now, now, recordId);
203
+ }
204
+ emitStateEvent('memory_records', { id: recordId, action });
205
+ return getMemoryRecord(recordId);
206
+ }
207
+
208
+ function listMemoryRecords(filters = {}) {
209
+ const where = [];
210
+ const args = [];
211
+ for (const [column, value] of [
212
+ ['status', filters.status],
213
+ ['memory_type', filters.memoryType],
214
+ ['scope_type', filters.scopeType],
215
+ ['scope_key', filters.scopeKey],
216
+ ]) {
217
+ if (value == null || value === '') continue;
218
+ where.push(`${column}=?`);
219
+ args.push(String(value));
220
+ }
221
+ const limit = Math.max(1, Math.min(500, Number(filters.limit) || 100));
222
+ const sql = `SELECT * FROM memory_records${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY updated_at DESC LIMIT ?`;
223
+ return prepareCached(getDb(), sql).all(...args, limit).map(_row);
224
+ }
225
+
226
+ function _matchQuery(query) {
227
+ const tokens = String(query || '')
228
+ .toLowerCase()
229
+ .match(/[a-z0-9][a-z0-9_.-]{1,63}/g);
230
+ if (!tokens) return '';
231
+ const unique = [...new Set(tokens)]
232
+ .filter(token => !QUERY_STOP_WORDS.has(token))
233
+ .slice(0, 32);
234
+ return unique.map(token => `"${token.replace(/"/g, '""')}"`).join(' OR ');
235
+ }
236
+
237
+ function searchMemoryRecords(query, options = {}) {
238
+ const match = _matchQuery(query);
239
+ if (!match) return [];
240
+ const where = [
241
+ 'memory_records_fts MATCH ?',
242
+ "(m.memory_type<>'procedural' OR m.trust IN ('human','system'))",
243
+ ];
244
+ const args = [match];
245
+ const statuses = Array.isArray(options.statuses)
246
+ ? options.statuses.filter(status => STATUSES.has(status))
247
+ : ['active'];
248
+ if (!statuses.length) return [];
249
+ where.push(`m.status IN (${statuses.map(() => '?').join(',')})`);
250
+ args.push(...statuses);
251
+ if (!options.allScopes) {
252
+ const scopes = ["m.scope_type='global'"];
253
+ if (options.project) {
254
+ scopes.push("(m.scope_type='project' AND m.scope_key=?)");
255
+ args.push(String(options.project));
256
+ }
257
+ if (options.agent) {
258
+ scopes.push("(m.scope_type='agent' AND m.scope_key=?)");
259
+ args.push(String(options.agent));
260
+ }
261
+ where.push(`(${scopes.join(' OR ')})`);
262
+ }
263
+ if (Array.isArray(options.memoryTypes) && options.memoryTypes.length) {
264
+ const types = options.memoryTypes.filter(t => MEMORY_TYPES.has(t));
265
+ if (types.length) {
266
+ where.push(`m.memory_type IN (${types.map(() => '?').join(',')})`);
267
+ args.push(...types);
268
+ }
269
+ }
270
+ const limit = Math.max(1, Math.min(200, Number(options.limit) || 50));
271
+ args.push(limit);
272
+ const sql = `
273
+ SELECT m.*, bm25(memory_records_fts, 5.0, 1.0, 2.0, 3.0) AS fts_rank
274
+ FROM memory_records_fts
275
+ JOIN memory_records m ON m.rowid=memory_records_fts.rowid
276
+ WHERE ${where.join(' AND ')}
277
+ ORDER BY fts_rank
278
+ LIMIT ?
279
+ `;
280
+ return prepareCached(getDb(), sql).all(...args).map(_row);
281
+ }
282
+
283
+ function transitionMemoryRecord(id, status, options = {}) {
284
+ if (!STATUSES.has(status) || status === 'active') throw new TypeError(`invalid memory status: ${status}`);
285
+ const now = Date.now();
286
+ const result = getDb().prepare(`
287
+ UPDATE memory_records
288
+ SET status=?, valid_to=?, superseded_by=COALESCE(?, superseded_by), updated_at=?
289
+ WHERE id=?
290
+ `).run(status, Number(options.validTo) || now, options.supersededBy || null, now, String(id));
291
+ if (result.changes) emitStateEvent('memory_records', { id: String(id), status });
292
+ return result.changes > 0;
293
+ }
294
+
295
+ function supersedeMatchingRecords(options = {}) {
296
+ const texts = Array.isArray(options.oldTexts) ? options.oldTexts.map(String).filter(Boolean) : [];
297
+ if (!texts.length || !options.supersededBy) return 0;
298
+ const db = getDb();
299
+ const now = Date.now();
300
+ let changed = 0;
301
+ withTransaction(db, () => {
302
+ for (const text of texts) {
303
+ const rows = db.prepare(`
304
+ SELECT id FROM memory_records
305
+ WHERE status='active' AND scope_type=? AND scope_key=? AND instr(body, ?) > 0
306
+ `).all(options.scopeType || 'global', options.scopeKey || '', text);
307
+ for (const row of rows) {
308
+ const result = db.prepare(`
309
+ UPDATE memory_records
310
+ SET status='superseded', valid_to=?, superseded_by=?, updated_at=?
311
+ WHERE id=? AND status='active'
312
+ `).run(now, String(options.supersededBy), now, row.id);
313
+ changed += Number(result.changes) || 0;
314
+ }
315
+ }
316
+ });
317
+ if (changed) emitStateEvent('memory_records', { supersededBy: String(options.supersededBy), changed });
318
+ return changed;
319
+ }
320
+
321
+ function recordRetrievalRun(run) {
322
+ const db = getDb();
323
+ withTransaction(db, () => {
324
+ db.prepare(`
325
+ INSERT INTO memory_retrieval_runs (
326
+ work_item_id, project, agent, query, candidate_count, selected_count,
327
+ selected_bytes, duration_ms, shadow, created_at
328
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
329
+ `).run(
330
+ run.workItemId || null, run.project || null, run.agent || null,
331
+ String(run.query || ''), Number(run.candidateCount) || 0,
332
+ Number(run.selectedCount) || 0, Number(run.selectedBytes) || 0,
333
+ Number(run.durationMs) || 0, run.shadow ? 1 : 0, Date.now(),
334
+ );
335
+ db.prepare(`
336
+ DELETE FROM memory_retrieval_runs
337
+ WHERE id NOT IN (SELECT id FROM memory_retrieval_runs ORDER BY id DESC LIMIT ?)
338
+ `).run(MAX_RETRIEVAL_RUNS);
339
+ });
340
+ }
341
+
342
+ function getRetrievalStats() {
343
+ return prepareCached(getDb(), `
344
+ SELECT COUNT(*) AS runs,
345
+ COALESCE(AVG(duration_ms), 0) AS avg_duration_ms,
346
+ COALESCE(AVG(selected_count), 0) AS avg_selected_count,
347
+ COALESCE(AVG(selected_bytes), 0) AS avg_selected_bytes,
348
+ COALESCE(MAX(duration_ms), 0) AS max_duration_ms
349
+ FROM memory_retrieval_runs
350
+ `).get();
351
+ }
352
+
353
+ module.exports = {
354
+ MEMORY_TYPES,
355
+ SCOPE_TYPES,
356
+ STATUSES,
357
+ upsertMemoryRecord,
358
+ getMemoryRecord,
359
+ updateMemoryRecordState,
360
+ listMemoryRecords,
361
+ searchMemoryRecords,
362
+ transitionMemoryRecord,
363
+ supersedeMatchingRecords,
364
+ recordRetrievalRun,
365
+ getRetrievalStats,
366
+ _matchQuery,
367
+ };