@shadowforge0/aquifer-memory 1.8.1 → 1.9.1

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 (57) hide show
  1. package/.env.example +1 -0
  2. package/README.md +82 -26
  3. package/README_CN.md +33 -23
  4. package/README_TW.md +25 -24
  5. package/aquifer.config.example.json +2 -1
  6. package/consumers/cli.js +587 -33
  7. package/consumers/codex-active-checkpoint.js +3 -1
  8. package/consumers/codex-current-memory.js +10 -6
  9. package/consumers/codex.js +6 -3
  10. package/consumers/default/daily-entries.js +2 -2
  11. package/consumers/default/index.js +40 -30
  12. package/consumers/default/prompts/summary.js +2 -2
  13. package/consumers/mcp.js +56 -46
  14. package/consumers/openclaw-ext/index.js +65 -7
  15. package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
  16. package/consumers/openclaw-ext/package.json +1 -1
  17. package/consumers/openclaw-install.js +326 -0
  18. package/consumers/openclaw-plugin.js +105 -24
  19. package/consumers/shared/compat-recall.js +101 -0
  20. package/consumers/shared/config.js +2 -0
  21. package/consumers/shared/openclaw-product-tools.js +130 -0
  22. package/consumers/shared/recall-format.js +2 -2
  23. package/core/aquifer.js +553 -41
  24. package/core/backends/local.js +169 -1
  25. package/core/doctor.js +924 -0
  26. package/core/finalization-inspector.js +164 -0
  27. package/core/finalization-review.js +88 -42
  28. package/core/interface.js +629 -0
  29. package/core/mcp-manifest.js +11 -3
  30. package/core/memory-bootstrap.js +25 -27
  31. package/core/memory-consolidation.js +564 -42
  32. package/core/memory-explain.js +593 -0
  33. package/core/memory-promotion.js +392 -55
  34. package/core/memory-recall.js +75 -71
  35. package/core/memory-records.js +107 -108
  36. package/core/memory-review.js +891 -0
  37. package/core/memory-serving.js +61 -4
  38. package/core/memory-type-policy.js +298 -0
  39. package/core/operator-observability.js +249 -0
  40. package/core/postgres-migrations.js +22 -0
  41. package/core/session-checkpoint-producer.js +3 -1
  42. package/core/session-checkpoints.js +1 -1
  43. package/core/session-finalization.js +78 -3
  44. package/core/storage.js +124 -8
  45. package/docs/getting-started.md +50 -4
  46. package/docs/setup.md +163 -24
  47. package/package.json +5 -4
  48. package/schema/004-completion.sql +4 -4
  49. package/schema/010-v1-finalization-review.sql +72 -0
  50. package/schema/019-v1-memory-review-resolutions.sql +53 -0
  51. package/schema/020-v1-assistant-shaping-memory.sql +30 -0
  52. package/scripts/backfill-canonical-key.js +1 -1
  53. package/scripts/codex-checkpoint-commands.js +28 -0
  54. package/scripts/codex-checkpoint-runtime.js +109 -0
  55. package/scripts/codex-recovery.js +16 -4
  56. package/scripts/diagnose-fts-zh.js +1 -1
  57. package/scripts/extract-insights-from-recent-sessions.js +4 -4
@@ -1,12 +1,53 @@
1
1
  'use strict';
2
2
 
3
3
  function splitScopePath(value) {
4
- if (Array.isArray(value)) return value.map(v => String(v).trim()).filter(Boolean);
4
+ if (Array.isArray(value)) {
5
+ const parts = value.map(v => String(v).trim()).filter(Boolean);
6
+ return parts.length > 0 ? parts : null;
7
+ }
5
8
  if (typeof value !== 'string') return null;
6
9
  const parts = value.split(',').map(v => v.trim()).filter(Boolean);
7
10
  return parts.length > 0 ? parts : null;
8
11
  }
9
12
 
