memorix 1.6.1 → 1.6.2

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 (113) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +1574 -1063
  4. package/dist/index.js +588 -107
  5. package/dist/maintenance-runner.js +565 -84
  6. package/dist/media-audio-runner.js +565 -84
  7. package/dist/media-video-runner.js +565 -84
  8. package/dist/memcode-runtime/CHANGELOG.md +22 -0
  9. package/dist/sdk.js +621 -111
  10. package/dist/vector-backfill-runner.js +565 -84
  11. package/docs/DESIGN_DECISIONS.md +357 -357
  12. package/docs/PERFORMANCE.md +15 -0
  13. package/package.json +2 -1
  14. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  15. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  16. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  17. package/plugins/copilot/memorix/plugin.json +1 -1
  18. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  19. package/plugins/omp/memorix/package.json +1 -1
  20. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/pi/memorix/package.json +1 -1
  22. package/server.json +2 -2
  23. package/src/audit/index.ts +156 -156
  24. package/src/cli/commands/background.ts +34 -2
  25. package/src/cli/commands/formation.ts +48 -48
  26. package/src/cli/commands/git-hook-install.ts +111 -111
  27. package/src/cli/commands/handoff.ts +54 -54
  28. package/src/cli/commands/hooks-status.ts +63 -63
  29. package/src/cli/commands/ingest.ts +44 -44
  30. package/src/cli/commands/integrate-shared.ts +15 -15
  31. package/src/cli/commands/lock.ts +82 -82
  32. package/src/cli/commands/message.ts +104 -104
  33. package/src/cli/commands/operator-shared.ts +6 -3
  34. package/src/cli/commands/poll.ts +58 -58
  35. package/src/cli/commands/reasoning.ts +118 -118
  36. package/src/cli/commands/serve-http.ts +4 -0
  37. package/src/cli/commands/skills.ts +114 -114
  38. package/src/cli/commands/task.ts +167 -167
  39. package/src/cli/commands/transfer.ts +47 -47
  40. package/src/cli/index.ts +406 -406
  41. package/src/cli/tui/ChatView.tsx +234 -234
  42. package/src/cli/tui/CommandBar.tsx +312 -312
  43. package/src/cli/tui/ContextRail.tsx +118 -118
  44. package/src/cli/tui/HeaderBar.tsx +72 -72
  45. package/src/cli/tui/LogoBanner.tsx +51 -51
  46. package/src/cli/tui/index.ts +41 -41
  47. package/src/cli/version.ts +7 -7
  48. package/src/cli/workbench.ts +1 -1
  49. package/src/dashboard/project-classification.ts +64 -64
  50. package/src/embedding/api-provider.ts +503 -27
  51. package/src/embedding/provider.ts +20 -3
  52. package/src/git/hooks-path.ts +85 -85
  53. package/src/hooks/lifecycle.ts +1 -0
  54. package/src/hooks/pattern-detector.ts +173 -173
  55. package/src/hooks/significance-filter.ts +250 -250
  56. package/src/llm/memory-manager.ts +328 -328
  57. package/src/llm/quality.ts +248 -248
  58. package/src/memory/attribution-guard.ts +249 -249
  59. package/src/memory/disclosure-policy.ts +135 -135
  60. package/src/memory/entity-extractor.ts +197 -197
  61. package/src/memory/formation/evaluate.ts +217 -217
  62. package/src/memory/freshness.ts +122 -122
  63. package/src/memory/graph.ts +197 -197
  64. package/src/memory/observations.ts +68 -18
  65. package/src/memory/refs.ts +94 -94
  66. package/src/memory/secret-filter.ts +79 -79
  67. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  68. package/src/orchestrate/adapters/claude.ts +111 -111
  69. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  70. package/src/orchestrate/adapters/codex.ts +41 -41
  71. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  72. package/src/orchestrate/adapters/gemini.ts +42 -42
  73. package/src/orchestrate/adapters/index.ts +73 -73
  74. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  75. package/src/orchestrate/adapters/opencode.ts +47 -47
  76. package/src/orchestrate/adapters/types.ts +77 -77
  77. package/src/orchestrate/capability-router.ts +284 -284
  78. package/src/orchestrate/cost-tracker.ts +219 -219
  79. package/src/orchestrate/error-recovery.ts +191 -191
  80. package/src/orchestrate/evidence.ts +140 -140
  81. package/src/orchestrate/ledger.ts +110 -110
  82. package/src/orchestrate/memorix-bridge.ts +343 -343
  83. package/src/orchestrate/output-budget.ts +80 -80
  84. package/src/orchestrate/permission.ts +152 -152
  85. package/src/orchestrate/pipeline-trace.ts +131 -131
  86. package/src/orchestrate/prompt-builder.ts +155 -155
  87. package/src/orchestrate/ring-buffer.ts +37 -37
  88. package/src/orchestrate/task-graph.ts +389 -389
  89. package/src/project/aliases.ts +374 -374
  90. package/src/rules/adapters/trae.ts +56 -56
  91. package/src/rules/syncer.ts +235 -235
  92. package/src/sdk.ts +12 -8
  93. package/src/search/query-expansion.ts +52 -52
  94. package/src/server/formation-timeout.ts +27 -27
  95. package/src/skills/mini-skills.ts +386 -386
  96. package/src/store/chat-store.ts +119 -119
  97. package/src/store/graph-store.ts +249 -249
  98. package/src/store/mini-skill-store.ts +349 -349
  99. package/src/store/orama-store.ts +21 -3
  100. package/src/store/persistence-json.ts +212 -212
  101. package/src/store/persistence.ts +291 -291
  102. package/src/store/project-affinity.ts +195 -195
  103. package/src/team/event-bus.ts +76 -76
  104. package/src/team/handoff.ts +161 -161
  105. package/src/team/poll.ts +132 -132
  106. package/src/workspace/mcp-adapters/codex.ts +191 -191
  107. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  108. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  109. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  110. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  111. package/src/workspace/mcp-adapters/trae.ts +134 -134
  112. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  113. package/src/workspace/sanitizer.ts +60 -60
