@litmers/cursorflow-orchestrator 0.1.20 → 0.1.26

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 (224) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +4 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +8 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +392 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +4 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +9 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1131 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +388 -162
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +374 -2
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * State management utilities for CursorFlow
3
+ *
4
+ * Features:
5
+ * - Atomic writes to prevent corruption
6
+ * - State validation and repair
7
+ * - Versioned state with conflict detection
3
8
  */
4
9
 
5
10
  import * as fs from 'fs';
@@ -12,10 +17,33 @@ import {
12
17
  EventEntry,
13
18
  RunnerConfig
14
19
  } from './types';
20
+ import { tryAcquireLockSync, releaseLockSync, LockOptions } from './lock';
21
+ import * as git from './git';
22
+
15
23
  export { LaneState, ConversationEntry, GitLogEntry, EventEntry };
16
24
 
17
25
  /**
18
- * Save state to JSON file
26
+ * Extended state with metadata for versioning
27
+ */
28
+ export interface VersionedState<T> {
29
+ _version: number;
30
+ _updatedAt: number;
31
+ _pid: number;
32
+ data: T;
33
+ }
34
+
35
+ /**
36
+ * State validation result
37
+ */
38
+ export interface StateValidationResult {
39
+ valid: boolean;
40
+ issues: string[];
41
+ repaired: boolean;
42
+ repairedState?: LaneState;
43
+ }
44
+
45
+ /**
46
+ * Save state to JSON file with atomic write
19
47
  */
