@shadowforge0/aquifer-memory 1.5.12 → 1.6.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.
Files changed (60) hide show
  1. package/.env.example +23 -0
  2. package/README.md +78 -73
  3. package/README_CN.md +659 -0
  4. package/README_TW.md +680 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +353 -52
  8. package/consumers/codex-handoff.js +152 -0
  9. package/consumers/codex.js +1549 -0
  10. package/consumers/default/daily-entries.js +23 -4
  11. package/consumers/default/index.js +2 -2
  12. package/consumers/default/prompts/summary.js +6 -6
  13. package/consumers/mcp.js +96 -5
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +1 -1
  16. package/consumers/shared/config.js +8 -0
  17. package/consumers/shared/factory.js +1 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +27 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +372 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/mcp-manifest.js +52 -2
  25. package/core/memory-bootstrap.js +188 -0
  26. package/core/memory-consolidation.js +1236 -0
  27. package/core/memory-promotion.js +544 -0
  28. package/core/memory-recall.js +247 -0
  29. package/core/memory-records.js +581 -0
  30. package/core/memory-safety-gate.js +224 -0
  31. package/core/session-finalization.js +350 -0
  32. package/core/storage.js +385 -2
  33. package/docs/getting-started.md +99 -0
  34. package/docs/postprocess-contract.md +2 -2
  35. package/docs/setup.md +51 -2
  36. package/package.json +25 -11
  37. package/pipeline/normalize/adapters/codex.js +106 -0
  38. package/pipeline/normalize/detect.js +3 -2
  39. package/schema/001-base.sql +3 -0
  40. package/schema/007-v1-foundation.sql +273 -0
  41. package/schema/008-session-finalizations.sql +50 -0
  42. package/schema/009-v1-assertion-plane.sql +193 -0
  43. package/schema/010-v1-finalization-review.sql +160 -0
  44. package/schema/011-v1-compaction-claim.sql +46 -0
  45. package/schema/012-v1-compaction-lease.sql +39 -0
  46. package/schema/013-v1-compaction-lineage.sql +193 -0
  47. package/scripts/codex-recovery.js +532 -0
  48. package/consumers/miranda/context-inject.js +0 -120
  49. package/consumers/miranda/daily-entries.js +0 -224
  50. package/consumers/miranda/index.js +0 -364
  51. package/consumers/miranda/instance.js +0 -55
  52. package/consumers/miranda/llm.js +0 -99
  53. package/consumers/miranda/profile.json +0 -145
  54. package/consumers/miranda/prompts/summary.js +0 -303
  55. package/consumers/miranda/recall-format.js +0 -76
  56. package/consumers/miranda/render-daily-md.js +0 -186
  57. package/consumers/miranda/workspace-files.js +0 -91
  58. package/scripts/drop-entity-state-history.sql +0 -17
  59. package/scripts/drop-insights.sql +0 -12
  60. package/scripts/install-openclaw.sh +0 -59
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ const TYPE_LABELS = {
4
+ state: '狀態',
5
+ decision: '決策',
6
+ fact: '事實',
7
+ preference: '偏好',
8
+ constraint: '限制',
9
+ entity_note: '註記',
10
+ open_loop: '未完成',
11
+ conclusion: '判斷',
12
+ };
13
+
14
+ const SESSION_START_TYPE_PRIORITY = {
15
+ state: 0,
16
+ open_loop: 1,
17
+ constraint: 2,
18
+ preference: 3,
19
+ decision: 4,
20
+ fact: 5,
21
+ conclusion: 6,
22
+ entity_note: 7,
23
+ };
24
+
25
+ const AUTHORITY_PRIORITY = {
26
+ user_explicit: 0,
27
+ executable_evidence: 1,
28
+ manual: 2,
29
+ system: 3,
30
+ verified_summary: 4,
31
+ llm_inference: 5,
32
+ raw_transcript: 6,
33
+ };
34
+
35
+ const MEMORY_KEYS = [
36
+ 'summary',
37
+ 'title',
38
+ 'decision',
39
+ 'item',
40
+ 'conclusion',
41
+ 'statement',
42
+ 'fact',
43
+ 'preference',
44
+ 'constraint',
45
+ 'state',
46
+ 'note',
47
+ 'text',
48
+ 'value',
49
+ ];
50
+
51
+ const STRUCTURED_FIELDS = [
52
+ ['states', 'state'],
53
+ ['state', 'state'],
54
+ ['decisions', 'decision'],
55
+ ['important_facts', 'fact'],
56
+ ['facts', 'fact'],
57
+ ['preferences', 'preference'],
58
+ ['constraints', 'constraint'],
59
+ ['conclusions', 'conclusion'],
60
+ ['entity_notes', 'entity_note'],
61
+ ['open_loops', 'open_loop'],
62
+ ];
63
+
64
+ const DEFAULT_OMIT = [
65
+ '整段逐字稿、工具輸出、debug 訊息',
66
+ 'DB row id、hash、message count 這類 audit 欄位',
67
+ '已作廢、隔離、錯誤或 superseded 的記憶',
68
+ ];
69
+
70
+ function normalizeText(value) {
71
+ return String(value || '').trim().replace(/\s+/g, ' ');
72
+ }
73
+
74
+ function sanitizeHumanText(value) {
75
+ return normalizeText(value)
76
+ .replace(/\bDB Write Plan\b/g, 'DB 寫入計畫')
77
+ .replace(/\bLegacy Continuity Text\b/g, '舊 handoff 包裝文字')
78
+ .replace(/\bStructured Summary\b/g, 'structured summary 原始欄位')
79
+ .replace(/\braw JSON\b/gi, '原始 JSON');
80
+ }
81
+
82
+ function stripTerminalPunctuation(value) {
83
+ return normalizeText(value).replace(/[。.!?!?]+$/g, '');
84
+ }
85
+
86
+ function comparable(value) {
87
+ return stripTerminalPunctuation(value).toLowerCase();
88
+ }
89
+
90
+ function firstText(value) {
91
+ if (typeof value === 'string') return normalizeText(value);
92
+ if (!value || typeof value !== 'object') return '';
93
+ for (const key of MEMORY_KEYS) {
94
+ const text = normalizeText(value[key]);
95
+ if (text) return text;
96
+ }
97
+ const payload = value.payload && typeof value.payload === 'object' ? value.payload : null;
98
+ if (payload) return firstText(payload);
99
+ return '';
100
+ }
101
+
102
+ function memoryTypeOf(value) {
103
+ if (!value || typeof value !== 'object') return 'memory';
104
+ return value.memoryType || value.memory_type || value.type || 'memory';
105
+ }
106
+
107
+ function labelFor(type) {
108
+ return TYPE_LABELS[type] || TYPE_LABELS[String(type || '').toLowerCase()] || '記憶';
109
+ }
110
+
111
+ function pushUnique(out, text) {
112
+ const normalized = sanitizeHumanText(text);
113
+ if (!normalized) return;
114
+ const key = comparable(normalized);
115
+ if (!key || out.some(item => comparable(item) === key)) return;
116
+ out.push(normalized);
117
+ }
118
+
119
+ function asLine(type, text, suffix = '') {
120
+ const body = normalizeText(text);
121
+ if (!body) return '';
122
+ return `${labelFor(type)}:${body}${suffix}`;
123
+ }
124
+
125
+ function truncate(text, max = 220) {
126
+ const normalized = sanitizeHumanText(text);
127
+ if (normalized.length <= max) return normalized;
128
+ return `${normalized.slice(0, max - 1)}...`;
129
+ }
130
+
131
+ function addStructuredItems(out, structuredSummary = {}, filter = null) {
132
+ for (const [field, type] of STRUCTURED_FIELDS) {
133
+ if (filter && !filter(type)) continue;
134
+ const items = Array.isArray(structuredSummary[field]) ? structuredSummary[field] : [];
135
+ for (const item of items) {
136
+ const text = firstText(item);
137
+ if (!text) continue;
138
+ const owner = type === 'open_loop' && item && typeof item === 'object' && normalizeText(item.owner)
139
+ ? `(owner: ${normalizeText(item.owner)})`
140
+ : '';
141
+ pushUnique(out, asLine(type, text, owner));
142
+ }
143
+ }
144
+ }
145
+
146
+ function promotedMemoryLines(memoryResults = []) {
147
+ const lines = [];
148
+ for (const result of memoryResults || []) {
149
+ if (!result || result.action !== 'promote') continue;
150
+ const memory = result.memory || result.record || result.candidate || {};
151
+ const type = memoryTypeOf(memory);
152
+ if (type === 'open_loop') continue;
153
+ pushUnique(lines, asLine(type, firstText(memory)));
154
+ }
155
+ return lines;
156
+ }
157
+
158
+ function openLoopLines(memoryResults = [], structuredSummary = {}) {
159
+ const lines = [];
160
+ for (const result of memoryResults || []) {
161
+ if (!result || result.action !== 'promote') continue;
162
+ const memory = result.memory || result.record || result.candidate || {};
163
+ const type = memoryTypeOf(memory);
164
+ if (type !== 'open_loop') continue;
165
+ const owner = normalizeText(memory.owner || memory.payload?.owner);
166
+ pushUnique(lines, asLine(type, firstText(memory), owner ? `(owner: ${owner})` : ''));
167
+ }
168
+ if (lines.length === 0) {
169
+ addStructuredItems(lines, structuredSummary, type => type === 'open_loop');
170
+ }
171
+ return lines;
172
+ }
173
+
174
+ function inactiveLines(memoryResults = [], extraInactive = []) {
175
+ const lines = [];
176
+ for (const result of memoryResults || []) {
177
+ if (!result || result.action === 'promote') continue;
178
+ const candidate = result.candidate || result.memory || result.record || {};
179
+ const text = firstText(candidate);
180
+ const reason = normalizeText(result.reason);
181
+ const action = normalizeText(result.action || 'skipped');
182
+ if (text || reason) {
183
+ pushUnique(lines, `${action}:${text || '未命名候選'}${reason ? `(${reason})` : ''}`);
184
+ }
185
+ }
186
+ for (const item of extraInactive || []) {
187
+ const text = firstText(item);
188
+ const status = normalizeText(item.status || item.action || 'inactive');
189
+ const reason = normalizeText(item.reason || item.obsoleteReason || item.obsolete_reason);
190
+ if (text || reason) {
191
+ pushUnique(lines, `${status}:${text || '未命名記憶'}${reason ? `(${reason})` : ''}`);
192
+ }
193
+ }
194
+ return lines;
195
+ }
196
+
197
+ function linesOrNone(lines) {
198
+ if (!lines || lines.length === 0) return '無';
199
+ return lines.map(line => `- ${line}`).join('\n');
200
+ }
201
+
202
+ function buildAuditLines(input = {}) {
203
+ const finalization = input.finalization || {};
204
+ const memoryResult = input.memoryResult || {};
205
+ const audit = input.audit || {};
206
+ const pairs = [
207
+ ['sessionId', audit.sessionId || input.sessionId],
208
+ ['finalizationId', audit.finalizationId || finalization.id],
209
+ ['handoffId', audit.handoffId || input.handoffId],
210
+ ['transcriptHash', audit.transcriptHash || input.transcriptHash],
211
+ ['promoted', memoryResult.promoted],
212
+ ['quarantined', memoryResult.quarantined],
213
+ ['skipped', memoryResult.skipped],
214
+ ['policyVersion', audit.policyVersion || input.policyVersion],
215
+ ['schemaVersion', audit.schemaVersion || input.schemaVersion],
216
+ ].filter(([, value]) => value !== undefined && value !== null && value !== '');
217
+ return pairs.map(([key, value]) => `${key}: ${value}`);
218
+ }
219
+
220
+ function collectRemembered(input = {}) {
221
+ const structuredSummary = input.structuredSummary || input.summary?.structuredSummary || {};
222
+ const memoryResults = input.memoryResults || [];
223
+ const lines = promotedMemoryLines(memoryResults);
224
+ if (lines.length === 0) {
225
+ addStructuredItems(lines, structuredSummary, type => type !== 'open_loop');
226
+ }
227
+ return lines;
228
+ }
229
+
230
+ function buildCarryForwardLines(input = {}) {
231
+ const lines = [];
232
+ for (const line of input.openLoops || openLoopLines(input.memoryResults, input.structuredSummary || input.summary?.structuredSummary || {})) {
233
+ pushUnique(lines, line);
234
+ }
235
+ const next = normalizeText(input.next || input.metadata?.handoff?.next);
236
+ if (next && next !== '無') pushUnique(lines, `下一步:${next}`);
237
+ return lines;
238
+ }
239
+
240
+ function buildFinalizationReview(input = {}, opts = {}) {
241
+ const summary = input.summary || {};
242
+ const structuredSummary = input.structuredSummary || summary.structuredSummary || {};
243
+ const summaryText = input.summaryText || summary.summaryText || input.overview || '';
244
+ const statusLine = truncate(input.currentStatus || summaryText || input.title || '已完成本段 finalization。');
245
+ const remembered = collectRemembered({ ...input, structuredSummary });
246
+ const openLoops = openLoopLines(input.memoryResults || [], structuredSummary);
247
+ const inactive = inactiveLines(input.memoryResults || [], input.inactive || []);
248
+ const carryForward = buildCarryForwardLines({ ...input, structuredSummary, openLoops });
249
+ const omit = [];
250
+ for (const item of opts.omit || input.omit || DEFAULT_OMIT) pushUnique(omit, item);
251
+ const heading = opts.preview ? '準備整理進 DB:' : '已整理進 DB:';
252
+ const lines = [
253
+ heading,
254
+ `目前狀態:\n${linesOrNone([statusLine])}`,
255
+ `已記住:\n${linesOrNone(remembered)}`,
256
+ `未完成:\n${linesOrNone(openLoops)}`,
257
+ `已作廢或隔離:\n${linesOrNone(inactive)}`,
258
+ `下一段只需要帶:\n${linesOrNone(carryForward)}`,
259
+ `不要帶:\n${linesOrNone(omit)}`,
260
+ ];
261
+ if (opts.includeAudit === true) {
262
+ lines.push(`Audit:\n${linesOrNone(buildAuditLines(input))}`);
263
+ }
264
+ return `${lines.join('\n\n')}\n`;
265
+ }
266
+
267
+ function buildSessionStartContext(records = [], opts = {}) {
268
+ const asOf = opts.asOf ? Date.parse(opts.asOf) : null;
269
+ const limit = Math.max(1, Math.min(50, opts.limit || 12));
270
+ const maxChars = Math.max(120, opts.maxChars || 1800);
271
+ const active = [];
272
+ for (const [index, record] of (records || []).entries()) {
273
+ const status = record.status || 'candidate';
274
+ const visible = record.visibleInBootstrap ?? record.visible_in_bootstrap;
275
+ if (status !== 'active' || visible !== true) continue;
276
+ if (Number.isFinite(asOf)) {
277
+ const validFrom = Date.parse(record.validFrom || record.valid_from || '');
278
+ const validTo = Date.parse(record.validTo || record.valid_to || '');
279
+ const staleAfter = Date.parse(record.staleAfter || record.stale_after || '');
280
+ if (Number.isFinite(validFrom) && validFrom > asOf) continue;
281
+ if (Number.isFinite(validTo) && validTo <= asOf) continue;
282
+ if (Number.isFinite(staleAfter) && staleAfter <= asOf) continue;
283
+ }
284
+ active.push({ record, index });
285
+ }
286
+
287
+ active.sort((a, b) => {
288
+ const aType = SESSION_START_TYPE_PRIORITY[memoryTypeOf(a.record)] ?? 99;
289
+ const bType = SESSION_START_TYPE_PRIORITY[memoryTypeOf(b.record)] ?? 99;
290
+ if (aType !== bType) return aType - bType;
291
+
292
+ const aAuth = AUTHORITY_PRIORITY[a.record.authority] ?? 99;
293
+ const bAuth = AUTHORITY_PRIORITY[b.record.authority] ?? 99;
294
+ if (aAuth !== bAuth) return aAuth - bAuth;
295
+
296
+ const aAccepted = Date.parse(a.record.acceptedAt || a.record.accepted_at || '') || 0;
297
+ const bAccepted = Date.parse(b.record.acceptedAt || b.record.accepted_at || '') || 0;
298
+ if (aAccepted !== bAccepted) return bAccepted - aAccepted;
299
+ return a.index - b.index;
300
+ });
301
+
302
+ const lines = [];
303
+ for (const { record } of active.slice(0, limit)) {
304
+ const type = memoryTypeOf(record);
305
+ pushUnique(lines, asLine(type, firstText(record)));
306
+ }
307
+ let selected = lines;
308
+ let text = `下一段只需要帶:\n${linesOrNone(selected)}\n`;
309
+ while (text.length > maxChars && selected.length > 1) {
310
+ selected = selected.slice(0, -1);
311
+ text = `下一段只需要帶:\n${linesOrNone(selected)}\n`;
312
+ }
313
+ return text;
314
+ }
315
+
316
+ module.exports = {
317
+ buildFinalizationReview,
318
+ buildSessionStartContext,
319
+ };
@@ -20,7 +20,7 @@ const MCP_SERVER_NAME = 'aquifer-memory';
20
20
  const MCP_TOOL_MANIFEST = Object.freeze([
21
21
  {
22
22
  name: 'session_recall',
23
- description: 'Search stored sessions by keyword or natural language. Use entities when the user names specific people, projects, files, tools, or concepts; entityMode="all" hard-filters to sessions containing every entity (default "any" boosts). Use mode to force fts/vector/hybrid (default hybrid). Use dateFrom/dateTo for time-bounded recall.',
23
+ description: 'Search Aquifer memory. When memory serving mode is curated, this searches active curated memory only; use evidence_recall for legacy session/evidence lookup. Use entities/date filters where supported by the active serving mode.',
24
24
  inputSchema: {
25
25
  type: 'object',
26
26
  additionalProperties: false,
@@ -50,13 +50,41 @@ const MCP_TOOL_MANIFEST = Object.freeze([
50
50
  type: 'boolean',
51
51
  description: 'Include per-result score breakdown (rrf, timeDecay, entity, trust, rerank). Diagnostic use only.',
52
52
  },
53
+ activeScopeKey: { type: 'string', description: 'Active curated memory scope key, e.g. project:aquifer' },
54
+ activeScopePath: {
55
+ type: 'array',
56
+ items: { type: 'string' },
57
+ description: 'Ordered curated scope path from global to active scope',
58
+ },
59
+ },
60
+ required: ['query'],
61
+ },
62
+ },
63
+ {
64
+ name: 'evidence_recall',
65
+ description: 'Explicit legacy/evidence search over stored sessions and summaries. This is for audit/debug/distillation and must not implicitly feed bootstrap.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ additionalProperties: false,
69
+ properties: {
70
+ query: { type: 'string', minLength: 1, description: 'Evidence search query (keyword or natural language)' },
71
+ limit: { type: 'integer', minimum: 1, maximum: 20, description: 'Max results (default 5)' },
72
+ agentId: { type: 'string', description: 'Filter by agent ID' },
73
+ source: { type: 'string', description: 'Filter by source (e.g., gateway, cc)' },
74
+ dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },
75
+ dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },
76
+ entities: { type: 'array', items: { type: 'string' }, description: 'Entity names to match' },
77
+ entityMode: { type: 'string', enum: ['any', 'all'], description: '"any" (default, boost) or "all" hard filter' },
78
+ mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'Legacy evidence recall mode' },
79
+ explain: { type: 'boolean', description: 'Include diagnostic score breakdown.' },
80
+ allowUnsafeDebug: { type: 'boolean', description: 'Allow broad evidence/debug search without an audit boundary.' },
53
81
  },
