@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 +6 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/consumers/cli.js +69 -2
- package/consumers/codex-handoff.js +112 -6
- package/consumers/codex.js +136 -9
- package/core/aquifer.js +8 -0
- package/core/memory-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-records.js +220 -4
- package/core/session-finalization.js +16 -1
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +44 -3
- package/package.json +2 -2
- package/scripts/codex-recovery.js +141 -1
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}
|
|
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 = {
|
|
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 {
|
|
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
|
|
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
|
};
|
package/consumers/codex.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
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);
|
package/core/memory-bootstrap.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const TYPE_PRIORITY = {
|
|
4
4
|
constraint: 0,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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:
|
|
188
|
+
limit: Math.max(50, Math.min(200, requestedLimit * 4)),
|
|
177
189
|
});
|
|
178
190
|
return buildMemoryBootstrap(rows, opts);
|
|
179
191
|
}
|