13
+ function uniqueList(values = []) {
14
+ const seen = new Set();
15
+ const out = [];
16
+ for (const value of values) {
17
+ const key = String(value || '').trim();
18
+ if (!key || seen.has(key)) continue;
19
+ seen.add(key);
20
+ out.push(key);
21
+ }
22
+ return out;
23
+ }
24
+
25
+ function normalizeScopeList(value) {
26
+ return uniqueList(splitScopePath(value) || []);
27
+ }
28
+
29
+ function requestedScopeKeys(opts = {}) {
30
+ const fromPath = normalizeScopeList(opts.activeScopePath);
31
+ const keys = [];
32
+ if (fromPath.length > 0) keys.push(...fromPath);
33
+ if (opts.activeScopeKey) keys.push(opts.activeScopeKey);
34
+ if (opts.scopeKey) keys.push(opts.scopeKey);
35
+ if (opts.resolvedScopeKey) keys.push(opts.resolvedScopeKey);
36
+ if (opts.scopeKeys) keys.push(...normalizeScopeList(opts.scopeKeys));
37
+ return uniqueList(keys);
38
+ }
39
+
40
+ function assertAllowedScopeRequest(opts = {}) {
41
+ const allowed = normalizeScopeList(opts.allowedScopeKeys);
42
+ if (allowed.length === 0) return;
43
+ const allowedSet = new Set(allowed);
44
+ const requested = requestedScopeKeys(opts);
45
+ const denied = requested.filter(key => !allowedSet.has(key));
46
+ if (denied.length > 0) {
47
+ throw new Error(`Requested memory scope is outside allowedScopeKeys: ${denied.join(', ')}`);
48
+ }
49
+ }
50
+
10
51
  function hasEvidenceBoundary(opts = {}) {
11
52
  return Boolean(
12
53
  opts.agentId
@@ -89,7 +130,13 @@ function normalizeCuratedRecallRow(row = {}) {
89
130
  function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
90
131
  const servingMode = memoryCfg.servingMode || env.AQUIFER_MEMORY_SERVING_MODE || 'legacy';
91
132
  const defaultActiveScopeKey = memoryCfg.activeScopeKey || null;
92
- const defaultActiveScopePath = splitScopePath(memoryCfg.activeScopePath || null);
133
+ const configuredActiveScopePath = splitScopePath(memoryCfg.activeScopePath || null);
134
+ const defaultActiveScopePath = configuredActiveScopePath
135
+ || uniqueList(['global', defaultActiveScopeKey].filter(Boolean));
136
+ const configuredAllowedScopeKeys = normalizeScopeList(memoryCfg.allowedScopeKeys || env.AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS || null);
137
+ const defaultAllowedScopeKeys = configuredAllowedScopeKeys.length > 0
138
+ ? configuredAllowedScopeKeys
139
+ : defaultActiveScopePath;
93
140
 
94
141
  function resolveMode(opts = {}) {
95
142
  const mode = opts.memoryMode || opts.servingMode || servingMode;
@@ -100,12 +147,18 @@ function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
100
147
 
101
148
  function withDefaultScope(opts = {}) {
102
149
  const next = { ...opts };
103
- if (!next.activeScopePath && defaultActiveScopePath) next.activeScopePath = defaultActiveScopePath;
150
+ const scopeIdOnly = next.scopeId && !next.activeScopePath && !next.activeScopeKey;
151
+ if (!next.allowedScopeKeys && defaultAllowedScopeKeys) {
152
+ next.allowedScopeKeys = defaultAllowedScopeKeys;
153
+ }
154
+ if (!scopeIdOnly && !next.activeScopePath && defaultActiveScopePath) next.activeScopePath = defaultActiveScopePath;
104
155
  if (Array.isArray(next.activeScopePath) && next.activeScopePath.length > 0) {
105
156
  if (!next.activeScopeKey) next.activeScopeKey = next.activeScopePath[next.activeScopePath.length - 1];
157
+ assertAllowedScopeRequest(next);
106
158
  return next;
107
159
  }
108
- if (!next.activeScopeKey && defaultActiveScopeKey) next.activeScopeKey = defaultActiveScopeKey;
160
+ if (!scopeIdOnly && !next.activeScopeKey && defaultActiveScopeKey) next.activeScopeKey = defaultActiveScopeKey;
161
+ assertAllowedScopeRequest(next);
109
162
  return next;
110
163
  }
111
164
 
@@ -114,6 +167,7 @@ function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
114
167
  assertCuratedRecallOpts,
115
168
  defaultActiveScopeKey,
116
169
  defaultActiveScopePath,
170
+ defaultAllowedScopeKeys,
117
171
  hasEvidenceBoundary,
118
172
  normalizeCuratedRecallRow,
119
173
  resolveMode,
@@ -123,10 +177,13 @@ function createMemoryServingRuntime(memoryCfg = {}, env = process.env) {
123
177
  }
124
178
 
125
179
  module.exports = {
180
+ assertAllowedScopeRequest,
126
181
  assertCuratedBootstrapOpts,
127
182
  assertCuratedRecallOpts,
128
183
  createMemoryServingRuntime,
129
184
  hasEvidenceBoundary,
130
185
  normalizeCuratedRecallRow,
186
+ normalizeScopeList,
187
+ requestedScopeKeys,
131
188
  splitScopePath,
132
189
  };
@@ -0,0 +1,298 @@
1
+ 'use strict';
2
+
3
+ const MEMORY_TYPE_POLICY = Object.freeze({
4
+ assistant_shaping: {
5
+ label: '回答塑形',
6
+ active: true,
7
+ defaultInheritance: 'defaultable',
8
+ defaultAspect: 'assistant_shaping',
9
+ bootstrapPriority: 0,
10
+ recallRank: 95,
11
+ runtimeContract: true,
12
+ },
13
+ constraint: {
14
+ label: '限制',
15
+ active: true,
16
+ defaultInheritance: 'additive',
17
+ defaultAspect: 'constraint',
18
+ bootstrapPriority: 1,
19
+ recallRank: 80,
20
+ },
21
+ state: {
22
+ label: '狀態',
23
+ active: true,
24
+ defaultInheritance: 'defaultable',
25
+ defaultAspect: 'state',
26
+ bootstrapPriority: 2,
27
+ recallRank: 60,
28
+ },
29
+ open_loop: {
30
+ label: '未完成',
31
+ active: true,
32
+ defaultInheritance: 'non_inheritable',
33
+ defaultAspect: 'open_loop',
34
+ bootstrapPriority: 3,
35
+ recallRank: 55,
36
+ },
37
+ decision: {
38
+ label: '決策',
39
+ active: true,
40
+ defaultInheritance: 'non_inheritable',
41
+ defaultAspect: 'decision',
42
+ bootstrapPriority: 4,
43
+ recallRank: 50,
44
+ },
45
+ preference: {
46
+ label: '偏好',
47
+ active: true,
48
+ defaultInheritance: 'defaultable',
49
+ defaultAspect: 'preference',
50
+ bootstrapPriority: 5,
51
+ recallRank: 70,
52
+ },
53
+ fact: {
54
+ label: '事實',
55
+ active: true,
56
+ defaultInheritance: 'defaultable',
57
+ defaultAspect: 'fact',
58
+ bootstrapPriority: 6,
59
+ recallRank: 40,
60
+ },
61
+ conclusion: {
62
+ label: '判斷',
63
+ active: true,
64
+ defaultInheritance: 'defaultable',
65
+ defaultAspect: 'conclusion',
66
+ bootstrapPriority: 7,
67
+ recallRank: 30,
68
+ },
69
+ entity_note: {
70
+ label: '註記',
71
+ active: true,
72
+ defaultInheritance: 'defaultable',
73
+ defaultAspect: 'entity_note',
74
+ bootstrapPriority: 8,
75
+ recallRank: 20,
76
+ },
77
+ });
78
+
79
+ const AUTHORITY_POLICY = Object.freeze({
80
+ raw_transcript: { strength: 0, sortPriority: 6 },
81
+ llm_inference: { strength: 1, sortPriority: 5 },
82
+ verified_summary: { strength: 2, sortPriority: 4 },
83
+ manual: { strength: 3, sortPriority: 2 },
84
+ system: { strength: 4, sortPriority: 3 },
85
+ executable_evidence: { strength: 5, sortPriority: 1 },
86
+ user_explicit: { strength: 6, sortPriority: 0 },
87
+ });
88
+
89
+ const FEEDBACK_WEIGHT = Object.freeze({
90
+ helpful: 0.15,
91
+ confirm: 0.10,
92
+ irrelevant: -0.20,
93
+ scope_mismatch: -0.25,
94
+ stale: -0.30,
95
+ incorrect: -0.50,
96
+ });
97
+
98
+ const ASSISTANT_SHAPING_KINDS = Object.freeze([
99
+ 'response_style',
100
+ 'interaction_boundary',
101
+ 'retrieval_policy',
102
+ 'tool_routing',
103
+ 'memory_policy',
104
+ 'user_preference',
105
+ 'correction_pattern',
106
+ 'decision_style',
107
+ ]);
108
+
109
+ const ASSISTANT_SHAPING_SERVING_IMPACTS = Object.freeze([
110
+ 'changes_future_responses',
111
+ 'changes_retrieval_selection',
112
+ 'changes_bootstrap_selection',
113
+ 'changes_tool_routing',
114
+ 'changes_memory_promotion',
115
+ 'sets_user_boundary',
116
+ 'sets_assistant_behavior',
117
+ ]);
118
+
119
+ const ASSISTANT_SHAPING_PROMPT_LINES = Object.freeze([
120
+ 'Use assistant_shaping only when a memory changes durable cross-consumer runtime behavior for the user: future response style, interaction boundary, retrieval selection, tool routing, or memory promotion policy.',
121
+ 'Each assistant_shaping item must include guidance or policy text plus shapingKind, servingImpact, and userRelevance. For non-explicit-user-instruction summaries, include temporalSupport showing repeated observations, multiple sessions, or a consolidation window.',
122
+ 'Treat shapingKind and servingImpact as extensible hints, not as a closed product taxonomy. If the item only reports current task state, implementation facts, or a single answer, keep it in the ordinary memory layer instead.',
123
+ ]);
124
+
125
+ function normalizeToken(value) {
126
+ return String(value || '').trim().toLowerCase();
127
+ }
128
+
129
+ function memoryTypePolicy(type) {
130
+ return MEMORY_TYPE_POLICY[normalizeToken(type)] || null;
131
+ }
132
+
133
+ function activeMemoryTypes() {
134
+ return Object.keys(MEMORY_TYPE_POLICY).filter(type => MEMORY_TYPE_POLICY[type].active);
135
+ }
136
+
137
+ function isActiveMemoryType(type) {
138
+ const policy = memoryTypePolicy(type);
139
+ return Boolean(policy && policy.active);
140
+ }
141
+
142
+ function defaultInheritanceForMemoryType(type) {
143
+ return memoryTypePolicy(type)?.defaultInheritance || 'defaultable';
144
+ }
145
+
146
+ function defaultAspectForMemoryType(type) {
147
+ return memoryTypePolicy(type)?.defaultAspect || normalizeToken(type) || 'memory';
148
+ }
149
+
150
+ function memoryTypeLabel(type) {
151
+ return memoryTypePolicy(type)?.label || '記憶';
152
+ }
153
+
154
+ function memoryTypeBootstrapPriority(type) {
155
+ return memoryTypePolicy(type)?.bootstrapPriority ?? 99;
156
+ }
157
+
158
+ function memoryTypeRecallRank(type) {
159
+ return memoryTypePolicy(type)?.recallRank ?? 0;
160
+ }
161
+
162
+ function memoryTypeRuntimeContract(type) {
163
+ return memoryTypePolicy(type)?.runtimeContract === true;
164
+ }
165
+
166
+ function authorityPolicy(authority) {
167
+ return AUTHORITY_POLICY[normalizeToken(authority)] || null;
168
+ }
169
+
170
+ function authorityStrength(authority) {
171
+ return authorityPolicy(authority)?.strength ?? 0;
172
+ }
173
+
174
+ function authoritySortPriority(authority) {
175
+ return authorityPolicy(authority)?.sortPriority ?? 99;
176
+ }
177
+
178
+ function feedbackWeight(type) {
179
+ return FEEDBACK_WEIGHT[normalizeToken(type)] || 0;
180
+ }
181
+
182
+ function sqlLiteral(value) {
183
+ return `'${String(value).replace(/'/g, "''")}'`;
184
+ }
185
+
186
+ function caseSql(expr, entries, valueKey, fallback) {
187
+ const clauses = entries
188
+ .map(([key, policy]) => `WHEN ${sqlLiteral(key)} THEN ${policy[valueKey]}`)
189
+ .join('\n ');
190
+ return `CASE ${expr}\n ${clauses}\n ELSE ${fallback}\n END`;
191
+ }
192
+
193
+ function memoryTypeBootstrapPrioritySql(expr = 'm.memory_type') {
194
+ return caseSql(expr, Object.entries(MEMORY_TYPE_POLICY), 'bootstrapPriority', 99);
195
+ }
196
+
197
+ function memoryTypeRecallRankSql(expr = 'm.memory_type') {
198
+ const entries = Object.entries(MEMORY_TYPE_POLICY)
199
+ .map(([key, policy]) => [key, { recallWeight: (policy.recallRank || 0) / 100 }]);
200
+ return caseSql(expr, entries, 'recallWeight', 0);
201
+ }
202
+
203
+ function authoritySortPrioritySql(expr = 'm.authority') {
204
+ return caseSql(expr, Object.entries(AUTHORITY_POLICY), 'sortPriority', 99);
205
+ }
206
+
207
+ function feedbackWeightSql(expr = 'f.feedback_type') {
208
+ const entries = Object.entries(FEEDBACK_WEIGHT).map(([key, weight]) => [key, { weight }]);
209
+ return caseSql(expr, entries, 'weight', 0);
210
+ }
211
+
212
+ function memoryTypeOf(record = {}) {
213
+ return record.memoryType || record.memory_type || record.type || 'memory';
214
+ }
215
+
216
+ function memorySummaryText(record = {}) {
217
+ return String(record.summary || record.summaryText || record.title || '').replace(/\s+/g, ' ').trim();
218
+ }
219
+
220
+ function assistantShapingMetadata(record = {}) {
221
+ const payload = record.payload && typeof record.payload === 'object' ? record.payload : {};
222
+ const entries = [
223
+ ['policy', payload.policyKey || payload.policy_key || record.policyKey || record.policy_key],
224
+ ['kind', payload.shapingKind || payload.shaping_kind || payload.kind || payload.category || record.shapingKind || record.shaping_kind],
225
+ ['impact', payload.servingImpact || payload.serving_impact || payload.impact || record.servingImpact || record.serving_impact],
226
+ ];
227
+ return entries
228
+ .map(([key, value]) => [key, String(value || '').trim()])
229
+ .filter(([, value]) => value);
230
+ }
231
+
232
+ function formatRuntimeMemoryLine(record = {}, opts = {}) {
233
+ const type = memoryTypeOf(record);
234
+ const label = opts.localizedLabel === true ? memoryTypeLabel(type) : type;
235
+ const attrs = [];
236
+ const scope = record.scopeKey || record.scope_key;
237
+ const authority = record.authority;
238
+ const confidence = record.confidence || record.payload?.confidence || record.payload?.currentMemoryConfidence;
239
+ if (opts.includeScope === true && scope) attrs.push(`scope=${scope}`);
240
+ if (opts.includeAuthority === true && authority) attrs.push(`authority=${authority}`);
241
+ if (opts.includeConfidence === true && confidence) attrs.push(`confidence=${confidence}`);
242
+ const suffix = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
243
+ const metadata = type === 'assistant_shaping'
244
+ ? assistantShapingMetadata(record).map(([key, value]) => `${key}=${value}`).join(' ')
245
+ : '';
246
+ const metadataSuffix = metadata ? ` (${metadata})` : '';
247
+ return `- ${label}${suffix}: ${memorySummaryText(record)}${metadataSuffix}`;
248
+ }
249
+
250
+ function assistantShapingKindAllowed(value) {
251
+ return ASSISTANT_SHAPING_KINDS.includes(normalizeToken(value));
252
+ }
253
+
254
+ function assistantShapingServingImpactAllowed(value) {
255
+ const token = normalizeToken(value);
256
+ return assistantShapingServingImpactKnown(token)
257
+ || /^(changes|sets|routes|selects|filters|prioritizes|suppresses)_[a-z0-9_]+$/.test(token);
258
+ }
259
+
260
+ function assistantShapingServingImpactKnown(value) {
261
+ return ASSISTANT_SHAPING_SERVING_IMPACTS.includes(normalizeToken(value));
262
+ }
263
+
264
+ function assistantShapingPromptLines() {
265
+ return ASSISTANT_SHAPING_PROMPT_LINES.slice();
266
+ }
267
+
268
+ module.exports = {
269
+ ASSISTANT_SHAPING_KINDS,
270
+ ASSISTANT_SHAPING_SERVING_IMPACTS,
271
+ AUTHORITY_POLICY,
272
+ FEEDBACK_WEIGHT,
273
+ MEMORY_TYPE_POLICY,
274
+ activeMemoryTypes,
275
+ assistantShapingKindAllowed,
276
+ assistantShapingMetadata,
277
+ assistantShapingPromptLines,
278
+ assistantShapingServingImpactAllowed,
279
+ assistantShapingServingImpactKnown,
280
+ authoritySortPriority,
281
+ authoritySortPrioritySql,
282
+ authorityStrength,
283
+ defaultAspectForMemoryType,
284
+ defaultInheritanceForMemoryType,
285
+ feedbackWeight,
286
+ feedbackWeightSql,
287
+ formatRuntimeMemoryLine,
288
+ isActiveMemoryType,
289
+ memorySummaryText,
290
+ memoryTypeBootstrapPriority,
291
+ memoryTypeBootstrapPrioritySql,
292
+ memoryTypeLabel,
293
+ memoryTypeOf,
294
+ memoryTypePolicy,
295
+ memoryTypeRecallRank,
296
+ memoryTypeRecallRankSql,
297
+ memoryTypeRuntimeContract,
298
+ };
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ function parseJsonObject(value, fallback = {}) {
4
+ if (value === null || value === undefined) return fallback;
5
+ if (typeof value === 'string') {
6
+ try {
7
+ const parsed = JSON.parse(value);
8
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : fallback;
14
+ }
15
+
16
+ function rowTime(value) {
17
+ if (!value) return null;
18
+ const parsed = new Date(value);
19
+ return isNaN(parsed.getTime()) ? String(value) : parsed.toISOString();
20
+ }
21
+
22
+ function summarizeCompactionRun(row = {}) {
23
+ const output = parseJsonObject(row.output, {});
24
+ const plan = output.plan || output;
25
+ const candidates = Array.isArray(plan.candidates) ? plan.candidates : [];
26
+ const statusUpdates = Array.isArray(plan.statusUpdates || plan.status_updates)
27
+ ? (plan.statusUpdates || plan.status_updates)
28
+ : [];
29
+ return {
30
+ id: row.id,
31
+ kind: 'compaction',
32
+ status: row.status,
33
+ cadence: row.cadence,
34
+ periodStart: row.period_start,
35
+ periodEnd: row.period_end,
36
+ policyVersion: row.policy_version || null,
37
+ workerId: row.worker_id || null,
38
+ claimedAt: rowTime(row.claimed_at),
39
+ leaseExpiresAt: rowTime(row.lease_expires_at),
40
+ appliedAt: rowTime(row.applied_at),
41
+ reclaimedAt: rowTime(row.reclaimed_at),
42
+ error: row.error || null,
43
+ sourceCoverage: parseJsonObject(row.source_coverage, {}),
44
+ outputCoverage: parseJsonObject(row.output_coverage, {}),
45
+ candidateCount: candidates.length,
46
+ statusUpdateCount: statusUpdates.length,
47
+ createdAt: rowTime(row.created_at),
48
+ updatedAt: rowTime(row.updated_at),
49
+ };
50
+ }
51
+
52
+ function summarizeCheckpointRun(row = {}, sourceCount = 0) {
53
+ const payload = parseJsonObject(row.checkpoint_payload, {});
54
+ return {
55
+ id: row.id,
56
+ kind: 'checkpoint',
57
+ status: row.status,
58
+ scopeId: row.scope_id,
59
+ checkpointKey: row.checkpoint_key,
60
+ fromFinalizationIdExclusive: row.from_finalization_id_exclusive,
61
+ toFinalizationIdInclusive: row.to_finalization_id_inclusive,
62
+ windowStart: rowTime(row.window_start),
63
+ windowEnd: rowTime(row.window_end),
64
+ claimedAt: rowTime(row.claimed_at),
65
+ finalizedAt: rowTime(row.finalized_at),
66
+ error: row.error || null,
67
+ sourceCount,
68
+ checkpointPayloadKeys: Object.keys(payload).sort(),
69
+ createdAt: rowTime(row.created_at),
70
+ updatedAt: rowTime(row.updated_at),
71
+ };
72
+ }
73
+
74
+ function sourceValue(row = {}, key) {
75
+ const metadata = parseJsonObject(row.metadata, {});
76
+ return row[key] ?? metadata[key] ?? null;
77
+ }
78
+
79
+ function tableMissingError(error) {
80
+ return error && error.code === '42P01';
81
+ }
82
+
83
+ function createOperatorObservability({ pool, schema, defaultTenantId = 'default' }) {
84
+ async function compactionStatus(input = {}) {
85
+ const tenantId = input.tenantId || defaultTenantId;
86
+ const limit = Math.max(1, Math.min(50, input.limit || 10));
87
+ try {
88
+ const [latest, stale, counts] = await Promise.all([
89
+ pool.query(
90
+ `SELECT *
91
+ FROM ${schema}.compaction_runs
92
+ WHERE tenant_id = $1
93
+ ORDER BY COALESCE(applied_at, created_at) DESC, id DESC
94
+ LIMIT $2`,
95
+ [tenantId, limit],
96
+ ),
97
+ pool.query(
98
+ `SELECT *
99
+ FROM ${schema}.compaction_runs
100
+ WHERE tenant_id = $1
101
+ AND status = 'applying'
102
+ AND lease_expires_at IS NOT NULL
103
+ AND lease_expires_at < transaction_timestamp()
104
+ ORDER BY lease_expires_at ASC, id ASC
105
+ LIMIT $2`,
106
+ [tenantId, limit],
107
+ ),
108
+ pool.query(
109
+ `SELECT status, COUNT(*)::int AS count
110
+ FROM ${schema}.compaction_runs
111
+ WHERE tenant_id = $1
112
+ GROUP BY status`,
113
+ [tenantId],
114
+ ),
115
+ ]);
116
+ return {
117
+ available: true,
118
+ latest: latest.rows.map(summarizeCompactionRun),
119
+ staleClaims: stale.rows.map(summarizeCompactionRun),
120
+ statusCounts: Object.fromEntries(counts.rows.map(row => [row.status, row.count])),
121
+ };
122
+ } catch (error) {
123
+ if (tableMissingError(error)) {
124
+ return { available: false, latest: [], staleClaims: [], statusCounts: {}, error: 'compaction_runs table is missing' };
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ async function checkpointStatus(input = {}) {
131
+ const tenantId = input.tenantId || defaultTenantId;
132
+ const limit = Math.max(1, Math.min(50, input.limit || 10));
133
+ try {
134
+ const [latest, counts] = await Promise.all([
135
+ pool.query(
136
+ `SELECT c.*, COUNT(s.id)::int AS source_count
137
+ FROM ${schema}.checkpoint_runs c
138
+ LEFT JOIN ${schema}.checkpoint_run_sources s
139
+ ON s.tenant_id = c.tenant_id
140
+ AND s.checkpoint_run_id = c.id
141
+ WHERE c.tenant_id = $1
142
+ GROUP BY c.id
143
+ ORDER BY c.updated_at DESC, c.id DESC
144
+ LIMIT $2`,
145
+ [tenantId, limit],
146
+ ),
147
+ pool.query(
148
+ `SELECT status, COUNT(*)::int AS count
149
+ FROM ${schema}.checkpoint_runs
150
+ WHERE tenant_id = $1
151
+ GROUP BY status`,
152
+ [tenantId],
153
+ ),
154
+ ]);
155
+ return {
156
+ available: true,
157
+ latest: latest.rows.map(row => summarizeCheckpointRun(row, row.source_count || 0)),
158
+ statusCounts: Object.fromEntries(counts.rows.map(row => [row.status, row.count])),
159
+ };
160
+ } catch (error) {
161
+ if (tableMissingError(error)) {
162
+ return { available: false, latest: [], statusCounts: {}, error: 'checkpoint_runs table is missing' };
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ async function status(input = {}) {
169
+ const [compaction, checkpoint] = await Promise.all([
170
+ compactionStatus(input),
171
+ checkpointStatus(input),
172
+ ]);
173
+ return {
174
+ readOnly: true,
175
+ compaction,
176
+ checkpoint,
177
+ };
178
+ }
179
+
180
+ async function inspect(input = {}) {
181
+ const tenantId = input.tenantId || defaultTenantId;
182
+ const runId = input.runId || input.id;
183
+ if (!runId) throw new Error('runId is required');
184
+ const kind = input.kind || 'compaction';
185
+ if (kind === 'compaction') {
186
+ const result = await pool.query(
187
+ `SELECT *
188
+ FROM ${schema}.compaction_runs
189
+ WHERE tenant_id = $1
190
+ AND id = $2
191
+ LIMIT 1`,
192
+ [tenantId, runId],
193
+ );
194
+ if (!result.rows[0]) throw new Error('Compaction run not found');
195
+ return {
196
+ readOnly: true,
197
+ run: summarizeCompactionRun(result.rows[0]),
198
+ };
199
+ }
200
+ if (kind === 'checkpoint') {
201
+ const [run, sources] = await Promise.all([
202
+ pool.query(
203
+ `SELECT *
204
+ FROM ${schema}.checkpoint_runs
205
+ WHERE tenant_id = $1
206
+ AND id = $2
207
+ LIMIT 1`,
208
+ [tenantId, runId],
209
+ ),
210
+ pool.query(
211
+ `SELECT id, finalization_id, source_index, session_id, transcript_hash, finalized_at, metadata
212
+ FROM ${schema}.checkpoint_run_sources
213
+ WHERE tenant_id = $1
214
+ AND checkpoint_run_id = $2
215
+ ORDER BY source_index ASC, id ASC`,
216
+ [tenantId, runId],
217
+ ),
218
+ ]);
219
+ if (!run.rows[0]) throw new Error('Checkpoint run not found');
220
+ return {
221
+ readOnly: true,
222
+ run: summarizeCheckpointRun(run.rows[0], sources.rows.length),
223
+ sources: sources.rows.map(row => ({
224
+ id: row.id,
225
+ finalizationId: row.finalization_id,
226
+ sourceIndex: row.source_index,
227
+ sessionId: row.session_id,
228
+ agentId: sourceValue(row, 'agentId') || sourceValue(row, 'agent_id'),
229
+ source: sourceValue(row, 'source'),
230
+ status: sourceValue(row, 'status'),
231
+ transcriptHashPrefix: row.transcript_hash ? String(row.transcript_hash).slice(0, 12) : null,
232
+ finalizedAt: rowTime(row.finalized_at),
233
+ })),
234
+ };
235
+ }
236
+ throw new Error('kind must be compaction or checkpoint');
237
+ }
238
+
239
+ return {
240
+ status,
241
+ inspect,
242
+ };
243
+ }
244
+
245
+ module.exports = {
246
+ createOperatorObservability,
247
+ summarizeCompactionRun,
248
+ summarizeCheckpointRun,
249
+ };
@@ -81,6 +81,26 @@ const MIGRATION_PLAN = [
81
81
  { table: 'finalization_candidates', column: 'candidate_hash' },
82
82
  ],
83
83
  },
84
+ {
85
+ id: '019-v1-memory-review-resolutions',
86
+ file: '019-v1-memory-review-resolutions.sql',
87
+ always: true,
88
+ signature: [
89
+ 'memory_review_resolutions',
90
+ { index: 'idx_memory_review_resolutions_memory_latest' },
91
+ { index: 'idx_memory_review_resolutions_canonical_latest' },
92
+ { index: 'idx_memory_review_resolutions_defer_until' },
93
+ { index: 'idx_feedback_memory_review_latest' },
94
+ ],
95
+ },
96
+ {
97
+ id: '020-v1-assistant-shaping-memory',
98
+ file: '020-v1-assistant-shaping-memory.sql',
99
+ always: true,
100
+ signature: [
101
+ { index: 'idx_memory_records_assistant_shaping_bootstrap' },
102
+ ],
103
+ },
84
104
  ];
85
105
 
86
106
  function createPostgresMigrationRuntime(opts = {}) {
@@ -296,6 +316,8 @@ function createPostgresMigrationRuntime(opts = {}) {
296
316
  ['016-v1-evidence-ref-multi-item.sql', '016-v1-evidence-ref-multi-item'],
297
317
  ['017-v1-memory-record-embeddings.sql', '017-v1-memory-record-embeddings'],
298
318
  ['018-v1-finalization-candidate-envelope.sql', '018-v1-finalization-candidate-envelope'],
319
+ ['019-v1-memory-review-resolutions.sql', '019-v1-memory-review-resolutions'],
320
+ ['020-v1-assistant-shaping-memory.sql', '020-v1-assistant-shaping-memory'],
299
321
  ]) {
300
322
  await client.query(loadSql(migration[0], schema));
301
323
  ddlExecuted.push(migration[1]);