@chendpoc/pi-memory 0.1.11 → 0.1.13

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 (177) hide show
  1. package/README.md +88 -16
  2. package/dist/cache/memoryCaches.d.ts +32 -0
  3. package/dist/cache/memoryCaches.d.ts.map +1 -0
  4. package/dist/cache/memoryCaches.js +59 -0
  5. package/dist/cache/memoryCaches.js.map +1 -0
  6. package/dist/cli.js +123 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts +28 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +37 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/consolidation/enqueue.d.ts +33 -0
  13. package/dist/consolidation/enqueue.d.ts.map +1 -0
  14. package/dist/consolidation/enqueue.js +145 -0
  15. package/dist/consolidation/enqueue.js.map +1 -0
  16. package/dist/consolidation/index.d.ts +19 -0
  17. package/dist/consolidation/index.d.ts.map +1 -0
  18. package/dist/consolidation/index.js +15 -0
  19. package/dist/consolidation/index.js.map +1 -0
  20. package/dist/consolidation/lock.d.ts +5 -0
  21. package/dist/consolidation/lock.d.ts.map +1 -0
  22. package/dist/consolidation/lock.js +33 -0
  23. package/dist/consolidation/lock.js.map +1 -0
  24. package/dist/consolidation/log.d.ts +8 -0
  25. package/dist/consolidation/log.d.ts.map +1 -0
  26. package/dist/consolidation/log.js +32 -0
  27. package/dist/consolidation/log.js.map +1 -0
  28. package/dist/consolidation/memoryIndex.d.ts +18 -0
  29. package/dist/consolidation/memoryIndex.d.ts.map +1 -0
  30. package/dist/consolidation/memoryIndex.js +100 -0
  31. package/dist/consolidation/memoryIndex.js.map +1 -0
  32. package/dist/consolidation/phase2/runPhase2.d.ts +23 -0
  33. package/dist/consolidation/phase2/runPhase2.d.ts.map +1 -0
  34. package/dist/consolidation/phase2/runPhase2.js +196 -0
  35. package/dist/consolidation/phase2/runPhase2.js.map +1 -0
  36. package/dist/consolidation/scheduler/launchd.d.ts +5 -0
  37. package/dist/consolidation/scheduler/launchd.d.ts.map +1 -0
  38. package/dist/consolidation/scheduler/launchd.js +54 -0
  39. package/dist/consolidation/scheduler/launchd.js.map +1 -0
  40. package/dist/consolidation/scheduler/runConsolidate.d.ts +26 -0
  41. package/dist/consolidation/scheduler/runConsolidate.d.ts.map +1 -0
  42. package/dist/consolidation/scheduler/runConsolidate.js +83 -0
  43. package/dist/consolidation/scheduler/runConsolidate.js.map +1 -0
  44. package/dist/consolidation/scheduler/setupSchedule.d.ts +3 -0
  45. package/dist/consolidation/scheduler/setupSchedule.d.ts.map +1 -0
  46. package/dist/consolidation/scheduler/setupSchedule.js +79 -0
  47. package/dist/consolidation/scheduler/setupSchedule.js.map +1 -0
  48. package/dist/consolidation/scheduler/systemd.d.ts +9 -0
  49. package/dist/consolidation/scheduler/systemd.d.ts.map +1 -0
  50. package/dist/consolidation/scheduler/systemd.js +57 -0
  51. package/dist/consolidation/scheduler/systemd.js.map +1 -0
  52. package/dist/consolidation/scheduler/types.d.ts +24 -0
  53. package/dist/consolidation/scheduler/types.d.ts.map +1 -0
  54. package/dist/consolidation/scheduler/types.js +2 -0
  55. package/dist/consolidation/scheduler/types.js.map +1 -0
  56. package/dist/consolidation/scope.d.ts +12 -0
  57. package/dist/consolidation/scope.d.ts.map +1 -0
  58. package/dist/consolidation/scope.js +51 -0
  59. package/dist/consolidation/scope.js.map +1 -0
  60. package/dist/consolidation/stage1/deltaExtract.d.ts +3 -0
  61. package/dist/consolidation/stage1/deltaExtract.d.ts.map +1 -0
  62. package/dist/consolidation/stage1/deltaExtract.js +19 -0
  63. package/dist/consolidation/stage1/deltaExtract.js.map +1 -0
  64. package/dist/consolidation/stage1/drainQueue.d.ts +21 -0
  65. package/dist/consolidation/stage1/drainQueue.d.ts.map +1 -0
  66. package/dist/consolidation/stage1/drainQueue.js +113 -0
  67. package/dist/consolidation/stage1/drainQueue.js.map +1 -0
  68. package/dist/consolidation/stage1/extractSession.d.ts +6 -0
  69. package/dist/consolidation/stage1/extractSession.d.ts.map +1 -0
  70. package/dist/consolidation/stage1/extractSession.js +47 -0
  71. package/dist/consolidation/stage1/extractSession.js.map +1 -0
  72. package/dist/consolidation/stage1/store.d.ts +45 -0
  73. package/dist/consolidation/stage1/store.d.ts.map +1 -0
  74. package/dist/consolidation/stage1/store.js +297 -0
  75. package/dist/consolidation/stage1/store.js.map +1 -0
  76. package/dist/consolidation/types.d.ts +49 -0
  77. package/dist/consolidation/types.d.ts.map +1 -0
  78. package/dist/consolidation/types.js +2 -0
  79. package/dist/consolidation/types.js.map +1 -0
  80. package/dist/fallback/llmRerank.d.ts.map +1 -1
  81. package/dist/fallback/llmRerank.js +9 -1
  82. package/dist/fallback/llmRerank.js.map +1 -1
  83. package/dist/fallback/sessionIndex.d.ts.map +1 -1
  84. package/dist/fallback/sessionIndex.js +15 -63
  85. package/dist/fallback/sessionIndex.js.map +1 -1
  86. package/dist/index.d.ts +4 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +4 -3
  89. package/dist/index.js.map +1 -1
  90. package/dist/local/graphQuery.d.ts +10 -0
  91. package/dist/local/graphQuery.d.ts.map +1 -1
  92. package/dist/local/graphQuery.js +25 -0
  93. package/dist/local/graphQuery.js.map +1 -1
  94. package/dist/pi-extension.d.ts.map +1 -1
  95. package/dist/pi-extension.js +356 -22
  96. package/dist/pi-extension.js.map +1 -1
  97. package/dist/preflight/detectIntents.d.ts.map +1 -1
  98. package/dist/preflight/detectIntents.js +7 -0
  99. package/dist/preflight/detectIntents.js.map +1 -1
  100. package/dist/preflight/hook.d.ts +18 -0
  101. package/dist/preflight/hook.d.ts.map +1 -1
  102. package/dist/preflight/hook.js +52 -7
  103. package/dist/preflight/hook.js.map +1 -1
  104. package/dist/preflight/render.d.ts +16 -2
  105. package/dist/preflight/render.d.ts.map +1 -1
  106. package/dist/preflight/render.js +24 -4
  107. package/dist/preflight/render.js.map +1 -1
  108. package/dist/service.d.ts +16 -0
  109. package/dist/service.d.ts.map +1 -1
  110. package/dist/service.js +57 -0
  111. package/dist/service.js.map +1 -1
  112. package/dist/session/activeBranch.d.ts +17 -0
  113. package/dist/session/activeBranch.d.ts.map +1 -0
  114. package/dist/session/activeBranch.js +251 -0
  115. package/dist/session/activeBranch.js.map +1 -0
  116. package/dist/settings.d.ts +5 -2
  117. package/dist/settings.d.ts.map +1 -1
  118. package/dist/settings.js +18 -1
  119. package/dist/settings.js.map +1 -1
  120. package/dist/tools/memoryAppend.d.ts +14 -4
  121. package/dist/tools/memoryAppend.d.ts.map +1 -1
  122. package/dist/tools/memoryAppend.js +46 -7
  123. package/dist/tools/memoryAppend.js.map +1 -1
  124. package/dist/tools/memoryRecall.d.ts +2 -2
  125. package/dist/tools/memoryRecall.d.ts.map +1 -1
  126. package/dist/tools/memoryRecall.js +35 -9
  127. package/dist/tools/memoryRecall.js.map +1 -1
  128. package/dist/trainer/extractFacts.d.ts +1 -1
  129. package/dist/trainer/index.d.ts +1 -1
  130. package/dist/trainer/index.d.ts.map +1 -1
  131. package/dist/trainer/index.js +1 -1
  132. package/dist/trainer/index.js.map +1 -1
  133. package/dist/trainer/scheduler.d.ts +2 -0
  134. package/dist/trainer/scheduler.d.ts.map +1 -1
  135. package/dist/trainer/scheduler.js +1 -0
  136. package/dist/trainer/scheduler.js.map +1 -1
  137. package/dist/trainer/sessionLoader.d.ts +3 -0
  138. package/dist/trainer/sessionLoader.d.ts.map +1 -1
  139. package/dist/trainer/sessionLoader.js +37 -74
  140. package/dist/trainer/sessionLoader.js.map +1 -1
  141. package/package.json +17 -4
  142. package/src/cache/memoryCaches.ts +72 -0
  143. package/src/cli.ts +144 -0
  144. package/src/config.ts +89 -1
  145. package/src/consolidation/enqueue.ts +205 -0
  146. package/src/consolidation/index.ts +90 -0
  147. package/src/consolidation/lock.ts +41 -0
  148. package/src/consolidation/log.ts +43 -0
  149. package/src/consolidation/memoryIndex.ts +127 -0
  150. package/src/consolidation/phase2/runPhase2.ts +255 -0
  151. package/src/consolidation/scheduler/launchd.ts +62 -0
  152. package/src/consolidation/scheduler/runConsolidate.ts +115 -0
  153. package/src/consolidation/scheduler/setupSchedule.ts +106 -0
  154. package/src/consolidation/scheduler/systemd.ts +78 -0
  155. package/src/consolidation/scheduler/types.ts +27 -0
  156. package/src/consolidation/scope.ts +58 -0
  157. package/src/consolidation/stage1/deltaExtract.ts +29 -0
  158. package/src/consolidation/stage1/drainQueue.ts +143 -0
  159. package/src/consolidation/stage1/extractSession.ts +58 -0
  160. package/src/consolidation/stage1/store.ts +411 -0
  161. package/src/consolidation/types.ts +52 -0
  162. package/src/fallback/llmRerank.ts +8 -1
  163. package/src/fallback/sessionIndex.ts +14 -51
  164. package/src/index.ts +8 -0
  165. package/src/local/graphQuery.ts +24 -0
  166. package/src/pi-extension.ts +393 -28
  167. package/src/preflight/detectIntents.ts +6 -0
  168. package/src/preflight/hook.ts +84 -7
  169. package/src/preflight/render.ts +35 -3
  170. package/src/service.ts +57 -0
  171. package/src/session/activeBranch.ts +294 -0
  172. package/src/settings.ts +37 -2
  173. package/src/tools/memoryAppend.ts +60 -7
  174. package/src/tools/memoryRecall.ts +33 -9
  175. package/src/trainer/index.ts +1 -1
  176. package/src/trainer/scheduler.ts +3 -0
  177. package/src/trainer/sessionLoader.ts +38 -87
