@lumenflow/memory 2.8.0 → 2.10.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/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export * from './mem-index-core.js';
19
19
  export * from './mem-promote-core.js';
20
20
  export * from './mem-profile-core.js';
21
21
  export * from './mem-delete-core.js';
22
+ export * from './mem-recover-core.js';
22
23
  export * from './memory-schema.js';
23
24
  export * from './memory-store.js';
24
25
  // WU-1238: Decay scoring and access tracking
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Memory Recovery Core (WU-1390)
3
+ *
4
+ * Generates post-compaction recovery context for agents that have lost
5
+ * their LumenFlow instructions due to context compaction.
6
+ *
7
+ * Features:
8
+ * - Loads last checkpoint from memory layer
9
+ * - Extracts compact constraints from .lumenflow/constraints.md
10
+ * - Provides essential CLI commands reference
11
+ * - Size-limited output (default 2KB) to prevent truncation
12
+ * - Vendor-agnostic (works for any client)
13
+ *
14
+ * @see {@link packages/@lumenflow/cli/src/mem-recover.ts} - CLI wrapper
15
+ * @see {@link packages/@lumenflow/memory/__tests__/mem-recover-core.test.ts} - Tests
16
+ */
17
+ import fs from 'node:fs/promises';
18
+ import path from 'node:path';
19
+ import { loadMemory } from './memory-store.js';
20
+ import { MEMORY_PATTERNS } from './memory-schema.js';
21
+ import { LUMENFLOW_MEMORY_PATHS } from './paths.js';
22
+ /**
23
+ * Default maximum recovery context size in bytes (2KB)
24
+ * Smaller than spawn context to ensure it doesn't get truncated
25
+ */
26
+ const DEFAULT_MAX_SIZE = 2048;
27
+ /**
28
+ * Node type constant for checkpoint filtering
29
+ * @see {@link MEMORY_NODE_TYPES} in memory-schema.ts
30
+ */
31
+ const NODE_TYPE_CHECKPOINT = 'checkpoint';
32
+ /**
33
+ * Constraints file name within .lumenflow directory
34
+ */
35
+ const CONSTRAINTS_FILENAME = 'constraints.md';
36
+ /**
37
+ * Default value for unknown timestamps
38
+ */
39
+ const TIMESTAMP_UNKNOWN = 'unknown';
40
+ /**
41
+ * Error messages for validation
42
+ */
43
+ const ERROR_MESSAGES = {
44
+ WU_ID_REQUIRED: 'wuId is required',
45
+ WU_ID_EMPTY: 'wuId cannot be empty',
46
+ WU_ID_INVALID: 'Invalid WU ID format. Expected pattern: WU-XXX (e.g., WU-1234)',
47
+ };
48
+ /**
49
+ * Section headers for recovery context formatting
50
+ */
51
+ const SECTION_HEADERS = {
52
+ RECOVERY_TITLE: 'POST-COMPACTION RECOVERY',
53
+ LAST_CHECKPOINT: 'Last Checkpoint',
54
+ CRITICAL_RULES: 'Critical Rules (DO NOT FORGET)',
55
+ CLI_COMMANDS: 'CLI Commands',
56
+ NEXT_ACTION: 'Next Action',
57
+ };
58
+ /**
59
+ * Essential CLI commands for recovery (hardcoded, ~400 bytes)
60
+ */
61
+ const ESSENTIAL_CLI_COMMANDS = `| Command | Purpose |
62
+ |---------|---------|
63
+ | pnpm wu:status --id WU-XXX | Check WU status and location |
64
+ | pnpm wu:spawn --id WU-XXX | Generate fresh agent spawn prompt |
65
+ | pnpm gates | Run quality gates before completion |
66
+ | pnpm mem:checkpoint | Save progress checkpoint |`;
67
+ /**
68
+ * Compact constraints (hardcoded fallback, ~800 bytes)
69
+ * Used when constraints.md cannot be loaded
70
+ */
71
+ const FALLBACK_CONSTRAINTS = `1. **Worktree Discipline**: Work only in worktrees, treat main as read-only
72
+ 2. **WUs Are Specs**: Respect code_paths boundaries, no feature creep
73
+ 3. **Docs-Only vs Code**: Documentation WUs use --docs-only gates
74
+ 4. **LLM-First Inference**: Use LLMs for semantic tasks, no brittle regex
75
+ 5. **Gates Required**: Run pnpm gates before wu:done
76
+ 6. **Safety & Governance**: No secrets in code, stop-and-ask for sensitive ops
77
+ 7. **Test Ratchet**: NEW test failures block, pre-existing show warning only`;
78
+ /**
79
+ * Validates WU ID format
80
+ */
81
+ function validateWuId(wuId) {
82
+ if (wuId === undefined || wuId === null) {
83
+ throw new Error(ERROR_MESSAGES.WU_ID_REQUIRED);
84
+ }
85
+ if (wuId === '') {
86
+ throw new Error(ERROR_MESSAGES.WU_ID_EMPTY);
87
+ }
88
+ if (!MEMORY_PATTERNS.WU_ID.test(wuId)) {
89
+ throw new Error(ERROR_MESSAGES.WU_ID_INVALID);
90
+ }
91
+ }
92
+ /**
93
+ * Gets the most recent checkpoint for a WU from memory
94
+ */
95
+ async function getLastCheckpoint(baseDir, wuId) {
96
+ const memoryDir = path.join(baseDir, LUMENFLOW_MEMORY_PATHS.MEMORY_DIR);
97
+ try {
98
+ const memory = await loadMemory(memoryDir);
99
+ const wuNodes = memory.byWu.get(wuId) ?? [];
100
+ // Filter to checkpoint nodes and sort by timestamp (most recent first)
101
+ const checkpoints = wuNodes
102
+ .filter((node) => node.type === NODE_TYPE_CHECKPOINT)
103
+ .sort((a, b) => {
104
+ const aTime = new Date(a.created_at).getTime();
105
+ const bTime = new Date(b.created_at).getTime();
106
+ return bTime - aTime; // Most recent first
107
+ });
108
+ if (checkpoints.length === 0) {
109
+ return null;
110
+ }
111
+ const latest = checkpoints[0];
112
+ return {
113
+ content: latest.content,
114
+ timestamp: latest.created_at,
115
+ progress: latest.metadata?.progress,
116
+ nextSteps: latest.metadata?.nextSteps,
117
+ };
118
+ }
119
+ catch {
120
+ // Memory layer not initialized or error loading
121
+ return null;
122
+ }
123
+ }
124
+ /**
125
+ * Loads compact constraints from .lumenflow/constraints.md
126
+ * Extracts just the rule summary from each of the 7 sections
127
+ */
128
+ async function loadCompactConstraints(baseDir) {
129
+ const constraintsPath = path.join(baseDir, LUMENFLOW_MEMORY_PATHS.BASE, CONSTRAINTS_FILENAME);
130
+ try {
131
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
132
+ const content = await fs.readFile(constraintsPath, 'utf-8');
133
+ // Extract section headers and their Rule lines
134
+ const rules = [];
135
+ const sectionPattern = /### (\d+)\. ([^\n]+)\n\n\*\*Rule:\*\* ([^\n]+)/g;
136
+ let match;
137
+ while ((match = sectionPattern.exec(content)) !== null) {
138
+ const [, num, title, rule] = match;
139
+ rules.push(`${num}. **${title}**: ${rule}`);
140
+ }
141
+ if (rules.length > 0) {
142
+ return rules.join('\n');
143
+ }
144
+ // Fallback if parsing failed
145
+ return FALLBACK_CONSTRAINTS;
146
+ }
147
+ catch {
148
+ // File not found or read error - use fallback
149
+ return FALLBACK_CONSTRAINTS;
150
+ }
151
+ }
152
+ /**
153
+ * Formats the recovery prompt with size budget
154
+ */
155
+ function formatRecoveryPrompt(wuId, checkpoint, constraints, cliRef, maxSize) {
156
+ const header = `# ${SECTION_HEADERS.RECOVERY_TITLE}
157
+
158
+ You are resuming work after context compaction. Your previous context was lost.
159
+ **WU:** ${wuId}
160
+
161
+ `;
162
+ const checkpointSection = checkpoint
163
+ ? `## ${SECTION_HEADERS.LAST_CHECKPOINT}
164
+ - **Progress:** ${checkpoint.content}
165
+ - **Timestamp:** ${checkpoint.timestamp}
166
+ ${checkpoint.nextSteps ? `- **Next Steps:** ${checkpoint.nextSteps}` : ''}
167
+
168
+ `
169
+ : `## ${SECTION_HEADERS.LAST_CHECKPOINT}
170
+ No checkpoint found for this WU.
171
+
172
+ `;
173
+ // Only include sections if they have content
174
+ const constraintsSection = constraints
175
+ ? `## ${SECTION_HEADERS.CRITICAL_RULES}
176
+ ${constraints}
177
+
178
+ `
179
+ : '';
180
+ const cliSection = cliRef
181
+ ? `## ${SECTION_HEADERS.CLI_COMMANDS}
182
+ ${cliRef}
183
+
184
+ `
185
+ : '';
186
+ const footer = `## ${SECTION_HEADERS.NEXT_ACTION}
187
+ Run \`pnpm wu:spawn --id ${wuId}\` to spawn a fresh agent with full context.
188
+ `;
189
+ // Build sections in priority order
190
+ // Priority: header > checkpoint > constraints > CLI > footer
191
+ // If over budget, truncate checkpoint content first
192
+ const fixedContent = header + constraintsSection + cliSection + footer;
193
+ const fixedSize = Buffer.byteLength(fixedContent, 'utf-8');
194
+ if (fixedSize > maxSize) {
195
+ // Even fixed content exceeds budget - return minimal recovery
196
+ const minimal = `# ${SECTION_HEADERS.RECOVERY_TITLE}
197
+ WU: ${wuId}
198
+ Run: pnpm wu:spawn --id ${wuId}
199
+ `;
200
+ return { context: minimal, truncated: true };
201
+ }
202
+ const remainingBudget = maxSize - fixedSize;
203
+ const checkpointSize = Buffer.byteLength(checkpointSection, 'utf-8');
204
+ if (checkpointSize <= remainingBudget) {
205
+ // Full checkpoint fits
206
+ const fullContext = header + checkpointSection + constraintsSection + cliSection + footer;
207
+ return { context: fullContext, truncated: false };
208
+ }
209
+ // Truncate checkpoint to fit
210
+ const truncatedCheckpoint = `## ${SECTION_HEADERS.LAST_CHECKPOINT}
211
+ - **Progress:** (truncated - run mem:ready --wu ${wuId} for details)
212
+ - **Timestamp:** ${checkpoint?.timestamp ?? TIMESTAMP_UNKNOWN}
213
+
214
+ `;
215
+ const context = header + truncatedCheckpoint + constraintsSection + cliSection + footer;
216
+ return { context, truncated: true };
217
+ }
218
+ /**
219
+ * Generates post-compaction recovery context for an agent.
220
+ *
221
+ * The recovery context includes:
222
+ * - Last checkpoint for the WU (from memory layer)
223
+ * - Compact constraints (7 rules from constraints.md)
224
+ * - Essential CLI commands reference
225
+ * - Guidance to spawn fresh agent
226
+ *
227
+ * Size is limited to prevent the recovery context itself from being
228
+ * truncated or compacted.
229
+ *
230
+ * @param options - Recovery options
231
+ * @returns Recovery context result
232
+ *
233
+ * @example
234
+ * const result = await generateRecoveryContext({
235
+ * wuId: 'WU-1390',
236
+ * maxSize: 2048,
237
+ * });
238
+ * console.log(result.context);
239
+ */
240
+ export async function generateRecoveryContext(options) {
241
+ const { wuId, baseDir = '.', maxSize = DEFAULT_MAX_SIZE, includeConstraints = true, includeCLIRef = true, } = options;
242
+ // Validate WU ID
243
+ validateWuId(wuId);
244
+ // Load checkpoint from memory
245
+ const checkpoint = await getLastCheckpoint(baseDir, wuId);
246
+ // Load constraints (or use empty if disabled)
247
+ const constraints = includeConstraints ? await loadCompactConstraints(baseDir) : '';
248
+ // Get CLI reference (or empty if disabled)
249
+ const cliRef = includeCLIRef ? ESSENTIAL_CLI_COMMANDS : '';
250
+ // Format with size budget
251
+ const { context, truncated } = formatRecoveryPrompt(wuId, checkpoint, constraints, cliRef, maxSize);
252
+ return {
253
+ success: true,
254
+ context,
255
+ size: Buffer.byteLength(context, 'utf-8'),
256
+ truncated,
257
+ };
258
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/memory",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
4
4
  "description": "Memory layer for LumenFlow workflow framework - session tracking, context recovery, and agent coordination",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -49,7 +49,7 @@
49
49
  "ms": "^2.1.3",
50
50
  "yaml": "^2.8.2",
51
51
  "zod": "^4.3.5",
52
- "@lumenflow/core": "2.8.0"
52
+ "@lumenflow/core": "2.10.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@vitest/coverage-v8": "^4.0.17",