@@ -331,6 +331,8 @@ export interface HydrateIndexOptions {
331
331
  allowNetworkProbe?: boolean;
332
332
  /** Attach cached vectors after lexical hydration instead of awaiting cache I/O. */
333
333
  deferCachedVectors?: boolean;
334
+ /** Skip cached-vector attach entirely (one-shot CLI reads). */
335
+ skipCachedVectors?: boolean;
334
336
  }
335
337
 
336
338
  type HydrationCandidate = { observation: any; id: string };
@@ -378,6 +380,9 @@ async function attachCachedVectors(
378
380
  } catch {
379
381
  // Vector cache hydration is best-effort. The normal backfill lane owns misses.
380
382
  }
383
+ if (index % 50 === 0) {
384
+ await new Promise<void>((resolve) => setImmediate(resolve));
385
+ }
381
386
  }
382
387
  }
383
388
 
@@ -437,11 +442,20 @@ export async function hydrateIndex(
437
442
  await insert(database, doc);
438
443
  rememberObservationDoc(doc);
439
444
  inserted++;
445
+ // Keep the HTTP event loop alive on large corpora so /health still answers.
446
+ if (inserted % 50 === 0) {
447
+ await new Promise<void>((resolve) => setImmediate(resolve));
448
+ }
449
+ if (inserted % 200 === 0) {
450
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
451
+ }
440
452
  } catch { /* skip malformed entries */ }
441
453
  }
442
454
 
443
- deferredCachedVectorHydration = deferCachedVectors
444
- ? attachCachedVectors(database, candidates).catch(() => {})
455
+ deferredCachedVectorHydration = options.skipCachedVectors
456
+ ? null
457
+ : deferCachedVectors
458
+ ? attachCachedVectors(database, candidates).catch(() => {})
445
459
  : null;
446
460
  return inserted;
447
461
  }