54
82
  required: ['query'],
55
83
  },
56
84
  },
57
85
  {
58
86
  name: 'session_feedback',
59
- description: 'After using session_recall, mark the result helpful if it directly informed your answer, or unhelpful if it was irrelevant/outdated. Include a short note. Sessions with more helpful feedback rank higher in future recalls.',
87
+ description: 'After using legacy session_recall or bootstrap, mark the recalled session helpful or unhelpful. This only targets legacy session trust, not curated memory rows.',
60
88
  inputSchema: {
61
89
  type: 'object',
62
90
  additionalProperties: false,
@@ -69,6 +97,22 @@ const MCP_TOOL_MANIFEST = Object.freeze([
69
97
  required: ['sessionId', 'verdict'],
70
98
  },
71
99
  },
100
+ {
101
+ name: 'memory_feedback',
102
+ description: 'Record append-only feedback on a curated memory row. This affects curated ranking/review priority only and does not mutate memory truth.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ additionalProperties: false,
106
+ properties: {
107
+ memoryId: { type: 'string', minLength: 1, description: 'Curated memory record ID to give feedback on' },
108
+ canonicalKey: { type: 'string', minLength: 1, description: 'Canonical key of the active curated memory record' },
109
+ feedbackType: { type: 'string', enum: ['helpful', 'confirm', 'irrelevant', 'scope_mismatch', 'stale', 'incorrect'], description: 'Curated memory feedback event type' },
110
+ note: { type: 'string', description: 'Optional reason' },
111
+ agentId: { type: 'string', description: 'Optional actor/agent label for audit metadata' },
112
+ },
113
+ required: ['feedbackType'],
114
+ },
115
+ },
72
116
  {
73
117
  name: 'memory_stats',
74
118
  description: 'Return storage statistics for the Aquifer memory store (session counts by status, summaries, turn embeddings, entities, date range).',
@@ -113,6 +157,12 @@ const MCP_TOOL_MANIFEST = Object.freeze([
113
157
  limit: { type: 'integer', minimum: 1, maximum: 20, description: 'Max sessions (default 5)' },
114
158
  lookbackDays: { type: 'integer', minimum: 1, maximum: 90, description: 'How far back in days (default 14)' },
115
159
  maxChars: { type: 'integer', minimum: 500, maximum: 12000, description: 'Max output characters (default 4000)' },
160
+ activeScopeKey: { type: 'string', description: 'Active curated memory scope key, e.g. project:aquifer' },
161
+ activeScopePath: {
162
+ type: 'array',
163
+ items: { type: 'string' },
164
+ description: 'Ordered curated scope path from global to active scope',
165
+ },
116
166
  },
117
167
  },
118
168
  },
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ const TYPE_PRIORITY = {
4
+ constraint: 0,
5
+ preference: 1,
6
+ state: 2,
7
+ open_loop: 3,
8
+ decision: 4,
9
+ fact: 5,
10
+ conclusion: 6,
11
+ entity_note: 7,
12
+ };
13
+
14
+ const AUTHORITY_PRIORITY = {
15
+ user_explicit: 0,
16
+ executable_evidence: 1,
17
+ manual: 2,
18
+ system: 3,
19
+ verified_summary: 4,
20
+ llm_inference: 5,
21
+ raw_transcript: 6,
22
+ };
23
+
24
+ function recordId(record) {
25
+ return String(record.memoryId || record.memory_id || record.id || record.canonicalKey || record.canonical_key);
26
+ }
27
+
28
+ function scopeKey(record) {
29
+ return record.scopeKey || record.scope_key || record.scope || '';
30
+ }
31
+
32
+ function inheritanceMode(record) {
33
+ return record.inheritanceMode || record.inheritance_mode || record.scope_inheritance_mode || 'defaultable';
34
+ }
35
+
36
+ function canonicalKey(record) {
37
+ return record.canonicalKey || record.canonical_key || recordId(record);
38
+ }
39
+
40
+ function parseTime(value) {
41
+ const t = Date.parse(value || '');
42
+ return Number.isFinite(t) ? t : null;
43
+ }
44
+
45
+ function isWithinTime(record, asOf) {
46
+ if (!asOf) return true;
47
+ const at = Date.parse(asOf);
48
+ if (!Number.isFinite(at)) return true;
49
+ const validFrom = parseTime(record.validFrom || record.valid_from);
50
+ const validTo = parseTime(record.validTo || record.valid_to);
51
+ const staleAfter = parseTime(record.staleAfter || record.stale_after);
52
+ if (validFrom !== null && validFrom > at) return false;
53
+ if (validTo !== null && validTo <= at) return false;
54
+ if (staleAfter !== null && staleAfter <= at) return false;
55
+ return true;
56
+ }
57
+
58
+ function isActiveBootstrap(record, opts = {}) {
59
+ return (record.status || 'candidate') === 'active'
60
+ && (record.visibleInBootstrap ?? record.visible_in_bootstrap) === true
61
+ && isWithinTime(record, opts.asOf);
62
+ }
63
+
64
+ function resolveApplicableRecords(records = [], opts = {}) {
65
+ const activeScopePath = opts.activeScopePath || (opts.activeScopeKey ? [opts.activeScopeKey] : ['global']);
66
+ const activeScope = opts.activeScopeKey || activeScopePath[activeScopePath.length - 1] || null;
67
+ const position = new Map(activeScopePath.map((key, idx) => [key, idx]));
68
+ const additive = [];
69
+ const winners = new Map();
70
+
71
+ for (const record of records) {
72
+ const recScope = scopeKey(record);
73
+ const mode = inheritanceMode(record);
74
+ const isExact = activeScope && recScope === activeScope;
75
+ const isInPath = position.has(recScope);
76
+ if (mode === 'non_inheritable' && !isExact) continue;
77
+ if (mode !== 'non_inheritable' && activeScopePath.length > 0 && !isInPath) continue;
78
+
79
+ if (mode === 'additive') {
80
+ additive.push(record);
81
+ continue;
82
+ }
83
+
84
+ const key = canonicalKey(record);
85
+ const existing = winners.get(key);
86
+ if (!existing) {
87
+ winners.set(key, record);
88
+ continue;
89
+ }
90
+
91
+ const currentPos = position.get(recScope) ?? -1;
92
+ const existingPos = position.get(scopeKey(existing)) ?? -1;
93
+ if (currentPos > existingPos) winners.set(key, record);
94
+ }
95
+
96
+ return [...winners.values(), ...additive];
97
+ }
98
+
99
+ function sortForBootstrap(a, b) {
100
+ const aType = TYPE_PRIORITY[a.memoryType || a.memory_type] ?? 99;
101
+ const bType = TYPE_PRIORITY[b.memoryType || b.memory_type] ?? 99;
102
+ if (aType !== bType) return aType - bType;
103
+
104
+ const aAuth = AUTHORITY_PRIORITY[a.authority] ?? 99;
105
+ const bAuth = AUTHORITY_PRIORITY[b.authority] ?? 99;
106
+ if (aAuth !== bAuth) return aAuth - bAuth;
107
+
108
+ const aAccepted = Date.parse(a.acceptedAt || a.accepted_at || '') || 0;
109
+ const bAccepted = Date.parse(b.acceptedAt || b.accepted_at || '') || 0;
110
+ if (aAccepted !== bAccepted) return bAccepted - aAccepted;
111
+
112
+ return canonicalKey(a).localeCompare(canonicalKey(b));
113
+ }
114
+
115
+ function lineFor(record) {
116
+ const type = record.memoryType || record.memory_type || 'memory';
117
+ const text = record.summary || record.title || '';
118
+ return `- ${type}: ${String(text).trim()}`;
119
+ }
120
+
121
+ function buildText(records, meta) {
122
+ const lines = records.map(lineFor);
123
+ return [
124
+ `<memory-bootstrap memories="${records.length}" overflow="${meta.overflow}" degraded="${meta.degraded}">`,
125
+ ...lines,
126
+ '</memory-bootstrap>',
127
+ ].join('\n');
128
+ }
129
+
130
+ function buildMemoryBootstrap(records = [], opts = {}) {
131
+ const maxChars = Math.max(120, opts.maxChars || 4000);
132
+ const active = resolveApplicableRecords(
133
+ records.filter(record => isActiveBootstrap(record, opts)),
134
+ opts,
135
+ ).sort(sortForBootstrap);
136
+
137
+ const meta = {
138
+ overflow: false,
139
+ degraded: false,
140
+ maxChars,
141
+ count: active.length,
142
+ };
143
+
144
+ let selected = active.slice();
145
+ let text = buildText(selected, meta);
146
+ while (text.length > maxChars && selected.length > 1) {
147
+ selected = selected.slice(0, -1);
148
+ meta.overflow = true;
149
+ meta.degraded = true;
150
+ text = buildText(selected, meta);
151
+ }
152
+
153
+ if (text.length > maxChars) {
154
+ meta.overflow = true;
155
+ meta.degraded = true;
156
+ }
157
+
158
+ const structured = {
159
+ memories: selected,
160
+ meta: { ...meta, count: selected.length },
161
+ };
162
+
163
+ if (opts.format === 'text') return { ...structured, text };
164
+ if (opts.format === 'both' || opts.format === undefined) return { ...structured, text };
165
+ return structured;
166
+ }
167
+
168
+ function createMemoryBootstrap({ records }) {
169
+ async function bootstrap(opts = {}) {
170
+ const rows = await records.listActive({
171
+ tenantId: opts.tenantId,
172
+ scopeId: opts.scopeId,
173
+ scopeKeys: opts.activeScopePath || (opts.activeScopeKey ? [opts.activeScopeKey] : undefined),
174
+ visibleInBootstrap: true,
175
+ asOf: opts.asOf,
176
+ limit: opts.limit || 50,
177
+ });
178
+ return buildMemoryBootstrap(rows, opts);
179
+ }
180
+
181
+ return { bootstrap };
182
+ }
183
+
184
+ module.exports = {
185
+ buildMemoryBootstrap,
186
+ resolveApplicableRecords,
187
+ createMemoryBootstrap,
188
+ };