package/src/cli.ts CHANGED
@@ -5,6 +5,15 @@ import path from "node:path";
5
5
  import { createStandaloneLLMClient } from "./adapters/piComplete.js";
6
6
  import { installBundle } from "./bundle/install.js";
7
7
  import type { MemoryConfig } from "./config.js";
8
+ import { getMemoryIndexStats } from "./consolidation/memoryIndex.js";
9
+ import { readRecentConsolidationLogs } from "./consolidation/log.js";
10
+ import { getConsolidationStatus } from "./consolidation/enqueue.js";
11
+ import {
12
+ defaultConsolidationDbPath,
13
+ runConsolidate,
14
+ } from "./consolidation/scheduler/runConsolidate.js";
15
+ import { setupSchedule } from "./consolidation/scheduler/setupSchedule.js";
16
+ import type { SchedulePlatform } from "./consolidation/scheduler/types.js";
8
17
  import { loadMemoryConfig } from "./settings.js";
9
18
  import { openSessionIndex } from "./fallback/sessionIndex.js";
10
19
  import { SidecarClient } from "./sidecar/client.js";
@@ -73,6 +82,55 @@ async function main(): Promise<void> {
73
82
  return;
74
83
  }
75
84
 
85
+ if (cmd === "memory-status") {
86
+ const dbPath = defaultConsolidationDbPath(cfg);
87
+ const status = getConsolidationStatus(dbPath);
88
+ const memoryIndex = getMemoryIndexStats(cfg.memoryMdPaths, {
89
+ maxLines: cfg.consolidation.memory_index_max_lines,
90
+ maxBytes: cfg.consolidation.memory_index_max_bytes,
91
+ });
92
+ const recentLogs = await readRecentConsolidationLogs(
93
+ cfg.consolidation.schedule.log_path,
94
+ 5,
95
+ );
96
+ const schedule = await readScheduleStatus(cfg);
97
+ console.log(JSON.stringify({
98
+ db_path: dbPath,
99
+ queue: status,
100
+ memory_index: memoryIndex,
101
+ schedule,
102
+ recent_logs: recentLogs,
103
+ }, null, 2));
104
+ return;
105
+ }
106
+
107
+ if (cmd === "consolidate") {
108
+ const flags = parseConsolidateFlags(rest);
109
+ const result = await runConsolidate({
110
+ config: cfg,
111
+ dryRun: flags.dryRun,
112
+ phase1Only: flags.phase1Only,
113
+ phase2Only: flags.phase2Only,
114
+ });
115
+ console.log(JSON.stringify(result, null, 2));
116
+ return;
117
+ }
118
+
119
+ if (cmd === "setup-schedule") {
120
+ const flags = parseScheduleFlags(rest);
121
+ const platform = resolveSchedulePlatform();
122
+ const result = await setupSchedule({
123
+ hour: flags.hour ?? cfg.consolidation.schedule.hour,
124
+ minute: flags.minute ?? cfg.consolidation.schedule.minute,
125
+ logPath: cfg.consolidation.schedule.log_path,
126
+ dryRun: flags.dryRun,
127
+ remove: flags.remove,
128
+ status: flags.status,
129
+ }, platform);
130
+ console.log(JSON.stringify(result, null, 2));
131
+ return;
132
+ }
133
+
76
134
  if (cmd === "install-bundle") {
77
135
  const bundlePath = rest[0];
78
136
  if (!bundlePath) {
@@ -214,12 +272,98 @@ function parseTrainFlags(args: string[]): {
214
272
  return { sessionsDir, full, dryRun, noMerge, extractor, watch, model };
215
273
  }
216
274
 
275
+ function parseConsolidateFlags(args: string[]): {
276
+ dryRun: boolean;
277
+ phase1Only: boolean;
278
+ phase2Only: boolean;
279
+ } {
280
+ return {
281
+ dryRun: args.includes("--dry-run"),
282
+ phase1Only: args.includes("--phase1-only"),
283
+ phase2Only: args.includes("--phase2-only"),
284
+ };
285
+ }
286
+
287
+ function parseScheduleFlags(args: string[]): {
288
+ hour?: number;
289
+ minute?: number;
290
+ dryRun: boolean;
291
+ remove: boolean;
292
+ status: boolean;
293
+ } {
294
+ let hour: number | undefined;
295
+ let minute: number | undefined;
296
+ for (let i = 0; i < args.length; i++) {
297
+ const arg = args[i]!;
298
+ if (arg === "--hour" && i + 1 < args.length) {
299
+ hour = Number(args[++i]);
300
+ continue;
301
+ }
302
+ if (arg === "--minute" && i + 1 < args.length) {
303
+ minute = Number(args[++i]);
304
+ continue;
305
+ }
306
+ }
307
+ return {
308
+ hour,
309
+ minute,
310
+ dryRun: args.includes("--dry-run"),
311
+ remove: args.includes("--remove"),
312
+ status: args.includes("--status"),
313
+ };
314
+ }
315
+
316
+ function resolveSchedulePlatform(): SchedulePlatform {
317
+ if (process.platform === "darwin") return "darwin";
318
+ if (process.platform === "linux") return "linux";
319
+ throw new Error(`setup-schedule is not supported on ${process.platform}`);
320
+ }
321
+
322
+ function tryResolveSchedulePlatform(): SchedulePlatform | null {
323
+ if (process.platform === "darwin") return "darwin";
324
+ if (process.platform === "linux") return "linux";
325
+ return null;
326
+ }
327
+
328
+ async function readScheduleStatus(cfg: MemoryConfig) {
329
+ const platform = tryResolveSchedulePlatform();
330
+ if (!platform) {
331
+ return {
332
+ supported: false,
333
+ platform: process.platform,
334
+ files: [],
335
+ };
336
+ }
337
+ const result = await setupSchedule({
338
+ hour: cfg.consolidation.schedule.hour,
339
+ minute: cfg.consolidation.schedule.minute,
340
+ logPath: cfg.consolidation.schedule.log_path,
341
+ status: true,
342
+ }, platform);
343
+ return {
344
+ supported: true,
345
+ platform,
346
+ files: result.files,
347
+ };
348
+ }
349
+
217
350
  function printHelp(): void {
218
351
  console.log(`pi-memory — local TLM episodic memory (mode B)
219
352
 
220
353
  Commands:
221
354
  health Start sidecar (if bundle present) and print /health
222
355
  status Print MemoryService status snapshot
356
+ memory-status Print consolidation queue, MEMORY.md cap, schedule status, and recent job logs
357
+ consolidate Run offline memory consolidation
358
+ --dry-run Report planned work without writing MEMORY.md/stage1
359
+ --phase1-only Drain queue and train graph only
360
+ --phase2-only Consume existing stage1 rows only
361
+ setup-schedule Install/update the OS user scheduler
362
+ --hour Local hour (default from memory.json consolidation.schedule.hour)
363
+ --minute Local minute (default from memory.json consolidation.schedule.minute)
364
+ --dry-run Print files that would be written
365
+ --remove Remove scheduler files
366
+ --status Report scheduler file existence
223
367
  query POST /query with JSON QueryIntent
224
368
  install-bundle Copy a local bundle dir into ~/.pi/memory/current
225
369
  train Build a bundle from session history (delta merge by default)
package/src/config.ts CHANGED
@@ -21,6 +21,32 @@ export interface TrainerConfig {
21
21
  auto_interval: string | null;
22
22
  }
23
23
 
24
+ export interface ConsolidationScheduleConfig {
25
+ /** Local hour for the background consolidation job (default 3). */
26
+ hour: number;
27
+ /** Local minute for the background consolidation job (default 0). */
28
+ minute: number;
29
+ /** Structured JSONL log path (default ~/.pi/memory/consolidation.log). */
30
+ log_path: string;
31
+ }
32
+
33
+ export interface ConsolidationConfig {
34
+ /** Whether session_shutdown enqueues sessions for offline consolidation. */
35
+ enabled: boolean;
36
+ /** Minimum non-empty user turns before enqueue (default 3). */
37
+ min_user_turns: number;
38
+ /** MEMORY.md session cap by line count (default 200). */
39
+ memory_index_max_lines: number;
40
+ /** MEMORY.md session cap by UTF-8 bytes (default 25KB). */
41
+ memory_index_max_bytes: number;
42
+ /** Maximum stage1 rows selected per Phase2 run (default 20). */
43
+ phase2_top_n: number;
44
+ /** M4 prune threshold by last usage (default 90 days). */
45
+ max_unused_days: number;
46
+ /** Daily OS scheduler defaults. */
47
+ schedule: ConsolidationScheduleConfig;
48
+ }
49
+
24
50
  export interface MemoryConfig {
25
51
  provider: MemoryProvider;
26
52
  tlmPath: string;
@@ -32,6 +58,7 @@ export interface MemoryConfig {
32
58
  sessionsDir: string;
33
59
  memoryMdPaths: string[];
34
60
  trainer: TrainerConfig;
61
+ consolidation: ConsolidationConfig;
35
62
  }
36
63
 
37
64
  export const defaultTrainerConfig: TrainerConfig = {
@@ -40,10 +67,37 @@ export const defaultTrainerConfig: TrainerConfig = {
40
67
  auto_interval: null,
41
68
  };
42
69
 
70
+ export function defaultConsolidationConfig(
71
+ overrides: Partial<ConsolidationConfig> & {
72
+ schedule?: Partial<ConsolidationScheduleConfig>;
73
+ } = {},
74
+ ): ConsolidationConfig {
75
+ const defaultSchedule: ConsolidationScheduleConfig = {
76
+ hour: 3,
77
+ minute: 0,
78
+ log_path: path.join(defaultBundleRoot(), "consolidation.log"),
79
+ };
80
+ const { schedule, ...rest } = overrides;
81
+ return {
82
+ enabled: true,
83
+ min_user_turns: 3,
84
+ memory_index_max_lines: 200,
85
+ memory_index_max_bytes: 25_600,
86
+ phase2_top_n: 20,
87
+ max_unused_days: 90,
88
+ schedule: { ...defaultSchedule, ...schedule },
89
+ ...rest,
90
+ };
91
+ }
92
+
43
93
  export function defaultMemoryConfig(
44
94
  overrides: Partial<MemoryConfig> = {},
45
95
  ): MemoryConfig {
46
- const { trainer: trainerOverrides, ...rest } = overrides;
96
+ const {
97
+ trainer: trainerOverrides,
98
+ consolidation: consolidationOverrides,
99
+ ...rest
100
+ } = overrides;
47
101
  return {
48
102
  provider: "local",
49
103
  tlmPath: "tlm",
@@ -55,6 +109,7 @@ export function defaultMemoryConfig(
55
109
  sessionsDir: defaultSessionsDir(),
56
110
  memoryMdPaths: [path.join(defaultPiHome(), "MEMORY.md")],
57
111
  trainer: { ...defaultTrainerConfig, ...trainerOverrides },
112
+ consolidation: defaultConsolidationConfig(consolidationOverrides),
58
113
  ...rest,
59
114
  };
60
115
  }
@@ -65,6 +120,12 @@ export function normalizeMemoryConfig(
65
120
  ): MemoryConfig {
66
121
  const base = defaultMemoryConfig();
67
122
  const rawTrainer = (raw.trainer ?? {}) as Partial<TrainerConfig>;
123
+ const rawConsolidation = (raw.consolidation ?? {}) as
124
+ Partial<ConsolidationConfig> & {
125
+ schedule?: Partial<ConsolidationScheduleConfig>;
126
+ };
127
+ const rawSchedule = (rawConsolidation.schedule ?? {}) as
128
+ Partial<ConsolidationScheduleConfig>;
68
129
  return {
69
130
  provider: (raw.provider as MemoryProvider) ?? base.provider,
70
131
  tlmPath: expandPath(String(raw.tlmPath ?? base.tlmPath)),
@@ -88,5 +149,32 @@ export function normalizeMemoryConfig(
88
149
  ? rawTrainer.auto_interval
89
150
  : base.trainer.auto_interval,
90
151
  },
152
+ consolidation: {
153
+ enabled: rawConsolidation.enabled ?? base.consolidation.enabled,
154
+ min_user_turns: Number(
155
+ rawConsolidation.min_user_turns ?? base.consolidation.min_user_turns,
156
+ ),
157
+ memory_index_max_lines: Number(
158
+ rawConsolidation.memory_index_max_lines ??
159
+ base.consolidation.memory_index_max_lines,
160
+ ),
161
+ memory_index_max_bytes: Number(
162
+ rawConsolidation.memory_index_max_bytes ??
163
+ base.consolidation.memory_index_max_bytes,
164
+ ),
165
+ phase2_top_n: Number(
166
+ rawConsolidation.phase2_top_n ?? base.consolidation.phase2_top_n,
167
+ ),
168
+ max_unused_days: Number(
169
+ rawConsolidation.max_unused_days ?? base.consolidation.max_unused_days,
170
+ ),
171
+ schedule: {
172
+ hour: Number(rawSchedule.hour ?? base.consolidation.schedule.hour),
173
+ minute: Number(rawSchedule.minute ?? base.consolidation.schedule.minute),
174
+ log_path: expandPath(
175
+ String(rawSchedule.log_path ?? base.consolidation.schedule.log_path),
176
+ ),
177
+ },
178
+ },
91
179
  };
92
180
  }
@@ -0,0 +1,205 @@
1
+ import fs from "node:fs";
2
+
3
+ import type {
4
+ ConsolidationStatus,
5
+ JobReport,
6
+ PendingSession,
7
+ ConsolidationStatusReport,
8
+ } from "./types.js";
9
+ import { openConsolidationStore, type SqliteDatabase } from "./stage1/store.js";
10
+
11
+ export interface EnqueueSessionInput {
12
+ session_id: string;
13
+ session_file: string;
14
+ cwd: string;
15
+ git_root: string | null;
16
+ project_hash: string | null;
17
+ parent_session_id: string | null;
18
+ parent_session_file: string | null;
19
+ user_turn_count: number;
20
+ ended_at: string;
21
+ }
22
+
23
+ export interface EnqueueOptions {
24
+ enabled?: boolean;
25
+ minUserTurns?: number;
26
+ /** Injected sqlite handle for tests. */
27
+ db?: SqliteDatabase;
28
+ /** Force replacing existing done/skipped sessions. */
29
+ force?: boolean;
30
+ /** Override status for the newly enqueued job (default: pending). */
31
+ status?: ConsolidationStatus;
32
+ /** Optional now source for created_at / updated_at. */
33
+ now?: Date | string;
34
+ /** Test seam: production defaults to checking session file existence. */
35
+ checkFileExists?: boolean;
36
+ }
37
+
38
+ function toIso(value: Date | string | undefined): string {
39
+ const dt = value ? new Date(value) : new Date();
40
+ return dt.toISOString();
41
+ }
42
+
43
+ function isMissingFile(file: string): boolean {
44
+ return typeof file !== "string" || !file.trim();
45
+ }
46
+
47
+ function isAllowedStatus(status: string): status is ConsolidationStatus {
48
+ return status === "pending" || status === "processing" || status === "done" || status === "skipped" ||
49
+ status === "failed";
50
+ }
51
+
52
+ export function enqueueSession(
53
+ dbPath: string,
54
+ meta: EnqueueSessionInput,
55
+ opts: EnqueueOptions = {},
56
+ ): JobReport {
57
+ const enabled = opts.enabled !== false;
58
+ const minUserTurns = opts.minUserTurns ?? 3;
59
+ const now = toIso(opts.now);
60
+
61
+ if (!enabled) {
62
+ return {
63
+ session_id: meta.session_id,
64
+ enqueued: false,
65
+ action: "skipped",
66
+ status: "skipped",
67
+ reason: "consolidation_disabled",
68
+ now,
69
+ };
70
+ }
71
+
72
+ if (isMissingFile(meta.session_file)) {
73
+ return {
74
+ session_id: meta.session_id,
75
+ enqueued: false,
76
+ action: "skipped",
77
+ status: "skipped",
78
+ reason: "missing_session_file",
79
+ now,
80
+ };
81
+ }
82
+
83
+ if (opts.checkFileExists !== false && !fs.existsSync(meta.session_file)) {
84
+ return {
85
+ session_id: meta.session_id,
86
+ enqueued: false,
87
+ action: "skipped",
88
+ status: "skipped",
89
+ reason: "session_file_not_found",
90
+ now,
91
+ };
92
+ }
93
+
94
+ if (meta.user_turn_count < minUserTurns) {
95
+ return {
96
+ session_id: meta.session_id,
97
+ enqueued: false,
98
+ action: "skipped",
99
+ status: "skipped",
100
+ reason: "insufficient_user_turns",
101
+ now,
102
+ };
103
+ }
104
+
105
+ const store = openConsolidationStore(dbPath, opts.db);
106
+ if (!store) {
107
+ return {
108
+ session_id: meta.session_id,
109
+ enqueued: false,
110
+ action: "skipped",
111
+ status: "skipped",
112
+ reason: "missing_sqlite",
113
+ now,
114
+ };
115
+ }
116
+
117
+ const incomingStatus: ConsolidationStatus = isAllowedStatus(opts.status ?? "") ? opts.status! : "pending";
118
+ const existing = store.getPendingSession(meta.session_id);
119
+ const isFinal = existing?.status === "done" || existing?.status === "skipped";
120
+
121
+ if (existing && isFinal && !opts.force) {
122
+ store.close();
123
+ return {
124
+ session_id: meta.session_id,
125
+ enqueued: false,
126
+ action: "skipped",
127
+ status: existing.status,
128
+ reason: "already_finalized",
129
+ now,
130
+ };
131
+ }
132
+
133
+ const existingCreatedAt = existing?.created_at ?? now;
134
+ const payload: Omit<PendingSession, "created_at" | "updated_at"> & {
135
+ created_at: string;
136
+ updated_at: string;
137
+ } = {
138
+ session_id: meta.session_id,
139
+ session_file: meta.session_file,
140
+ cwd: meta.cwd,
141
+ git_root: meta.git_root,
142
+ project_hash: meta.project_hash,
143
+ parent_session_id: meta.parent_session_id,
144
+ parent_session_file: meta.parent_session_file,
145
+ user_turn_count: meta.user_turn_count,
146
+ ended_at: meta.ended_at,
147
+ status: incomingStatus,
148
+ error_message: null,
149
+ created_at: existingCreatedAt,
150
+ updated_at: now,
151
+ };
152
+
153
+ try {
154
+ store.upsertPendingSession(payload);
155
+ const action = existing ? "updated" : "created";
156
+ return {
157
+ session_id: meta.session_id,
158
+ enqueued: true,
159
+ action,
160
+ status: incomingStatus,
161
+ now,
162
+ };
163
+ } finally {
164
+ store.close();
165
+ }
166
+ }
167
+
168
+ export interface ConsolidationStatusQueryOptions {
169
+ db?: SqliteDatabase;
170
+ }
171
+
172
+ export function getConsolidationStatus(
173
+ dbPath: string,
174
+ opts: ConsolidationStatusQueryOptions = {},
175
+ ): ConsolidationStatusReport {
176
+ const store = openConsolidationStore(dbPath, opts.db);
177
+ if (!store) {
178
+ return {
179
+ pending: 0,
180
+ processing: 0,
181
+ done: 0,
182
+ skipped: 0,
183
+ failed: 0,
184
+ stage1Count: 0,
185
+ lastGeneratedAt: null,
186
+ lastUpdatedAt: null,
187
+ };
188
+ }
189
+
190
+ try {
191
+ const byStatus = store.getConsolidationStatusCounts();
192
+ return {
193
+ pending: byStatus.pending,
194
+ processing: byStatus.processing,
195
+ done: byStatus.done,
196
+ skipped: byStatus.skipped,
197
+ failed: byStatus.failed,
198
+ stage1Count: store.getStage1Count(),
199
+ lastGeneratedAt: store.getLastGeneratedAt(),
200
+ lastUpdatedAt: store.getLastUpdatedAt(),
201
+ };
202
+ } finally {
203
+ store.close();
204
+ }
205
+ }
@@ -0,0 +1,90 @@
1
+ export type {
2
+ ConsolidationStatus,
3
+ ConsolidationStatusReport,
4
+ JobReport,
5
+ PendingSession,
6
+ Stage1Output,
7
+ } from "./types.js";
8
+
9
+ export {
10
+ getMemoryIndexStats,
11
+ memoryIndexSnippet,
12
+ readMemoryIndexCap,
13
+ type MemoryIndexCapOptions,
14
+ type MemoryIndexStats,
15
+ } from "./memoryIndex.js";
16
+
17
+ export {
18
+ findGitRoot,
19
+ lineMatchesScope,
20
+ parseScopeComment,
21
+ projectHash,
22
+ projectScope,
23
+ scopeForCwd,
24
+ type MemoryScope,
25
+ } from "./scope.js";
26
+
27
+ export {
28
+ appendConsolidationLog,
29
+ readRecentConsolidationLogs,
30
+ type ConsolidationLogEntry,
31
+ } from "./log.js";
32
+
33
+ export { withFileLock } from "./lock.js";
34
+
35
+ export { enqueueSession, getConsolidationStatus } from "./enqueue.js";
36
+ export type {
37
+ ConsolidationStatusQueryOptions,
38
+ EnqueueOptions,
39
+ EnqueueSessionInput,
40
+ } from "./enqueue.js";
41
+
42
+ export { openConsolidationStore } from "./stage1/store.js";
43
+ export type { SqliteDatabase, ConsolidationStore } from "./stage1/store.js";
44
+
45
+ export {
46
+ computeDeltaTurns,
47
+ } from "./stage1/deltaExtract.js";
48
+
49
+ export {
50
+ drainQueue,
51
+ type DrainQueueOptions,
52
+ type DrainQueueReport,
53
+ } from "./stage1/drainQueue.js";
54
+
55
+ export { extractSessionToStage1 } from "./stage1/extractSession.js";
56
+
57
+ export {
58
+ runPhase2,
59
+ type Phase2Options,
60
+ type Phase2Report,
61
+ } from "./phase2/runPhase2.js";
62
+
63
+ export {
64
+ defaultConsolidationDbPath,
65
+ runConsolidate,
66
+ type ConsolidateOptions,
67
+ type ConsolidateReport,
68
+ } from "./scheduler/runConsolidate.js";
69
+
70
+ export {
71
+ buildLaunchdPlist,
72
+ launchdPlistPath,
73
+ } from "./scheduler/launchd.js";
74
+
75
+ export {
76
+ buildSystemdService,
77
+ buildSystemdTimer,
78
+ systemdServicePath,
79
+ systemdTimerPath,
80
+ } from "./scheduler/systemd.js";
81
+
82
+ export { setupSchedule } from "./scheduler/setupSchedule.js";
83
+
84
+ export type {
85
+ ScheduleAction,
86
+ ScheduledFile,
87
+ ScheduleOptions,
88
+ SchedulePlatform,
89
+ SetupScheduleResult,
90
+ } from "./scheduler/types.js";
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const LOCK_RETRIES = 50;
5
+ const LOCK_DELAY_MS = 100;
6
+
7
+ async function sleep(ms: number): Promise<void> {
8
+ await new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ export async function withFileLock<T>(
12
+ lockPath: string,
13
+ fn: () => Promise<T>,
14
+ opts: { retries?: number; delayMs?: number } = {},
15
+ ): Promise<T> {
16
+ const retries = opts.retries ?? LOCK_RETRIES;
17
+ const delayMs = opts.delayMs ?? LOCK_DELAY_MS;
18
+ let handle: fs.FileHandle | null = null;
19
+
20
+ await fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
21
+
22
+ for (let i = 0; i < retries; i++) {
23
+ try {
24
+ handle = await fs.open(lockPath, "wx");
25
+ break;
26
+ } catch {
27
+ await sleep(delayMs);
28
+ }
29
+ }
30
+
31
+ if (!handle) {
32
+ throw new Error(`could not acquire lock: ${lockPath}`);
33
+ }
34
+
35
+ try {
36
+ return await fn();
37
+ } finally {
38
+ await handle.close().catch(() => {});
39
+ await fs.unlink(lockPath).catch(() => {});
40
+ }
41
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export interface ConsolidationLogEntry {
5
+ ts?: string;
6
+ phase: string;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ export async function appendConsolidationLog(
11
+ logPath: string,
12
+ entry: ConsolidationLogEntry,
13
+ ): Promise<void> {
14
+ await fs.mkdir(path.dirname(logPath), { recursive: true, mode: 0o700 });
15
+ const line = JSON.stringify({
16
+ ts: entry.ts ?? new Date().toISOString(),
17
+ ...entry,
18
+ });
19
+ await fs.appendFile(logPath, `${line}\n`, { encoding: "utf8", mode: 0o600 });
20
+ }
21
+
22
+ export async function readRecentConsolidationLogs(
23
+ logPath: string,
24
+ limit = 5,
25
+ ): Promise<ConsolidationLogEntry[]> {
26
+ let text: string;
27
+ try {
28
+ text = await fs.readFile(logPath, "utf8");
29
+ } catch {
30
+ return [];
31
+ }
32
+ return text
33
+ .split("\n")
34
+ .filter((line) => line.trim())
35
+ .slice(-limit)
36
+ .map((line) => {
37
+ try {
38
+ return JSON.parse(line) as ConsolidationLogEntry;
39
+ } catch {
40
+ return { phase: "invalid_log_line", raw: line };
41
+ }
42
+ });
43
+ }