20
48
  export function saveState(statePath: string, state: any): void {
21
49
  const stateDir = path.dirname(statePath);
@@ -24,11 +52,64 @@ export function saveState(statePath: string, state: any): void {
24
52
  fs.mkdirSync(stateDir, { recursive: true });
25
53
  }
26
54
 
27
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
55
+ // Atomic write: write to temp file, then rename
56
+ const tempPath = `${statePath}.tmp.${process.pid}`;
57
+
58
+ try {
59
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), 'utf8');
60
+ fs.renameSync(tempPath, statePath);
61
+ } catch (error) {
62
+ // Clean up temp file if rename fails
63
+ try {
64
+ if (fs.existsSync(tempPath)) {
65
+ fs.unlinkSync(tempPath);
66
+ }
67
+ } catch {
68
+ // Ignore cleanup errors
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Save state with file lock to prevent race conditions
76
+ */
77
+ export function saveStateWithLock(statePath: string, state: any, lockOptions?: LockOptions): void {
78
+ const lockPath = `${statePath}.lock`;
79
+
80
+ // Try to acquire lock
81
+ const maxRetries = 50;
82
+ let acquired = false;
83
+ let retries = 0;
84
+
85
+ while (retries < maxRetries && !acquired) {
86
+ acquired = tryAcquireLockSync(lockPath, {
87
+ operation: 'saveState',
88
+ staleTimeoutMs: 10000,
89
+ ...lockOptions
90
+ });
91
+
92
+ if (!acquired) {
93
+ retries++;
94
+ // Sync sleep
95
+ const end = Date.now() + 100;
96
+ while (Date.now() < end) { /* wait */ }
97
+ }
98
+ }
99
+
100
+ if (!acquired) {
101
+ throw new Error(`Failed to acquire lock for state file: ${statePath}`);
102
+ }
103
+
104
+ try {
105
+ saveState(statePath, state);
106
+ } finally {
107
+ releaseLockSync(lockPath);
108
+ }
28
109
  }
29
110
 
30
111
  /**
31
- * Load state from JSON file
112
+ * Load state from JSON file with corruption recovery
32
113
  */
33
114
  export function loadState<T = any>(statePath: string): T | null {
34
115
  if (!fs.existsSync(statePath)) {
@@ -39,11 +120,296 @@ export function loadState<T = any>(statePath: string): T | null {
39
120
  const content = fs.readFileSync(statePath, 'utf8');
40
121
  return JSON.parse(content) as T;
41
122
  } catch (error: any) {
123
+ // Try to recover from backup if exists
124
+ const backupPath = `${statePath}.backup`;
125
+ if (fs.existsSync(backupPath)) {
126
+ try {
127
+ console.warn(`Warning: Main state file corrupted, trying backup: ${statePath}`);
128
+ const backupContent = fs.readFileSync(backupPath, 'utf8');
129
+ const backupState = JSON.parse(backupContent) as T;
130
+
131
+ // Restore from backup
132
+ saveState(statePath, backupState);
133
+ return backupState;
134
+ } catch {
135
+ // Backup also corrupted
136
+ }
137
+ }
138
+
42
139
  console.warn(`Warning: Failed to parse state file ${statePath}: ${error.message}`);
43
140
  return null;
44
141
  }
45
142
  }
46
143
 
144
+ /**
145
+ * Load state with automatic backup creation
146
+ */
147
+ export function loadStateWithBackup<T = any>(statePath: string): T | null {
148
+ const state = loadState<T>(statePath);
149
+
150
+ if (state) {
151
+ // Create backup
152
+ const backupPath = `${statePath}.backup`;
153
+ try {
154
+ fs.copyFileSync(statePath, backupPath);
155
+ } catch {
156
+ // Ignore backup failures
157
+ }
158
+ }
159
+
160
+ return state;
161
+ }
162
+
163
+ /**
164
+ * Update state with optimistic locking
165
+ */
166
+ export function updateStateAtomic(
167
+ statePath: string,
168
+ updater: (state: LaneState | null) => LaneState
169
+ ): LaneState {
170
+ const lockPath = `${statePath}.lock`;
171
+
172
+ // Acquire lock
173
+ const maxRetries = 50;
174
+ let acquired = false;
175
+ let retries = 0;
176
+
177
+ while (retries < maxRetries && !acquired) {
178
+ acquired = tryAcquireLockSync(lockPath, { operation: 'updateState', staleTimeoutMs: 10000 });
179
+ if (!acquired) {
180
+ retries++;
181
+ const end = Date.now() + 100;
182
+ while (Date.now() < end) { /* wait */ }
183
+ }
184
+ }
185
+
186
+ if (!acquired) {
187
+ throw new Error(`Failed to acquire lock for state update: ${statePath}`);
188
+ }
189
+
190
+ try {
191
+ const currentState = loadState<LaneState>(statePath);
192
+ const newState = updater(currentState);
193
+ newState.updatedAt = Date.now();
194
+ saveState(statePath, newState);
195
+ return newState;
196
+ } finally {
197
+ releaseLockSync(lockPath);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Validate lane state and check for inconsistencies
203
+ */
204
+ export function validateLaneState(
205
+ statePath: string,
206
+ options: {
207
+ checkWorktree?: boolean;
208
+ checkBranch?: boolean;
209
+ autoRepair?: boolean;
210
+ } = {}
211
+ ): StateValidationResult {
212
+ const state = loadState<LaneState>(statePath);
213
+ const issues: string[] = [];
214
+ let repaired = false;
215
+ let repairedState: LaneState | undefined;
216
+
217
+ if (!state) {
218
+ return {
219
+ valid: false,
220
+ issues: ['State file not found or corrupted'],
221
+ repaired: false,
222
+ };
223
+ }
224
+
225
+ // Check required fields
226
+ if (!state.label) {
227
+ issues.push('Missing label field');
228
+ }
229
+
230
+ if (state.status === undefined) {
231
+ issues.push('Missing status field');
232
+ }
233
+
234
+ if (state.currentTaskIndex === undefined || state.currentTaskIndex < 0) {
235
+ issues.push(`Invalid currentTaskIndex: ${state.currentTaskIndex}`);
236
+ }
237
+
238
+ if (state.totalTasks === undefined || state.totalTasks < 0) {
239
+ issues.push(`Invalid totalTasks: ${state.totalTasks}`);
240
+ }
241
+
242
+ if (state.currentTaskIndex > state.totalTasks) {
243
+ issues.push(`currentTaskIndex (${state.currentTaskIndex}) exceeds totalTasks (${state.totalTasks})`);
244
+ }
245
+
246
+ // Check worktree existence
247
+ if (options.checkWorktree && state.worktreeDir) {
248
+ if (!fs.existsSync(state.worktreeDir)) {
249
+ issues.push(`Worktree directory not found: ${state.worktreeDir}`);
250
+
251
+ if (options.autoRepair) {
252
+ state.worktreeDir = null;
253
+ repaired = true;
254
+ }
255
+ }
256
+ }
257
+
258
+ // Check branch existence
259
+ if (options.checkBranch && state.pipelineBranch) {
260
+ try {
261
+ if (!git.branchExists(state.pipelineBranch)) {
262
+ issues.push(`Branch not found: ${state.pipelineBranch}`);
263
+
264
+ if (options.autoRepair) {
265
+ state.pipelineBranch = null;
266
+ repaired = true;
267
+ }
268
+ }
269
+ } catch {
270
+ // Git check failed, don't add issue
271
+ }
272
+ }
273
+
274
+ // Check status consistency
275
+ if (state.status === 'completed' && !state.endTime) {
276
+ issues.push('Status is completed but endTime is not set');
277
+
278
+ if (options.autoRepair) {
279
+ state.endTime = Date.now();
280
+ repaired = true;
281
+ }
282
+ }
283
+
284
+ if (state.status === 'failed' && !state.error) {
285
+ issues.push('Status is failed but no error message');
286
+ }
287
+
288
+ // Save repaired state if auto-repair was enabled
289
+ if (repaired && options.autoRepair) {
290
+ state.updatedAt = Date.now();
291
+ saveState(statePath, state);
292
+ repairedState = state;
293
+ }
294
+
295
+ return {
296
+ valid: issues.length === 0,
297
+ issues,
298
+ repaired,
299
+ repairedState,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Repair lane state by resetting inconsistent values
305
+ */
306
+ export function repairLaneState(statePath: string): LaneState | null {
307
+ const result = validateLaneState(statePath, {
308
+ checkWorktree: true,
309
+ checkBranch: true,
310
+ autoRepair: true,
311
+ });
312
+
313
+ if (result.repairedState) {
314
+ return result.repairedState;
315
+ }
316
+
317
+ // If validation failed completely, try to create minimal valid state
318
+ const state = loadState<LaneState>(statePath);
319
+ if (!state) {
320
+ return null;
321
+ }
322
+
323
+ // Ensure all required fields have valid values
324
+ const repairedState: LaneState = {
325
+ label: state.label || path.basename(path.dirname(statePath)),
326
+ status: 'pending',
327
+ currentTaskIndex: Math.max(0, state.currentTaskIndex || 0),
328
+ totalTasks: Math.max(0, state.totalTasks || 0),
329
+ worktreeDir: state.worktreeDir && fs.existsSync(state.worktreeDir) ? state.worktreeDir : null,
330
+ pipelineBranch: state.pipelineBranch,
331
+ startTime: state.startTime || Date.now(),
332
+ endTime: null,
333
+ error: null,
334
+ dependencyRequest: null,
335
+ tasksFile: state.tasksFile,
336
+ dependsOn: state.dependsOn || [],
337
+ completedTasks: state.completedTasks || [],
338
+ updatedAt: Date.now(),
339
+ };
340
+
341
+ // Ensure currentTaskIndex doesn't exceed totalTasks
342
+ if (repairedState.currentTaskIndex > repairedState.totalTasks) {
343
+ repairedState.currentTaskIndex = repairedState.totalTasks;
344
+ }
345
+
346
+ saveState(statePath, repairedState);
347
+ return repairedState;
348
+ }
349
+
350
+ /**
351
+ * Check if state needs recovery (e.g., after crash)
352
+ */
353
+ export function stateNeedsRecovery(statePath: string): boolean {
354
+ const state = loadState<LaneState>(statePath);
355
+
356
+ if (!state) {
357
+ return false;
358
+ }
359
+
360
+ // Running state with no recent update might indicate a crash
361
+ if (state.status === 'running') {
362
+ const lastUpdate = state.updatedAt || state.startTime;
363
+ const staleThresholdMs = 5 * 60 * 1000; // 5 minutes
364
+
365
+ if (Date.now() - lastUpdate > staleThresholdMs) {
366
+ return true;
367
+ }
368
+ }
369
+
370
+ // Check for temp files indicating incomplete write
371
+ const tempPattern = `${statePath}.tmp.`;
372
+ const stateDir = path.dirname(statePath);
373
+
374
+ try {
375
+ const files = fs.readdirSync(stateDir);
376
+ for (const file of files) {
377
+ if (file.startsWith(path.basename(tempPattern))) {
378
+ return true;
379
+ }
380
+ }
381
+ } catch {
382
+ // Ignore
383
+ }
384
+
385
+ return false;
386
+ }
387
+
388
+ /**
389
+ * Clean up temp files from incomplete writes
390
+ */
391
+ export function cleanupTempFiles(stateDir: string): number {
392
+ let cleaned = 0;
393
+
394
+ try {
395
+ const files = fs.readdirSync(stateDir);
396
+ for (const file of files) {
397
+ if (file.includes('.tmp.')) {
398
+ try {
399
+ fs.unlinkSync(safeJoin(stateDir, file));
400
+ cleaned++;
401
+ } catch {
402
+ // Ignore
403
+ }
404
+ }
405
+ }
406
+ } catch {
407
+ // Ignore
408
+ }
409
+
410
+ return cleaned;
411
+ }
412
+
47
413
  /**
48
414
  * Append to JSONL log file
49
415
  */