@shadowforge0/aquifer-memory 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,9 +77,15 @@ Keep `AQUIFER_MEMORY_SERVING_MODE=legacy` for first rollout. Switch to `curated`
77
77
  | Start the MCP server | `npx aquifer mcp` |
78
78
  | Search memory manually | `npx aquifer recall "auth middleware"` |
79
79
  | Plan curated memory compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
80
+ | Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
81
+ | Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
80
82
  | Inspect storage health | `npx aquifer stats` |
81
83
  | Enrich pending sessions | `npx aquifer backfill` |
82
84
 
85
+ Timer synthesis output is candidate material until an operator applies it with
86
+ `--promote-candidates`; it does not become active curated memory from the
87
+ prompt or summary file alone.
88
+
83
89
  Need LLM summarization, the knowledge graph, OpenAI embeddings, reranking, or operations details? See [docs/setup.md](docs/setup.md) and [Environment Variables](#environment-variables).
84
90
 
85
91
  ---
package/README_CN.md CHANGED
@@ -111,6 +111,23 @@ Claude Code、Claude Desktop 或任何支持 MCP 的 client——放进 `.mcp.js
111
111
 
112
112
  第一轮 rollout 先保持 `AQUIFER_MEMORY_SERVING_MODE=legacy`。只有在你要让 `session_recall` 和 `session_bootstrap` 提供 active curated memory 时,才切到 `curated`;`evidence_recall` 会保留为显式 audit/debug 路径。要 rollback 只要把 env 或 config 切回 `legacy`。
113
113
 
114
+ ### Common commands
115
+
116
+ | Goal | Command |
117
+ |---|---|
118
+ | Verify setup | `npx aquifer quickstart` |
119
+ | Start the MCP server | `npx aquifer mcp` |
120
+ | Search memory manually | `npx aquifer recall "auth middleware"` |
121
+ | Plan curated memory compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
122
+ | Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
123
+ | Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
124
+ | Inspect storage health | `npx aquifer stats` |
125
+ | Enrich pending sessions | `npx aquifer backfill` |
126
+
127
+ Timer synthesis output is candidate material until an operator applies it with
128
+ `--promote-candidates`; it does not become active curated memory from the
129
+ prompt or summary file alone.
130
+
114
131
  需要 LLM 摘要、知识图谱、OpenAI embedding 或 reranker?往下看 [环境变量](#环境变量) 和 [docs/setup.md](docs/setup.md)。
115
132
 
116
133
  ---
package/README_TW.md CHANGED
@@ -77,9 +77,13 @@ Claude Code、Claude Desktop 或任何支援 MCP 的 client——放進 `.mcp.js
77
77
  | 啟動 MCP server | `npx aquifer mcp` |
78
78
  | 手動查記憶 | `npx aquifer recall "auth middleware"` |
79
79
  | 規劃 curated memory 壓縮 | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
80
+ | 產生 timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
81
+ | 套用已審核的 timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
80
82
  | 看儲存狀態 | `npx aquifer stats` |
81
83
  | 補跑 pending session | `npx aquifer backfill` |
82
84
 
85
+ Timer synthesis output 在 operator 用 `--promote-candidates` apply 前都只是 candidate material;光有 prompt 或 summary file 不會變成 active curated memory。
86
+
83
87
  需要 LLM 摘要、知識圖譜、OpenAI embedding、reranker 或維運細節,就往下看 [環境變數](#環境變數) 跟 [docs/setup.md](docs/setup.md)。
84
88
 
85
89
  ---
package/consumers/cli.js CHANGED
@@ -51,6 +51,38 @@ function parseCsvList(value) {
51
51
  return parts.length > 0 ? parts : undefined;
52
52
  }
53
53
 
54
+ function readJsonFlagValue(value, label) {
55
+ if (!value || value === true) {
56
+ throw new Error(`${label} requires a JSON value`);
57
+ }
58
+ try {
59
+ return JSON.parse(String(value));
60
+ } catch (err) {
61
+ throw new Error(`${label} must be valid JSON: ${err.message}`);
62
+ }
63
+ }
64
+
65
+ function readJsonFlagFile(filePath, label) {
66
+ if (!filePath || filePath === true) {
67
+ throw new Error(`${label} requires a file path`);
68
+ }
69
+ try {
70
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
71
+ } catch (err) {
72
+ throw new Error(`${label} must point to valid JSON: ${err.message}`);
73
+ }
74
+ }
75
+
76
+ function readSynthesisSummaryFromFlags(flags = {}) {
77
+ if (flags['synthesis-summary-file']) {
78
+ return readJsonFlagFile(flags['synthesis-summary-file'], '--synthesis-summary-file');
79
+ }
80
+ if (flags['synthesis-summary']) {
81
+ return readJsonFlagValue(flags['synthesis-summary'], '--synthesis-summary');
82
+ }
83
+ return undefined;
84
+ }
85
+
54
86
  function hasQuickstartEmbedConfig(env) {
55
87
  return !!(
56
88
  env.EMBED_PROVIDER
@@ -145,6 +177,7 @@ function parseArgs(argv) {
145
177
  'out', 'active-scope-key', 'active-scope-path', 'cadence', 'period-start', 'period-end',
146
178
  'policy-version', 'worker-id', 'apply-token', 'claim-lease-seconds', 'snapshot-as-of',
147
179
  'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
180
+ 'synthesis-summary', 'synthesis-summary-file',
148
181
  ]);
149
182
  for (let i = 0; i < argv.length; i++) {
150
183
  if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
@@ -518,6 +551,7 @@ async function cmdOperator(aquifer, args) {
518
551
  const cadence = args.flags.cadence
519
552
  || (cadenceVerbs.has(operatorVerb) ? operatorVerb : args._[2])
520
553
  || 'manual';
554
+ const synthesisSummary = readSynthesisSummaryFromFlags(args.flags);
521
555
  const result = await aquifer.memory.consolidation.runJob({
522
556
  job: 'compaction',
523
557
  cadence,
@@ -531,8 +565,17 @@ async function cmdOperator(aquifer, args) {
531
565
  : undefined,
532
566
  snapshotAsOf: args.flags['snapshot-as-of'] || undefined,
533
567
  scopeKeys: parseCsvList(args.flags['scope-keys'] || args.flags['scope-key']),
568
+ scopeKind: args.flags['scope-kind'] || undefined,
569
+ scopeKey: args.flags['scope-key'] || undefined,
570
+ contextKey: args.flags['context-key'] || undefined,
571
+ topicKey: args.flags['topic-key'] || undefined,
572
+ activeScopeKey: args.flags['active-scope-key'] || undefined,
573
+ activeScopePath: parseScopePath(args.flags['active-scope-path']),
534
574
  limit: parsePositiveInt(args.flags.limit, 1000),
535
575
  apply: args.flags.apply === true,
576
+ promoteCandidates: args.flags['promote-candidates'] === true,
577
+ includeSynthesisPrompt: args.flags['include-synthesis-prompt'] === true,
578
+ synthesisSummary,
536
579
  });
537
580
 
538
581
  if (args.flags.json) {
@@ -542,7 +585,14 @@ async function cmdOperator(aquifer, args) {
542
585
 
543
586
  console.log(`${result.dryRun ? 'Planned' : 'Executed'} ${result.cadence} compaction window ${result.periodStart} -> ${result.periodEnd}`);
544
587
  console.log(`Snapshot: ${result.snapshotCount} active rows${result.snapshotTruncated ? ' (snapshot limit reached)' : ''}`);
545
- console.log(`Plan: ${result.plan.statusUpdates.length} lifecycle updates, ${result.plan.candidates.length} aggregate candidates`);
588
+ console.log(`Plan: ${result.plan.statusUpdates.length} lifecycle updates, ${result.plan.candidates.length} candidates`);
589
+ if (result.synthesisPrompt) {
590
+ console.log('\nSynthesis prompt:\n');
591
+ console.log(result.synthesisPrompt);
592
+ }
593
+ if (result.promotionReview) {
594
+ console.log(`\n${result.promotionReview}`);
595
+ }
546
596
  if (result.dryRun) {
547
597
  console.log('Mode: dry-run only. Re-run with --apply to write compaction_runs and lifecycle changes.');
548
598
  return;
@@ -611,6 +661,7 @@ Commands:
611
661
  stats Show database statistics
612
662
  export Export sessions as JSONL
613
663
  bootstrap Show recent session context (for new session start)
664
+ codex-recovery ... Inspect or run Codex SessionStart recovery flow
614
665
  ingest-opencode Import sessions from OpenCode's local SQLite DB
615
666
  mcp Start MCP server
616
667
 
@@ -642,7 +693,12 @@ Options:
642
693
  --period-start ISO Compaction window start
643
694
  --period-end ISO Compaction window end
644
695
  --apply Apply compaction; default is dry-run
696
+ --promote-candidates Promote compaction/synthesis candidates when applying
697
+ --include-synthesis-prompt Include timer synthesis prompt in operator output
698
+ --synthesis-summary JSON Timer synthesis summary JSON to attach to a compaction plan
699
+ --synthesis-summary-file P Read timer synthesis summary JSON from file
645
700
  --scope-key A,B Limit compaction snapshot to specific scope keys
701
+ --scope-kind KIND Explicit synthesis target scope kind
646
702
  --snapshot-as-of ISO Read active snapshot as of a specific instant
647
703
  --claim-lease-seconds N Override compaction apply lease
648
704
  --input PATH Archive distill input JSON path
@@ -652,7 +708,9 @@ Options:
652
708
 
653
709
  Operator examples:
654
710
  aquifer operator compaction daily --json
711
+ aquifer operator compaction daily --include-synthesis-prompt --json
655
712
  aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
713
+ aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json
656
714
  aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
657
715
  process.exit(0);
658
716
  }
@@ -661,6 +719,11 @@ Operator examples:
661
719
  const args = parseArgs(argv);
662
720
  let quickstartDetected = {};
663
721
 
722
+ if (command === 'codex-recovery') {
723
+ await require('../scripts/codex-recovery').main(argv.slice(1));
724
+ return;
725
+ }
726
+
664
727
  // MCP: delegate to mcp.js
665
728
  if (command === 'mcp') {
666
729
  require('./mcp').main().catch(err => {
@@ -772,7 +835,11 @@ Operator examples:
772
835
  }
773
836
 
774
837
  // Export for testing; execute only when run directly
775
- module.exports = { parseArgs };
838
+ module.exports = {
839
+ parseArgs,
840
+ cmdOperator,
841
+ readSynthesisSummaryFromFlags,
842
+ };
776
843
 
777
844
  if (require.main === module) {
778
845
  main().catch(err => {
@@ -1,6 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { finalizeTranscriptView } = require('./codex');
3
+ const {
4
+ buildFinalizationPrompt,
5
+ finalizeTranscriptView,
6
+ resolveCurrentMemoryForFinalization,
7
+ compactCurrentMemorySnapshot,
8
+ } = require('./codex');
4
9
  const { buildFinalizationReview } = require('../core/finalization-review');
5
10
 
6
11
  function normalizeText(value) {
@@ -95,25 +100,123 @@ function buildHandoffMetadata(payload = {}) {
95
100
  };
96
101
  }
97
102
 
103
+ function resolveHandoffSummary(payload = {}, opts = {}) {
104
+ const synthesisSummary = opts.synthesisSummary
105
+ || opts.handoffSynthesisSummary
106
+ || payload.synthesisSummary
107
+ || payload.handoffSynthesisSummary
108
+ || null;
109
+ if (synthesisSummary) {
110
+ return {
111
+ summary: synthesisSummary,
112
+ candidates: Array.isArray(synthesisSummary.candidates) ? synthesisSummary.candidates : undefined,
113
+ usedSynthesis: true,
114
+ };
115
+ }
116
+ return {
117
+ summary: opts.summary || payload.summary || {
118
+ summaryText: opts.summaryText || payload.summaryText,
119
+ structuredSummary: opts.structuredSummary || payload.structuredSummary,
120
+ },
121
+ candidates: Array.isArray(opts.candidates) ? opts.candidates : undefined,
122
+ usedSynthesis: false,
123
+ };
124
+ }
125
+
126
+ function formatHandoffContextBlock(metadata = {}) {
127
+ const handoff = metadata.handoff || {};
128
+ const lines = [
129
+ `<handoff_context source="${metadata.source || 'codex_handoff'}">`,
130
+ `title: ${handoff.title || 'untitled'}`,
131
+ `overview: ${handoff.overview || 'none'}`,
132
+ `status: ${handoff.status || 'unknown'}`,
133
+ `lastStep: ${handoff.lastStep || 'none'}`,
134
+ `next: ${handoff.next || 'none'}`,
135
+ ];
136
+ for (const decision of handoff.decisions || []) {
137
+ lines.push(`decision: ${decision.decision}${decision.reason ? ` | ${decision.reason}` : ''}`);
138
+ }
139
+ for (const loop of handoff.openLoops || []) {
140
+ lines.push(`open_loop: ${loop.item}${loop.owner ? ` | owner=${loop.owner}` : ''}`);
141
+ }
142
+ for (const topic of handoff.topics || []) {
143
+ const name = normalizeText(topic && (topic.name || topic.topic || topic.title));
144
+ const summary = normalizeText(topic && (topic.summary || topic.text));
145
+ if (name || summary) lines.push(`topic: ${name || 'topic'}${summary ? ` | ${summary}` : ''}`);
146
+ }
147
+ lines.push('</handoff_context>');
148
+ return lines.join('\n');
149
+ }
150
+
151
+ function buildHandoffSynthesisPrompt(payload = {}, view = {}, opts = {}) {
152
+ if (!view || view.status !== 'ok') {
153
+ throw new Error(`Codex handoff synthesis requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
154
+ }
155
+ const metadata = buildHandoffMetadata(payload);
156
+ const basePrompt = buildFinalizationPrompt(view, opts);
157
+ const handoffBlock = [
158
+ formatHandoffContextBlock(metadata),
159
+ '',
160
+ '<handoff_synthesis_rules>',
161
+ 'Treat handoff_context as producer process material, not current truth by itself.',
162
+ 'Use the sanitized transcript and current_memory to decide what should become current memory candidates.',
163
+ 'Do not copy old current_memory unchanged unless this session confirms it should carry forward.',
164
+ 'Represent resolved, superseded, revoked, or uncertain items explicitly in structuredSummary payload fields when applicable.',
165
+ 'Do not include raw transcript, tool output, debug ids, DB ids, hashes, secrets, or injected context in memory candidates.',
166
+ '</handoff_synthesis_rules>',
167
+ ].join('\n');
168
+ return basePrompt.replace('<sanitized_transcript>', `${handoffBlock}\n\n<sanitized_transcript>`);
169
+ }
170
+
171
+ async function prepareHandoffSynthesis(aquifer, payload = {}, opts = {}) {
172
+ const view = opts.view || payload.view;
173
+ if (!view || view.status !== 'ok') {
174
+ throw new Error(`Codex handoff synthesis requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
175
+ }
176
+ const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
177
+ return {
178
+ status: 'needs_agent_summary',
179
+ outputSchemaVersion: 'handoff_current_memory_synthesis_v1',
180
+ view,
181
+ currentMemory,
182
+ prompt: buildHandoffSynthesisPrompt(payload, view, { ...opts, currentMemory }),
183
+ };
184
+ }
185
+
98
186
  async function finalizeHandoff(aquifer, payload = {}, opts = {}) {
99
187
  const view = opts.view || payload.view;
100
188
  if (!view || view.status !== 'ok') {
101
189
  throw new Error(`Codex handoff finalization requires an ok normalized transcript view; got ${view && view.status ? view.status : 'missing'}`);
102
190
  }
103
- const summary = opts.summary || payload.summary || {
104
- summaryText: opts.summaryText || payload.summaryText,
105
- structuredSummary: opts.structuredSummary || payload.structuredSummary,
106
- };
191
+ const { summary, candidates, usedSynthesis } = resolveHandoffSummary(payload, opts);
107
192
  const metadata = {
108
193
  ...buildHandoffMetadata(payload),
109
194
  ...(opts.metadata || {}),
110
195
  };
196
+ if (usedSynthesis) {
197
+ metadata.handoffSynthesis = {
198
+ kind: 'handoff_current_memory_synthesis_v1',
199
+ source: 'operator_reviewed_summary',
200
+ promotionGate: 'core_finalization',
201
+ };
202
+ }
203
+ const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
204
+ if (currentMemory) metadata.currentMemory = compactCurrentMemorySnapshot(currentMemory, opts);
111
205
  const result = await finalizeTranscriptView(aquifer, view, summary, {
112
206
  ...opts,
113
207
  mode: 'handoff',
114
208
  metadataSource: 'codex_handoff',
115
209
  metadata,
116
- authority: opts.authority || 'manual',
210
+ authority: opts.authority || (usedSynthesis ? 'verified_summary' : 'manual'),
211
+ candidates,
212
+ candidatePayload: usedSynthesis
213
+ ? {
214
+ kind: 'handoff_synthesis',
215
+ synthesisKind: 'handoff_current_memory_synthesis_v1',
216
+ currentMemoryRole: 'handoff_synthesis_candidate',
217
+ promotionGate: 'core_finalization',
218
+ }
219
+ : opts.candidatePayload || null,
117
220
  });
118
221
  const coreResult = result.finalization || {};
119
222
  const finalSummary = coreResult.summary || {
@@ -148,5 +251,8 @@ async function finalizeHandoff(aquifer, payload = {}, opts = {}) {
148
251
 
149
252
  module.exports = {
150
253
  buildHandoffMetadata,
254
+ buildHandoffSynthesisPrompt,
255
+ prepareHandoffSynthesis,
256
+ resolveHandoffSummary,
151
257
  finalizeHandoff,
152
258
  };
@@ -323,11 +323,10 @@ function matchesRecoveryProvenance(metadata = {}, opts = {}, defaults = {}) {
323
323
  repoPath: opts.repoPath || null,
324
324
  };
325
325
  for (const [key, expectedValue] of Object.entries(expected)) {
326
- if (!expectedValue || !metadata[key]) continue;
326
+ if (!expectedValue) continue;
327
+ if (!metadata[key]) return false;
327
328
  if (String(metadata[key]) !== String(expectedValue)) return false;
328
329
  }
329
- if (metadata.source && metadata.source !== expected.source) return false;
330
- if (metadata.agentId && metadata.agentId !== expected.agentId) return false;
331
330
  return true;
332
331
  }
333
332
 
@@ -770,11 +769,13 @@ async function afterburnCandidate(aquifer, candidate, opts = {}) {
770
769
 
771
770
  let resolvedSummary = summaryInput;
772
771
  if (!hasFinalizationSummary(resolvedSummary) && typeof summaryProvider === 'function') {
772
+ const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
773
773
  resolvedSummary = await summaryProvider(view.messages, {
774
774
  aquifer,
775
775
  candidate: recoveryCandidate,
776
776
  existing,
777
777
  view,
778
+ currentMemory,
778
779
  agentId,
779
780
  source,
780
781
  sessionKey,
@@ -873,9 +874,18 @@ async function findRecoveryCandidates(aquifer, opts = {}) {
873
874
  const {
874
875
  agentId = 'main',
875
876
  source = 'codex',
877
+ sessionKey = null,
876
878
  maxRecoveryCandidates = 3,
877
879
  includeJsonlPreviews = false,
878
880
  } = opts;
881
+ const provenance = {
882
+ source,
883
+ agentId,
884
+ sessionKey,
885
+ workspace: opts.workspace || opts.workspacePath || null,
886
+ project: opts.project || opts.projectKey || null,
887
+ repoPath: opts.repoPath || null,
888
+ };
879
889
  if (!Number.isFinite(maxRecoveryCandidates) || maxRecoveryCandidates <= 0) return [];
880
890
  ensureDirs(paths.importedDir, paths.afterburnedDir, paths.claimDir, paths.decisionDir);
881
891
 
@@ -961,7 +971,7 @@ async function findRecoveryCandidates(aquifer, opts = {}) {
961
971
  transcriptHash: null,
962
972
  source,
963
973
  agentId,
964
- sessionKey: null,
974
+ sessionKey,
965
975
  userCount: null,
966
976
  messageCount: null,
967
977
  finalizationStatus: null,
@@ -972,8 +982,7 @@ async function findRecoveryCandidates(aquifer, opts = {}) {
972
982
  fileSessionId: safeFileSessionId,
973
983
  size: entry.stat.size,
974
984
  mtimeMs: entry.stat.mtimeMs,
975
- source,
976
- agentId,
985
+ ...provenance,
977
986
  },
978
987
  };
979
988
  const localDecision = readRecoveryDecision(paths, candidatePreview);
@@ -1203,12 +1212,105 @@ function materializeRecoveryTranscriptView(candidate = {}, opts = {}) {
1203
1212
  };
1204
1213
  }
1205
1214
 
1215
+ function compactCurrentMemoryRow(row = {}) {
1216
+ const payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
1217
+ const confidence = payload.confidence || payload.currentMemoryConfidence || null;
1218
+ return {
1219
+ memoryType: row.memoryType || row.memory_type || 'memory',
1220
+ canonicalKey: row.canonicalKey || row.canonical_key || null,
1221
+ scopeKey: row.scopeKey || row.scope_key || null,
1222
+ summary: String(row.summary || row.title || '').replace(/\s+/g, ' ').trim(),
1223
+ authority: row.authority || null,
1224
+ confidence,
1225
+ };
1226
+ }
1227
+
1228
+ function formatCurrentMemoryPromptBlock(currentMemory = null, opts = {}) {
1229
+ const maxItems = Math.max(0, Math.min(20, opts.maxCurrentMemoryItems || opts.currentMemoryLimit || 12));
1230
+ const meta = currentMemory && currentMemory.meta ? currentMemory.meta : {};
1231
+ const rows = Array.isArray(currentMemory?.memories)
1232
+ ? currentMemory.memories
1233
+ : (Array.isArray(currentMemory?.items) ? currentMemory.items : []);
1234
+ const compactRows = rows.map(compactCurrentMemoryRow).filter(row => row.summary).slice(0, maxItems);
1235
+ const attrs = [
1236
+ `source="${meta.source || 'memory_records'}"`,
1237
+ `serving_contract="${meta.servingContract || meta.serving_contract || 'current_memory_v1'}"`,
1238
+ `count="${compactRows.length}"`,
1239
+ `truncated="${Boolean(meta.truncated || rows.length > compactRows.length)}"`,
1240
+ `degraded="${Boolean(meta.degraded || currentMemory?.error)}"`,
1241
+ ];
1242
+ const lines = compactRows.map(row => {
1243
+ const scope = row.scopeKey ? ` scope=${row.scopeKey}` : '';
1244
+ const authority = row.authority ? ` authority=${row.authority}` : '';
1245
+ const confidence = row.confidence ? ` confidence=${row.confidence}` : '';
1246
+ return `- ${row.memoryType}${scope}${authority}${confidence}: ${row.summary}`;
1247
+ });
1248
+ if (currentMemory && currentMemory.error && lines.length === 0) {
1249
+ lines.push(`- degraded: ${String(currentMemory.error).replace(/\s+/g, ' ').trim()}`);
1250
+ }
1251
+ if (lines.length === 0) lines.push('- none');
1252
+ return [
1253
+ `<current_memory ${attrs.join(' ')}>`,
1254
+ ...lines,
1255
+ '</current_memory>',
1256
+ ].join('\n');
1257
+ }
1258
+
1259
+ function compactCurrentMemorySnapshot(currentMemory = null, opts = {}) {
1260
+ const maxItems = Math.max(0, Math.min(20, opts.maxCurrentMemoryItems || opts.currentMemoryLimit || 12));
1261
+ const meta = currentMemory && currentMemory.meta ? currentMemory.meta : {};
1262
+ const rows = Array.isArray(currentMemory?.memories)
1263
+ ? currentMemory.memories
1264
+ : (Array.isArray(currentMemory?.items) ? currentMemory.items : []);
1265
+ return {
1266
+ memories: rows.map(compactCurrentMemoryRow).filter(row => row.summary).slice(0, maxItems),
1267
+ meta: {
1268
+ source: meta.source || 'memory_records',
1269
+ servingContract: meta.servingContract || meta.serving_contract || 'current_memory_v1',
1270
+ count: Math.min(rows.length, maxItems),
1271
+ truncated: Boolean(meta.truncated || rows.length > maxItems),
1272
+ degraded: Boolean(meta.degraded || currentMemory?.error),
1273
+ },
1274
+ };
1275
+ }
1276
+
1277
+ async function resolveCurrentMemoryForFinalization(aquifer, opts = {}) {
1278
+ if (opts.includeCurrentMemory === false) return null;
1279
+ if (opts.currentMemory !== undefined) return opts.currentMemory;
1280
+ const currentFn = aquifer?.memory?.current || aquifer?.memory?.listCurrentMemory;
1281
+ if (typeof currentFn !== 'function') return null;
1282
+ const limit = Math.max(1, Math.min(20, opts.currentMemoryLimit || opts.maxCurrentMemoryItems || 12));
1283
+ try {
1284
+ return await currentFn.call(aquifer.memory, {
1285
+ tenantId: opts.tenantId,
1286
+ activeScopeKey: opts.activeScopeKey || opts.scopeKey,
1287
+ activeScopePath: opts.activeScopePath,
1288
+ scopeId: opts.scopeId,
1289
+ asOf: opts.asOf,
1290
+ limit,
1291
+ });
1292
+ } catch (err) {
1293
+ return {
1294
+ memories: [],
1295
+ meta: {
1296
+ source: 'memory_records',
1297
+ servingContract: 'current_memory_v1',
1298
+ count: 0,
1299
+ truncated: false,
1300
+ degraded: true,
1301
+ },
1302
+ error: err.message,
1303
+ };
1304
+ }
1305
+ }
1306
+
1206
1307
  function buildFinalizationPrompt(view = {}, opts = {}) {
1207
1308
  if (!view || view.status !== 'ok') {
1208
1309
  throw new Error('buildFinalizationPrompt requires an ok transcript view');
1209
1310
  }
1210
1311
  const maxFacts = opts.maxFacts || 8;
1211
- return [
1312
+ const includeCurrentMemory = opts.includeCurrentMemory !== false;
1313
+ const lines = [
1212
1314
  'You are finalizing an Aquifer memory session for Codex.',
1213
1315
  'Use only the sanitized transcript below. Do not infer from hidden tool output or injected context.',
1214
1316
  'Return compact JSON with this shape:',
@@ -1222,7 +1324,16 @@ function buildFinalizationPrompt(view = {}, opts = {}) {
1222
1324
  '<sanitized_transcript>',
1223
1325
  view.text || '',
1224
1326
  '</sanitized_transcript>',
1225
- ].join('\n');
1327
+ ];
1328
+ if (includeCurrentMemory) {
1329
+ lines.splice(
1330
+ 2,
1331
+ 0,
1332
+ 'Use current_memory as the already-committed current state. Reconcile the transcript against it: keep valid state, supersede stale state, and mark uncertain items explicitly.',
1333
+ );
1334
+ lines.splice(10, 0, formatCurrentMemoryPromptBlock(opts.currentMemory, opts), '');
1335
+ }
1336
+ return lines.join('\n');
1226
1337
  }
1227
1338
 
1228
1339
  function normalizeFinalizationSummary(summary = {}) {
@@ -1304,6 +1415,10 @@ async function finalizeTranscriptView(aquifer, view = {}, summary = {}, opts = {
1304
1415
  trigger: mode,
1305
1416
  ...(opts.metadata || {}),
1306
1417
  };
1418
+ if (!metadata.currentMemory) {
1419
+ const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
1420
+ if (currentMemory) metadata.currentMemory = compactCurrentMemorySnapshot(currentMemory, opts);
1421
+ }
1307
1422
  const result = await finalizeSession({
1308
1423
  sessionId: view.sessionId,
1309
1424
  agentId,
@@ -1325,6 +1440,8 @@ async function finalizeTranscriptView(aquifer, view = {}, summary = {}, opts = {
1325
1440
  contextKey: opts.contextKey || null,
1326
1441
  topicKey: opts.topicKey || null,
1327
1442
  authority: opts.authority || 'verified_summary',
1443
+ candidates: Array.isArray(opts.candidates) ? opts.candidates : undefined,
1444
+ candidatePayload: opts.candidatePayload || null,
1328
1445
  metadata,
1329
1446
  });
1330
1447
  const humanReviewText = result.humanReviewText || '';
@@ -1432,11 +1549,13 @@ async function prepareSessionStartRecovery(aquifer, opts = {}) {
1432
1549
  }
1433
1550
  return { status: 'skipped_short', candidate: skippedCandidate, view, userCount };
1434
1551
  }
1552
+ const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, recoveryOpts);
1435
1553
  return {
1436
1554
  status: 'needs_agent_summary',
1437
1555
  candidate,
1438
1556
  view,
1439
- prompt: buildFinalizationPrompt(view, recoveryOpts),
1557
+ currentMemory,
1558
+ prompt: buildFinalizationPrompt(view, { ...recoveryOpts, currentMemory }),
1440
1559
  };
1441
1560
  }
1442
1561
 
@@ -1470,6 +1589,11 @@ async function finalizeCodexSession(aquifer, input = {}, opts = {}) {
1470
1589
  scopeKey: input.scopeKey || opts.scopeKey || null,
1471
1590
  contextKey: input.contextKey || opts.contextKey || null,
1472
1591
  topicKey: input.topicKey || opts.topicKey || null,
1592
+ activeScopeKey: input.activeScopeKey || opts.activeScopeKey || input.scopeKey || opts.scopeKey || null,
1593
+ activeScopePath: input.activeScopePath || opts.activeScopePath || null,
1594
+ currentMemory: input.currentMemory !== undefined ? input.currentMemory : opts.currentMemory,
1595
+ currentMemoryLimit: input.currentMemoryLimit || opts.currentMemoryLimit || null,
1596
+ includeCurrentMemory: input.includeCurrentMemory !== undefined ? input.includeCurrentMemory : opts.includeCurrentMemory,
1473
1597
  });
1474
1598
  }
1475
1599
 
@@ -1546,4 +1670,7 @@ module.exports = {
1546
1670
  markerPath,
1547
1671
  hashNormalizedTranscript,
1548
1672
  readMarkerMetadataFromContent,
1673
+ formatCurrentMemoryPromptBlock,
1674
+ compactCurrentMemorySnapshot,
1675
+ resolveCurrentMemoryForFinalization,
1549
1676
  };
package/core/aquifer.js CHANGED
@@ -2150,6 +2150,14 @@ function createAquifer(config = {}) {
2150
2150
  await ensureMigrated();
2151
2151
  return memoryBootstrap.bootstrap(opts);
2152
2152
  },
2153
+ current: async (opts = {}) => {
2154
+ await ensureMigrated();
2155
+ return memoryRecords.currentProjection(withDefaultMemoryScope(opts));
2156
+ },
2157
+ listCurrentMemory: async (opts = {}) => {
2158
+ await ensureMigrated();
2159
+ return memoryRecords.currentProjection(withDefaultMemoryScope(opts));
2160
+ },
2153
2161
  recall: async (query, opts = {}) => {
2154
2162
  await ensureMigrated();
2155
2163
  return memoryRecall.recall(query, opts);
@@ -2,10 +2,10 @@
2
2
 
3
3
  const TYPE_PRIORITY = {
4
4
  constraint: 0,
5
- preference: 1,
6
- state: 2,
7
- open_loop: 3,
8
- decision: 4,
5
+ state: 1,
6
+ open_loop: 2,
7
+ decision: 3,
8
+ preference: 4,
9
9
  fact: 5,
10
10
  conclusion: 6,
11
11
  entity_note: 7,
@@ -96,7 +96,13 @@ function resolveApplicableRecords(records = [], opts = {}) {
96
96
  return [...winners.values(), ...additive];
97
97
  }
98
98
 
99
- function sortForBootstrap(a, b) {
99
+ function sortForBootstrap(a, b, opts = {}) {
100
+ const activeScopePath = opts.activeScopePath || (opts.activeScopeKey ? [opts.activeScopeKey] : ['global']);
101
+ const position = new Map(activeScopePath.map((key, idx) => [key, idx]));
102
+ const aScope = position.get(scopeKey(a)) ?? -1;
103
+ const bScope = position.get(scopeKey(b)) ?? -1;
104
+ if (bScope !== aScope) return bScope - aScope;
105
+
100
106
  const aType = TYPE_PRIORITY[a.memoryType || a.memory_type] ?? 99;
101
107
  const bType = TYPE_PRIORITY[b.memoryType || b.memory_type] ?? 99;
102
108
  if (aType !== bType) return aType - bType;
@@ -129,10 +135,11 @@ function buildText(records, meta) {
129
135
 
130
136
  function buildMemoryBootstrap(records = [], opts = {}) {
131
137
  const maxChars = Math.max(120, opts.maxChars || 4000);
138
+ const limit = Number.isFinite(opts.limit) ? Math.max(1, Math.min(100, Math.floor(opts.limit))) : null;
132
139
  const active = resolveApplicableRecords(
133
140
  records.filter(record => isActiveBootstrap(record, opts)),
134
141
  opts,
135
- ).sort(sortForBootstrap);
142
+ ).sort((a, b) => sortForBootstrap(a, b, opts));
136
143
 
137
144
  const meta = {
138
145
  overflow: false,
@@ -141,7 +148,11 @@ function buildMemoryBootstrap(records = [], opts = {}) {
141
148
  count: active.length,
142
149
  };
143
150
 
144
- let selected = active.slice();
151
+ let selected = limit ? active.slice(0, limit) : active.slice();
152
+ if (limit && active.length > limit) {
153
+ meta.overflow = true;
154
+ meta.degraded = true;
155
+ }
145
156
  let text = buildText(selected, meta);
146
157
  while (text.length > maxChars && selected.length > 1) {
147
158
  selected = selected.slice(0, -1);
@@ -167,13 +178,14 @@ function buildMemoryBootstrap(records = [], opts = {}) {
167
178
 
168
179
  function createMemoryBootstrap({ records }) {
169
180
  async function bootstrap(opts = {}) {
181
+ const requestedLimit = Number.isFinite(opts.limit) ? Math.max(1, Math.floor(opts.limit)) : 50;
170
182
  const rows = await records.listActive({
171
183
  tenantId: opts.tenantId,
172
184
  scopeId: opts.scopeId,
173
185
  scopeKeys: opts.activeScopePath || (opts.activeScopeKey ? [opts.activeScopeKey] : undefined),
174
186
  visibleInBootstrap: true,
175
187
  asOf: opts.asOf,
176
- limit: opts.limit || 50,
188
+ limit: Math.max(50, Math.min(200, requestedLimit * 4)),
177
189
  });
178
190
  return buildMemoryBootstrap(rows, opts);
179
191
  }