@@ -450,10 +464,14 @@ export async function hydrateIndex(
450
464
  * Prepare the lexical index without probing a remote embedding API. Cached
451
465
  * vectors attach after the disk cache is ready, outside the startup path.
452
466
  */
453
- export async function hydrateIndexForStartup(observations: any[]): Promise<number> {
467
+ export async function hydrateIndexForStartup(
468
+ observations: any[],
469
+ options: Pick<HydrateIndexOptions, 'skipCachedVectors'> = {},
470
+ ): Promise<number> {
454
471
  return hydrateIndex(observations, {
455
472
  allowNetworkProbe: false,
456
473
  deferCachedVectors: true,
474
+ skipCachedVectors: options.skipCachedVectors,
457
475
  });
458
476
  }
459
477
 
@@ -1,212 +1,212 @@
1
- /**
2
- * JSON Persistence Helpers — Migration / Export / Debug Only
3
- *
4
- * These functions read/write JSON/JSONL files for one-time migration
5
- * from legacy storage formats into SQLite, or for export/import and debug.
6
- *
7
- * NOT used as runtime canonical store — SQLite is the sole canonical backend.
8
- */
9
-
10
- import { promises as fs } from 'node:fs';
11
- import path from 'node:path';
12
- import { atomicWriteFile } from './file-lock.js';
13
-
14
- /**
15
- * Get the file path for the knowledge graph JSONL file.
16
- * (MCP-compatible format, same as official Memory Server)
17
- */
18
- export function getGraphFilePath(projectDir: string): string {
19
- return path.join(projectDir, 'graph.jsonl');
20
- }
21
-
22
- /**
23
- * Save the knowledge graph in JSONL format (MCP-compatible).
24
- * Each line is a JSON object with type: "entity" or "relation".
25
- *
26
- * Format adopted from MCP Official Memory Server.
27
- */
28
- export async function saveGraphJsonl(
29
- projectDir: string,
30
- entities: Array<{ name: string; entityType: string; observations: string[] }>,
31
- relations: Array<{ from: string; to: string; relationType: string }>,
32
- ): Promise<void> {
33
- const lines = [
34
- ...entities.map((e) =>
35
- JSON.stringify({ type: 'entity', name: e.name, entityType: e.entityType, observations: e.observations }),
36
- ),
37
- ...relations.map((r) =>
38
- JSON.stringify({ type: 'relation', from: r.from, to: r.to, relationType: r.relationType }),
39
- ),
40
- ];
41
- await atomicWriteFile(getGraphFilePath(projectDir), lines.join('\n'));
42
- }
43
-
44
- /**
45
- * Load the knowledge graph from JSONL format.
46
- */
47
- export async function loadGraphJsonl(
48
- projectDir: string,
49
- ): Promise<{
50
- entities: Array<{ name: string; entityType: string; observations: string[] }>;
51
- relations: Array<{ from: string; to: string; relationType: string }>;
52
- }> {
53
- const filePath = getGraphFilePath(projectDir);
54
- try {
55
- const data = await fs.readFile(filePath, 'utf-8');
56
- const lines = data.split('\n').filter((line) => line.trim() !== '');
57
- return lines.reduce(
58
- (graph, line) => {
59
- const item = JSON.parse(line);
60
- if (item.type === 'entity') {
61
- graph.entities.push({
62
- name: item.name,
63
- entityType: item.entityType,
64
- observations: item.observations,
65
- });
66
- }
67
- if (item.type === 'relation') {
68
- graph.relations.push({
69
- from: item.from,
70
- to: item.to,
71
- relationType: item.relationType,
72
- });
73
- }
74
- return graph;
75
- },
76
- {
77
- entities: [] as Array<{ name: string; entityType: string; observations: string[] }>,
78
- relations: [] as Array<{ from: string; to: string; relationType: string }>
79
- },
80
- );
81
- } catch (error) {
82
- if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
83
- return { entities: [], relations: [] };
84
- }
85
- throw error;
86
- }
87
- }
88
-
89
- /**
90
- * Save observation data as JSON (for Orama restore / export).
91
- */
92
- export async function saveObservationsJson(
93
- projectDir: string,
94
- observations: unknown[],
95
- ): Promise<void> {
96
- const filePath = path.join(projectDir, 'observations.json');
97
- await atomicWriteFile(filePath, JSON.stringify(observations, null, 2));
98
- }
99
-
100
- /**
101
- * Load observation data from JSON.
102
- */
103
- export async function loadObservationsJson(projectDir: string): Promise<unknown[]> {
104
- const filePath = path.join(projectDir, 'observations.json');
105
- try {
106
- const data = await fs.readFile(filePath, 'utf-8');
107
- return JSON.parse(data);
108
- } catch (error) {
109
- if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
110
- return [];
111
- }
112
- throw error;
113
- }
114
- }
115
-
116
- /**
117
- * Save the next observation ID counter (legacy JSON format).
118
- */
119
- export async function saveIdCounter(projectDir: string, nextId: number): Promise<void> {
120
- const filePath = path.join(projectDir, 'counter.json');
121
- await atomicWriteFile(filePath, JSON.stringify({ nextId }));
122
- }
123
-
124
- /**
125
- * Load the next observation ID counter (legacy JSON format).
126
- * For runtime use, prefer the SQLite meta table via SqliteBackend.
127
- */
128
- export async function loadIdCounter(projectDir: string): Promise<number> {
129
- const filePath = path.join(projectDir, 'counter.json');
130
- try {
131
- const data = await fs.readFile(filePath, 'utf-8');
132
- return JSON.parse(data).nextId ?? 1;
133
- } catch {
134
- return 1;
135
- }
136
- }
137
-
138
- /**
139
- * Save mini-skills data as JSON (migration source only).
140
- */
141
- export async function saveMiniSkillsJson(
142
- projectDir: string,
143
- skills: unknown[],
144
- ): Promise<void> {
145
- const filePath = path.join(projectDir, 'mini-skills.json');
146
- await atomicWriteFile(filePath, JSON.stringify(skills, null, 2));
147
- }
148
-
149
- /**
150
- * Load mini-skills data from JSON (migration source only).
151
- */
152
- export async function loadMiniSkillsJson(projectDir: string): Promise<unknown[]> {
153
- const filePath = path.join(projectDir, 'mini-skills.json');
154
- try {
155
- const data = await fs.readFile(filePath, 'utf-8');
156
- const parsed = JSON.parse(data);
157
- return Array.isArray(parsed) ? parsed : [];
158
- } catch (error) {
159
- if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
160
- return [];
161
- }
162
- throw error;
163
- }
164
- }
165
-
166
- /**
167
- * Load the mini-skills ID counter (legacy JSON format).
168
- */
169
- export async function loadMiniSkillsCounter(projectDir: string): Promise<number> {
170
- const filePath = path.join(projectDir, 'mini-skills-counter.json');
171
- try {
172
- const data = await fs.readFile(filePath, 'utf-8');
173
- return JSON.parse(data).nextId ?? 1;
174
- } catch {
175
- return 1;
176
- }
177
- }
178
-
179
- /**
180
- * Save the mini-skills ID counter (legacy JSON format).
181
- */
182
- export async function saveMiniSkillsCounter(projectDir: string, nextId: number): Promise<void> {
183
- const filePath = path.join(projectDir, 'mini-skills-counter.json');
184
- await atomicWriteFile(filePath, JSON.stringify({ nextId }));
185
- }
186
-
187
- /**
188
- * Save sessions data as JSON (migration source only).
189
- */
190
- export async function saveSessionsJson(
191
- projectDir: string,
192
- sessions: unknown[],
193
- ): Promise<void> {
194
- const filePath = path.join(projectDir, 'sessions.json');
195
- await atomicWriteFile(filePath, JSON.stringify(sessions, null, 2));
196
- }
197
-
198
- /**
199
- * Load sessions data from JSON (migration source only).
200
- */
201
- export async function loadSessionsJson(projectDir: string): Promise<unknown[]> {
202
- const filePath = path.join(projectDir, 'sessions.json');
203
- try {
204
- const data = await fs.readFile(filePath, 'utf-8');
205
- return JSON.parse(data);
206
- } catch (error) {
207
- if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
208
- return [];
209
- }
210
- throw error;
211
- }
212
- }
1
+ /**
2
+ * JSON Persistence Helpers — Migration / Export / Debug Only
3
+ *
4
+ * These functions read/write JSON/JSONL files for one-time migration
5
+ * from legacy storage formats into SQLite, or for export/import and debug.
6
+ *
7
+ * NOT used as runtime canonical store — SQLite is the sole canonical backend.
8
+ */
9
+
10
+ import { promises as fs } from 'node:fs';
11
+ import path from 'node:path';
12
+ import { atomicWriteFile } from './file-lock.js';
13
+
14
+ /**
15
+ * Get the file path for the knowledge graph JSONL file.
16
+ * (MCP-compatible format, same as official Memory Server)
17
+ */
18
+ export function getGraphFilePath(projectDir: string): string {
19
+ return path.join(projectDir, 'graph.jsonl');
20
+ }
21
+
22
+ /**
23
+ * Save the knowledge graph in JSONL format (MCP-compatible).
24
+ * Each line is a JSON object with type: "entity" or "relation".
25
+ *
26
+ * Format adopted from MCP Official Memory Server.
27
+ */
28
+ export async function saveGraphJsonl(
29
+ projectDir: string,
30
+ entities: Array<{ name: string; entityType: string; observations: string[] }>,
31
+ relations: Array<{ from: string; to: string; relationType: string }>,
32
+ ): Promise<void> {
33
+ const lines = [
34
+ ...entities.map((e) =>
35
+ JSON.stringify({ type: 'entity', name: e.name, entityType: e.entityType, observations: e.observations }),
36
+ ),
37
+ ...relations.map((r) =>
38
+ JSON.stringify({ type: 'relation', from: r.from, to: r.to, relationType: r.relationType }),
39
+ ),
40
+ ];
41
+ await atomicWriteFile(getGraphFilePath(projectDir), lines.join('\n'));
42
+ }
43
+
44
+ /**
45
+ * Load the knowledge graph from JSONL format.
46
+ */
47
+ export async function loadGraphJsonl(
48
+ projectDir: string,
49
+ ): Promise<{
50
+ entities: Array<{ name: string; entityType: string; observations: string[] }>;
51
+ relations: Array<{ from: string; to: string; relationType: string }>;
52
+ }> {
53
+ const filePath = getGraphFilePath(projectDir);
54
+ try {
55
+ const data = await fs.readFile(filePath, 'utf-8');
56
+ const lines = data.split('\n').filter((line) => line.trim() !== '');
57
+ return lines.reduce(
58
+ (graph, line) => {
59
+ const item = JSON.parse(line);
60
+ if (item.type === 'entity') {
61
+ graph.entities.push({
62
+ name: item.name,
63
+ entityType: item.entityType,
64
+ observations: item.observations,
65
+ });
66
+ }
67
+ if (item.type === 'relation') {
68
+ graph.relations.push({
69
+ from: item.from,
70
+ to: item.to,
71
+ relationType: item.relationType,
72
+ });
73
+ }
74
+ return graph;
75
+ },
76
+ {
77
+ entities: [] as Array<{ name: string; entityType: string; observations: string[] }>,
78
+ relations: [] as Array<{ from: string; to: string; relationType: string }>
79
+ },
80
+ );
81
+ } catch (error) {
82
+ if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
83
+ return { entities: [], relations: [] };
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Save observation data as JSON (for Orama restore / export).
91
+ */
92
+ export async function saveObservationsJson(
93
+ projectDir: string,
94
+ observations: unknown[],
95
+ ): Promise<void> {
96
+ const filePath = path.join(projectDir, 'observations.json');
97
+ await atomicWriteFile(filePath, JSON.stringify(observations, null, 2));
98
+ }
99
+
100
+ /**
101
+ * Load observation data from JSON.
102
+ */
103
+ export async function loadObservationsJson(projectDir: string): Promise<unknown[]> {
104
+ const filePath = path.join(projectDir, 'observations.json');
105
+ try {
106
+ const data = await fs.readFile(filePath, 'utf-8');
107
+ return JSON.parse(data);
108
+ } catch (error) {
109
+ if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
110
+ return [];
111
+ }
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Save the next observation ID counter (legacy JSON format).
118
+ */
119
+ export async function saveIdCounter(projectDir: string, nextId: number): Promise<void> {
120
+ const filePath = path.join(projectDir, 'counter.json');
121
+ await atomicWriteFile(filePath, JSON.stringify({ nextId }));
122
+ }
123
+
124
+ /**
125
+ * Load the next observation ID counter (legacy JSON format).
126
+ * For runtime use, prefer the SQLite meta table via SqliteBackend.
127
+ */
128
+ export async function loadIdCounter(projectDir: string): Promise<number> {
129
+ const filePath = path.join(projectDir, 'counter.json');
130
+ try {
131
+ const data = await fs.readFile(filePath, 'utf-8');
132
+ return JSON.parse(data).nextId ?? 1;
133
+ } catch {
134
+ return 1;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Save mini-skills data as JSON (migration source only).
140
+ */
141
+ export async function saveMiniSkillsJson(
142
+ projectDir: string,
143
+ skills: unknown[],
144
+ ): Promise<void> {
145
+ const filePath = path.join(projectDir, 'mini-skills.json');
146
+ await atomicWriteFile(filePath, JSON.stringify(skills, null, 2));
147
+ }
148
+
149
+ /**
150
+ * Load mini-skills data from JSON (migration source only).
151
+ */
152
+ export async function loadMiniSkillsJson(projectDir: string): Promise<unknown[]> {
153
+ const filePath = path.join(projectDir, 'mini-skills.json');
154
+ try {
155
+ const data = await fs.readFile(filePath, 'utf-8');
156
+ const parsed = JSON.parse(data);
157
+ return Array.isArray(parsed) ? parsed : [];
158
+ } catch (error) {
159
+ if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
160
+ return [];
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Load the mini-skills ID counter (legacy JSON format).
168
+ */
169
+ export async function loadMiniSkillsCounter(projectDir: string): Promise<number> {
170
+ const filePath = path.join(projectDir, 'mini-skills-counter.json');
171
+ try {
172
+ const data = await fs.readFile(filePath, 'utf-8');
173
+ return JSON.parse(data).nextId ?? 1;
174
+ } catch {
175
+ return 1;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Save the mini-skills ID counter (legacy JSON format).
181
+ */
182
+ export async function saveMiniSkillsCounter(projectDir: string, nextId: number): Promise<void> {
183
+ const filePath = path.join(projectDir, 'mini-skills-counter.json');
184
+ await atomicWriteFile(filePath, JSON.stringify({ nextId }));
185
+ }
186
+
187
+ /**
188
+ * Save sessions data as JSON (migration source only).
189
+ */
190
+ export async function saveSessionsJson(
191
+ projectDir: string,
192
+ sessions: unknown[],
193
+ ): Promise<void> {
194
+ const filePath = path.join(projectDir, 'sessions.json');
195
+ await atomicWriteFile(filePath, JSON.stringify(sessions, null, 2));
196
+ }
197
+
198
+ /**
199
+ * Load sessions data from JSON (migration source only).
200
+ */
201
+ export async function loadSessionsJson(projectDir: string): Promise<unknown[]> {
202
+ const filePath = path.join(projectDir, 'sessions.json');
203
+ try {
204
+ const data = await fs.readFile(filePath, 'utf-8');
205
+ return JSON.parse(data);
206
+ } catch (error) {
207
+ if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
208
+ return [];
209
+ }
210
+ throw error;
211
+ }
212
+ }