aicodeman 0.2.8

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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +403 -0
  3. package/dist/ai-checker-base.d.ts +175 -0
  4. package/dist/ai-checker-base.d.ts.map +1 -0
  5. package/dist/ai-checker-base.js +424 -0
  6. package/dist/ai-checker-base.js.map +1 -0
  7. package/dist/ai-idle-checker.d.ts +53 -0
  8. package/dist/ai-idle-checker.d.ts.map +1 -0
  9. package/dist/ai-idle-checker.js +141 -0
  10. package/dist/ai-idle-checker.js.map +1 -0
  11. package/dist/ai-plan-checker.d.ts +52 -0
  12. package/dist/ai-plan-checker.d.ts.map +1 -0
  13. package/dist/ai-plan-checker.js +103 -0
  14. package/dist/ai-plan-checker.js.map +1 -0
  15. package/dist/bash-tool-parser.d.ts +191 -0
  16. package/dist/bash-tool-parser.d.ts.map +1 -0
  17. package/dist/bash-tool-parser.js +598 -0
  18. package/dist/bash-tool-parser.js.map +1 -0
  19. package/dist/cli.d.ts +12 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +460 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config/buffer-limits.d.ts +59 -0
  24. package/dist/config/buffer-limits.d.ts.map +1 -0
  25. package/dist/config/buffer-limits.js +74 -0
  26. package/dist/config/buffer-limits.js.map +1 -0
  27. package/dist/config/map-limits.d.ts +40 -0
  28. package/dist/config/map-limits.d.ts.map +1 -0
  29. package/dist/config/map-limits.js +52 -0
  30. package/dist/config/map-limits.js.map +1 -0
  31. package/dist/file-stream-manager.d.ts +148 -0
  32. package/dist/file-stream-manager.d.ts.map +1 -0
  33. package/dist/file-stream-manager.js +351 -0
  34. package/dist/file-stream-manager.js.map +1 -0
  35. package/dist/hooks-config.d.ts +31 -0
  36. package/dist/hooks-config.d.ts.map +1 -0
  37. package/dist/hooks-config.js +115 -0
  38. package/dist/hooks-config.js.map +1 -0
  39. package/dist/image-watcher.d.ts +86 -0
  40. package/dist/image-watcher.d.ts.map +1 -0
  41. package/dist/image-watcher.js +275 -0
  42. package/dist/image-watcher.js.map +1 -0
  43. package/dist/index.d.ts +11 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +54 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/mux-factory.d.ts +13 -0
  48. package/dist/mux-factory.d.ts.map +1 -0
  49. package/dist/mux-factory.js +19 -0
  50. package/dist/mux-factory.js.map +1 -0
  51. package/dist/mux-interface.d.ts +145 -0
  52. package/dist/mux-interface.d.ts.map +1 -0
  53. package/dist/mux-interface.js +9 -0
  54. package/dist/mux-interface.js.map +1 -0
  55. package/dist/plan-orchestrator.d.ts +123 -0
  56. package/dist/plan-orchestrator.d.ts.map +1 -0
  57. package/dist/plan-orchestrator.js +500 -0
  58. package/dist/plan-orchestrator.js.map +1 -0
  59. package/dist/prompts/index.d.ts +9 -0
  60. package/dist/prompts/index.d.ts.map +1 -0
  61. package/dist/prompts/index.js +9 -0
  62. package/dist/prompts/index.js.map +1 -0
  63. package/dist/prompts/planner.d.ts +14 -0
  64. package/dist/prompts/planner.d.ts.map +1 -0
  65. package/dist/prompts/planner.js +83 -0
  66. package/dist/prompts/planner.js.map +1 -0
  67. package/dist/prompts/research-agent.d.ts +10 -0
  68. package/dist/prompts/research-agent.d.ts.map +1 -0
  69. package/dist/prompts/research-agent.js +143 -0
  70. package/dist/prompts/research-agent.js.map +1 -0
  71. package/dist/push-store.d.ts +41 -0
  72. package/dist/push-store.d.ts.map +1 -0
  73. package/dist/push-store.js +168 -0
  74. package/dist/push-store.js.map +1 -0
  75. package/dist/ralph-config.d.ts +67 -0
  76. package/dist/ralph-config.d.ts.map +1 -0
  77. package/dist/ralph-config.js +134 -0
  78. package/dist/ralph-config.js.map +1 -0
  79. package/dist/ralph-loop.d.ts +124 -0
  80. package/dist/ralph-loop.d.ts.map +1 -0
  81. package/dist/ralph-loop.js +418 -0
  82. package/dist/ralph-loop.js.map +1 -0
  83. package/dist/ralph-tracker.d.ts +1081 -0
  84. package/dist/ralph-tracker.d.ts.map +1 -0
  85. package/dist/ralph-tracker.js +3343 -0
  86. package/dist/ralph-tracker.js.map +1 -0
  87. package/dist/respawn-controller.d.ts +1182 -0
  88. package/dist/respawn-controller.d.ts.map +1 -0
  89. package/dist/respawn-controller.js +2754 -0
  90. package/dist/respawn-controller.js.map +1 -0
  91. package/dist/run-summary.d.ts +123 -0
  92. package/dist/run-summary.d.ts.map +1 -0
  93. package/dist/run-summary.js +325 -0
  94. package/dist/run-summary.js.map +1 -0
  95. package/dist/session-lifecycle-log.d.ts +36 -0
  96. package/dist/session-lifecycle-log.d.ts.map +1 -0
  97. package/dist/session-lifecycle-log.js +101 -0
  98. package/dist/session-lifecycle-log.js.map +1 -0
  99. package/dist/session-manager.d.ts +97 -0
  100. package/dist/session-manager.d.ts.map +1 -0
  101. package/dist/session-manager.js +224 -0
  102. package/dist/session-manager.js.map +1 -0
  103. package/dist/session.d.ts +686 -0
  104. package/dist/session.d.ts.map +1 -0
  105. package/dist/session.js +2025 -0
  106. package/dist/session.js.map +1 -0
  107. package/dist/state-store.d.ts +189 -0
  108. package/dist/state-store.d.ts.map +1 -0
  109. package/dist/state-store.js +730 -0
  110. package/dist/state-store.js.map +1 -0
  111. package/dist/subagent-watcher.d.ts +345 -0
  112. package/dist/subagent-watcher.d.ts.map +1 -0
  113. package/dist/subagent-watcher.js +1469 -0
  114. package/dist/subagent-watcher.js.map +1 -0
  115. package/dist/task-queue.d.ts +108 -0
  116. package/dist/task-queue.d.ts.map +1 -0
  117. package/dist/task-queue.js +235 -0
  118. package/dist/task-queue.js.map +1 -0
  119. package/dist/task-tracker.d.ts +306 -0
  120. package/dist/task-tracker.d.ts.map +1 -0
  121. package/dist/task-tracker.js +488 -0
  122. package/dist/task-tracker.js.map +1 -0
  123. package/dist/task.d.ts +73 -0
  124. package/dist/task.d.ts.map +1 -0
  125. package/dist/task.js +177 -0
  126. package/dist/task.js.map +1 -0
  127. package/dist/team-watcher.d.ts +53 -0
  128. package/dist/team-watcher.d.ts.map +1 -0
  129. package/dist/team-watcher.js +313 -0
  130. package/dist/team-watcher.js.map +1 -0
  131. package/dist/templates/case-template.md +461 -0
  132. package/dist/templates/claude-md.d.ts +26 -0
  133. package/dist/templates/claude-md.d.ts.map +1 -0
  134. package/dist/templates/claude-md.js +74 -0
  135. package/dist/templates/claude-md.js.map +1 -0
  136. package/dist/tmux-manager.d.ts +181 -0
  137. package/dist/tmux-manager.d.ts.map +1 -0
  138. package/dist/tmux-manager.js +1405 -0
  139. package/dist/tmux-manager.js.map +1 -0
  140. package/dist/transcript-watcher.d.ts +110 -0
  141. package/dist/transcript-watcher.d.ts.map +1 -0
  142. package/dist/transcript-watcher.js +338 -0
  143. package/dist/transcript-watcher.js.map +1 -0
  144. package/dist/tunnel-manager.d.ts +54 -0
  145. package/dist/tunnel-manager.d.ts.map +1 -0
  146. package/dist/tunnel-manager.js +251 -0
  147. package/dist/tunnel-manager.js.map +1 -0
  148. package/dist/types.d.ts +1139 -0
  149. package/dist/types.d.ts.map +1 -0
  150. package/dist/types.js +215 -0
  151. package/dist/types.js.map +1 -0
  152. package/dist/utils/buffer-accumulator.d.ts +111 -0
  153. package/dist/utils/buffer-accumulator.d.ts.map +1 -0
  154. package/dist/utils/buffer-accumulator.js +172 -0
  155. package/dist/utils/buffer-accumulator.js.map +1 -0
  156. package/dist/utils/claude-cli-resolver.d.ts +26 -0
  157. package/dist/utils/claude-cli-resolver.d.ts.map +1 -0
  158. package/dist/utils/claude-cli-resolver.js +78 -0
  159. package/dist/utils/claude-cli-resolver.js.map +1 -0
  160. package/dist/utils/cleanup-manager.d.ts +165 -0
  161. package/dist/utils/cleanup-manager.d.ts.map +1 -0
  162. package/dist/utils/cleanup-manager.js +274 -0
  163. package/dist/utils/cleanup-manager.js.map +1 -0
  164. package/dist/utils/index.d.ts +19 -0
  165. package/dist/utils/index.d.ts.map +1 -0
  166. package/dist/utils/index.js +19 -0
  167. package/dist/utils/index.js.map +1 -0
  168. package/dist/utils/lru-map.d.ts +140 -0
  169. package/dist/utils/lru-map.d.ts.map +1 -0
  170. package/dist/utils/lru-map.js +234 -0
  171. package/dist/utils/lru-map.js.map +1 -0
  172. package/dist/utils/nice-wrapper.d.ts +13 -0
  173. package/dist/utils/nice-wrapper.d.ts.map +1 -0
  174. package/dist/utils/nice-wrapper.js +17 -0
  175. package/dist/utils/nice-wrapper.js.map +1 -0
  176. package/dist/utils/opencode-cli-resolver.d.ts +21 -0
  177. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -0
  178. package/dist/utils/opencode-cli-resolver.js +67 -0
  179. package/dist/utils/opencode-cli-resolver.js.map +1 -0
  180. package/dist/utils/regex-patterns.d.ts +64 -0
  181. package/dist/utils/regex-patterns.d.ts.map +1 -0
  182. package/dist/utils/regex-patterns.js +74 -0
  183. package/dist/utils/regex-patterns.js.map +1 -0
  184. package/dist/utils/stale-expiration-map.d.ts +159 -0
  185. package/dist/utils/stale-expiration-map.d.ts.map +1 -0
  186. package/dist/utils/stale-expiration-map.js +277 -0
  187. package/dist/utils/stale-expiration-map.js.map +1 -0
  188. package/dist/utils/string-similarity.d.ts +108 -0
  189. package/dist/utils/string-similarity.d.ts.map +1 -0
  190. package/dist/utils/string-similarity.js +189 -0
  191. package/dist/utils/string-similarity.js.map +1 -0
  192. package/dist/utils/token-validation.d.ts +39 -0
  193. package/dist/utils/token-validation.d.ts.map +1 -0
  194. package/dist/utils/token-validation.js +59 -0
  195. package/dist/utils/token-validation.js.map +1 -0
  196. package/dist/utils/type-safety.d.ts +33 -0
  197. package/dist/utils/type-safety.d.ts.map +1 -0
  198. package/dist/utils/type-safety.js +35 -0
  199. package/dist/utils/type-safety.js.map +1 -0
  200. package/dist/web/public/app.js +491 -0
  201. package/dist/web/public/app.js.br +0 -0
  202. package/dist/web/public/app.js.gz +0 -0
  203. package/dist/web/public/index.html +1675 -0
  204. package/dist/web/public/index.html.br +0 -0
  205. package/dist/web/public/index.html.gz +0 -0
  206. package/dist/web/public/manifest.json +8 -0
  207. package/dist/web/public/mobile.css +1 -0
  208. package/dist/web/public/mobile.css.br +0 -0
  209. package/dist/web/public/mobile.css.gz +0 -0
  210. package/dist/web/public/ralph-wizard.js +1037 -0
  211. package/dist/web/public/ralph-wizard.js.br +0 -0
  212. package/dist/web/public/ralph-wizard.js.gz +0 -0
  213. package/dist/web/public/styles.css +1 -0
  214. package/dist/web/public/styles.css.br +0 -0
  215. package/dist/web/public/styles.css.gz +0 -0
  216. package/dist/web/public/sw.js +67 -0
  217. package/dist/web/public/sw.js.br +0 -0
  218. package/dist/web/public/sw.js.gz +0 -0
  219. package/dist/web/public/upload.html +155 -0
  220. package/dist/web/public/upload.html.br +0 -0
  221. package/dist/web/public/upload.html.gz +0 -0
  222. package/dist/web/public/vendor/xterm-addon-fit.min.js +1 -0
  223. package/dist/web/public/vendor/xterm-addon-fit.min.js.br +0 -0
  224. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  225. package/dist/web/public/vendor/xterm-addon-unicode11.min.js +1 -0
  226. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.br +0 -0
  227. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  228. package/dist/web/public/vendor/xterm-addon-webgl.min.js +2 -0
  229. package/dist/web/public/vendor/xterm-addon-webgl.min.js.br +0 -0
  230. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  231. package/dist/web/public/vendor/xterm.css +209 -0
  232. package/dist/web/public/vendor/xterm.css.br +0 -0
  233. package/dist/web/public/vendor/xterm.css.gz +0 -0
  234. package/dist/web/public/vendor/xterm.min.js +9 -0
  235. package/dist/web/public/vendor/xterm.min.js.br +0 -0
  236. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  237. package/dist/web/schemas.d.ts +479 -0
  238. package/dist/web/schemas.d.ts.map +1 -0
  239. package/dist/web/schemas.js +448 -0
  240. package/dist/web/schemas.js.map +1 -0
  241. package/dist/web/server.d.ts +207 -0
  242. package/dist/web/server.d.ts.map +1 -0
  243. package/dist/web/server.js +5784 -0
  244. package/dist/web/server.js.map +1 -0
  245. package/package.json +110 -0
  246. package/scripts/postinstall.js +390 -0
@@ -0,0 +1,730 @@
1
+ /**
2
+ * @fileoverview Persistent JSON state storage for Codeman.
3
+ *
4
+ * This module provides the StateStore class which persists application state
5
+ * to `~/.codeman/state.json` with debounced writes to prevent excessive disk I/O.
6
+ *
7
+ * State is split into two files:
8
+ * - `state.json`: Main app state (sessions, tasks, config)
9
+ * - `state-inner.json`: Inner loop state (todos, Ralph loop state per session)
10
+ *
11
+ * The separation reduces write frequency since Ralph state changes rapidly
12
+ * during Ralph Wiggum loops.
13
+ *
14
+ * @module state-store
15
+ */
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync, copyFileSync } from 'node:fs';
17
+ import { writeFile, rename, unlink, copyFile, access } from 'node:fs/promises';
18
+ import { homedir } from 'node:os';
19
+ import { dirname, join } from 'node:path';
20
+ import { createInitialState, createInitialRalphSessionState, createInitialGlobalStats, } from './types.js';
21
+ import { MAX_SESSION_TOKENS } from './utils/index.js';
22
+ /** Debounce delay for batching state writes (ms) */
23
+ const SAVE_DEBOUNCE_MS = 500;
24
+ /**
25
+ * Persistent JSON state storage with debounced writes.
26
+ *
27
+ * State is automatically loaded on construction and saved with 500ms
28
+ * debouncing to batch rapid updates into single disk writes.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const store = new StateStore();
33
+ *
34
+ * // Read state
35
+ * const sessions = store.getState().sessions;
36
+ *
37
+ * // Modify and save
38
+ * store.getState().sessions[id] = sessionState;
39
+ * store.save(); // Debounced - won't write immediately
40
+ *
41
+ * // Force immediate write
42
+ * store.saveNow();
43
+ * ```
44
+ */
45
+ /** Maximum consecutive save failures before circuit breaker opens */
46
+ const MAX_CONSECUTIVE_FAILURES = 3;
47
+ export class StateStore {
48
+ state;
49
+ filePath;
50
+ saveTimeout = null;
51
+ dirty = false;
52
+ // Inner state storage (separate from main state to reduce write frequency)
53
+ ralphStates = new Map();
54
+ ralphStatePath;
55
+ ralphStateSaveTimeout = null;
56
+ ralphStateDirty = false;
57
+ // Circuit breaker for save failures (prevents hammering disk on persistent errors)
58
+ consecutiveSaveFailures = 0;
59
+ circuitBreakerOpen = false;
60
+ // Guard against concurrent saveNowAsync() calls (debounce can race with in-flight write)
61
+ _saveInFlight = null;
62
+ constructor(filePath) {
63
+ // Migrate legacy data directory (~/.claudeman → ~/.codeman)
64
+ if (!filePath) {
65
+ const legacyDir = join(homedir(), '.claudeman');
66
+ const newDir = join(homedir(), '.codeman');
67
+ if (existsSync(legacyDir) && !existsSync(newDir)) {
68
+ console.log(`[state-store] Migrating data directory: ${legacyDir} → ${newDir}`);
69
+ renameSync(legacyDir, newDir);
70
+ }
71
+ const legacyCasesDir = join(homedir(), 'claudeman-cases');
72
+ const newCasesDir = join(homedir(), 'codeman-cases');
73
+ if (existsSync(legacyCasesDir) && !existsSync(newCasesDir)) {
74
+ console.log(`[state-store] Migrating cases directory: ${legacyCasesDir} → ${newCasesDir}`);
75
+ renameSync(legacyCasesDir, newCasesDir);
76
+ }
77
+ }
78
+ this.filePath = filePath || join(homedir(), '.codeman', 'state.json');
79
+ this.ralphStatePath = this.filePath.replace('.json', '-inner.json');
80
+ this.state = this.load();
81
+ this.state.config.stateFilePath = this.filePath;
82
+ this.loadRalphStates();
83
+ }
84
+ ensureDir() {
85
+ const dir = dirname(this.filePath);
86
+ if (!existsSync(dir)) {
87
+ // Use restrictive permissions (0o700) - owner only can read/write/traverse
88
+ // State files may contain sensitive session data
89
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
90
+ }
91
+ }
92
+ load() {
93
+ // Try main file first, then .bak fallback
94
+ for (const path of [this.filePath, this.filePath + '.bak']) {
95
+ try {
96
+ if (existsSync(path)) {
97
+ const data = readFileSync(path, 'utf-8');
98
+ const parsed = JSON.parse(data);
99
+ const initial = createInitialState();
100
+ const result = {
101
+ ...initial,
102
+ ...parsed,
103
+ sessions: { ...parsed.sessions },
104
+ tasks: { ...parsed.tasks },
105
+ ralphLoop: { ...initial.ralphLoop, ...parsed.ralphLoop },
106
+ config: { ...initial.config, ...parsed.config },
107
+ };
108
+ if (path !== this.filePath) {
109
+ console.warn(`[StateStore] Recovered state from backup: ${path}`);
110
+ }
111
+ return result;
112
+ }
113
+ }
114
+ catch (err) {
115
+ console.error(`Failed to load state from ${path}:`, err);
116
+ }
117
+ }
118
+ return createInitialState();
119
+ }
120
+ /**
121
+ * Schedules a debounced save.
122
+ * Multiple calls within 500ms are batched into a single disk write.
123
+ * Uses async I/O to avoid blocking the event loop.
124
+ */
125
+ save() {
126
+ this.dirty = true;
127
+ if (this.saveTimeout) {
128
+ return; // Already scheduled
129
+ }
130
+ this.saveTimeout = setTimeout(() => {
131
+ this.saveNowAsync().catch((err) => {
132
+ console.error('[StateStore] Async save failed:', err);
133
+ });
134
+ }, SAVE_DEBOUNCE_MS);
135
+ }
136
+ /**
137
+ * Async version of saveNow — used by the debounced save() path.
138
+ * Uses non-blocking fs.promises to avoid blocking the event loop during
139
+ * the debounced write cycle. For synchronous shutdown flush, use saveNow().
140
+ *
141
+ * Guards against concurrent execution: if a save is already in flight,
142
+ * waits for it to complete then re-checks dirty flag before starting another.
143
+ */
144
+ async saveNowAsync() {
145
+ if (this._saveInFlight) {
146
+ await this._saveInFlight;
147
+ // After waiting, re-check if still dirty (the previous save may have handled it)
148
+ if (!this.dirty)
149
+ return;
150
+ }
151
+ this._saveInFlight = this._doSaveAsync();
152
+ try {
153
+ await this._saveInFlight;
154
+ }
155
+ finally {
156
+ this._saveInFlight = null;
157
+ }
158
+ }
159
+ async _doSaveAsync() {
160
+ if (this.saveTimeout) {
161
+ clearTimeout(this.saveTimeout);
162
+ this.saveTimeout = null;
163
+ }
164
+ if (!this.dirty) {
165
+ return;
166
+ }
167
+ // Circuit breaker: stop attempting writes after too many failures
168
+ if (this.circuitBreakerOpen) {
169
+ console.warn('[StateStore] Circuit breaker open - skipping save (too many consecutive failures)');
170
+ return;
171
+ }
172
+ this.ensureDir();
173
+ const tempPath = this.filePath + '.tmp';
174
+ const backupPath = this.filePath + '.bak';
175
+ let json;
176
+ // Step 1: Serialize state (validates it's JSON-safe)
177
+ try {
178
+ json = JSON.stringify(this.state);
179
+ }
180
+ catch (err) {
181
+ console.error('[StateStore] Failed to serialize state (circular reference or invalid data):', err);
182
+ this.consecutiveSaveFailures++;
183
+ if (this.consecutiveSaveFailures >= MAX_CONSECUTIVE_FAILURES) {
184
+ console.error('[StateStore] Circuit breaker OPEN - serialization failing repeatedly');
185
+ this.circuitBreakerOpen = true;
186
+ }
187
+ return;
188
+ }
189
+ // Clear dirty flag BEFORE async I/O so mutations during write re-set it.
190
+ // The state snapshot is already captured in `json` above.
191
+ this.dirty = false;
192
+ // Step 2: Create backup via file copy (async, no read+parse+write)
193
+ try {
194
+ await access(this.filePath);
195
+ await copyFile(this.filePath, backupPath);
196
+ }
197
+ catch {
198
+ // Backup failed or file doesn't exist yet - continue with write
199
+ }
200
+ // Step 3: Atomic write: write to temp file, then rename (async)
201
+ try {
202
+ await writeFile(tempPath, json, 'utf-8');
203
+ await rename(tempPath, this.filePath);
204
+ this.consecutiveSaveFailures = 0;
205
+ if (this.circuitBreakerOpen) {
206
+ console.log('[StateStore] Circuit breaker CLOSED - save succeeded');
207
+ this.circuitBreakerOpen = false;
208
+ }
209
+ }
210
+ catch (err) {
211
+ console.error('[StateStore] Failed to write state file:', err);
212
+ // Re-mark dirty so the data is retried on the next save cycle
213
+ this.dirty = true;
214
+ this.consecutiveSaveFailures++;
215
+ // Try to clean up temp file on error
216
+ try {
217
+ await unlink(tempPath);
218
+ }
219
+ catch {
220
+ // Temp file may not exist
221
+ }
222
+ // Check circuit breaker threshold
223
+ if (this.consecutiveSaveFailures >= MAX_CONSECUTIVE_FAILURES) {
224
+ console.error('[StateStore] Circuit breaker OPEN - writes failing repeatedly');
225
+ this.circuitBreakerOpen = true;
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Synchronous immediate write to disk using atomic write pattern.
231
+ * Used by flushAll() during shutdown when async is not appropriate.
232
+ * Prefer saveNowAsync() for normal operation.
233
+ */
234
+ saveNow() {
235
+ if (this.saveTimeout) {
236
+ clearTimeout(this.saveTimeout);
237
+ this.saveTimeout = null;
238
+ }
239
+ if (!this.dirty) {
240
+ return;
241
+ }
242
+ if (this.circuitBreakerOpen) {
243
+ console.warn('[StateStore] Circuit breaker open - skipping save (too many consecutive failures)');
244
+ return;
245
+ }
246
+ this.ensureDir();
247
+ const tempPath = this.filePath + '.tmp';
248
+ const backupPath = this.filePath + '.bak';
249
+ let json;
250
+ try {
251
+ json = JSON.stringify(this.state);
252
+ }
253
+ catch (err) {
254
+ console.error('[StateStore] Failed to serialize state (circular reference or invalid data):', err);
255
+ this.consecutiveSaveFailures++;
256
+ if (this.consecutiveSaveFailures >= MAX_CONSECUTIVE_FAILURES) {
257
+ console.error('[StateStore] Circuit breaker OPEN - serialization failing repeatedly');
258
+ this.circuitBreakerOpen = true;
259
+ }
260
+ return;
261
+ }
262
+ // Backup via atomic copy (avoids reading entire file into memory)
263
+ try {
264
+ if (existsSync(this.filePath)) {
265
+ copyFileSync(this.filePath, backupPath);
266
+ }
267
+ }
268
+ catch {
269
+ // Backup failed - continue with write
270
+ }
271
+ try {
272
+ writeFileSync(tempPath, json, 'utf-8');
273
+ renameSync(tempPath, this.filePath);
274
+ // Clear dirty flag only AFTER successful write
275
+ this.dirty = false;
276
+ this.consecutiveSaveFailures = 0;
277
+ if (this.circuitBreakerOpen) {
278
+ console.log('[StateStore] Circuit breaker CLOSED - save succeeded');
279
+ this.circuitBreakerOpen = false;
280
+ }
281
+ }
282
+ catch (err) {
283
+ console.error('[StateStore] Failed to write state file:', err);
284
+ this.consecutiveSaveFailures++;
285
+ try {
286
+ if (existsSync(tempPath))
287
+ unlinkSync(tempPath);
288
+ }
289
+ catch {
290
+ /* ignore */
291
+ }
292
+ if (this.consecutiveSaveFailures >= MAX_CONSECUTIVE_FAILURES) {
293
+ console.error('[StateStore] Circuit breaker OPEN - writes failing repeatedly');
294
+ this.circuitBreakerOpen = true;
295
+ }
296
+ }
297
+ }
298
+ /**
299
+ * Attempt to recover state from backup file.
300
+ * Call this if main state file is corrupt.
301
+ */
302
+ recoverFromBackup() {
303
+ const backupPath = this.filePath + '.bak';
304
+ try {
305
+ if (existsSync(backupPath)) {
306
+ const backupContent = readFileSync(backupPath, 'utf-8');
307
+ const parsed = JSON.parse(backupContent);
308
+ const initial = createInitialState();
309
+ this.state = {
310
+ ...initial,
311
+ ...parsed,
312
+ sessions: { ...parsed.sessions },
313
+ tasks: { ...parsed.tasks },
314
+ ralphLoop: { ...initial.ralphLoop, ...parsed.ralphLoop },
315
+ config: { ...initial.config, ...parsed.config },
316
+ };
317
+ console.log('[StateStore] Successfully recovered state from backup');
318
+ // Reset circuit breaker after successful recovery
319
+ this.circuitBreakerOpen = false;
320
+ this.consecutiveSaveFailures = 0;
321
+ return true;
322
+ }
323
+ }
324
+ catch (err) {
325
+ console.error('[StateStore] Failed to recover from backup:', err);
326
+ }
327
+ return false;
328
+ }
329
+ /**
330
+ * Reset the circuit breaker (for manual intervention).
331
+ */
332
+ resetCircuitBreaker() {
333
+ this.circuitBreakerOpen = false;
334
+ this.consecutiveSaveFailures = 0;
335
+ console.log('[StateStore] Circuit breaker manually reset');
336
+ }
337
+ /** Flushes any pending main state save. Call before shutdown. */
338
+ flush() {
339
+ this.saveNow();
340
+ }
341
+ /** Returns the full application state object. */
342
+ getState() {
343
+ return this.state;
344
+ }
345
+ /** Returns all session states keyed by session ID. */
346
+ getSessions() {
347
+ return this.state.sessions;
348
+ }
349
+ /** Returns a session state by ID, or null if not found. */
350
+ getSession(id) {
351
+ return this.state.sessions[id] ?? null;
352
+ }
353
+ /** Sets a session state and triggers a debounced save. */
354
+ setSession(id, session) {
355
+ this.state.sessions[id] = session;
356
+ this.save();
357
+ }
358
+ /** Removes a session state and triggers a debounced save. */
359
+ removeSession(id) {
360
+ delete this.state.sessions[id];
361
+ this.save();
362
+ }
363
+ /**
364
+ * Cleans up stale sessions from state that don't have corresponding active sessions.
365
+ * @param activeSessionIds - Set of currently active session IDs
366
+ * @returns Number of sessions cleaned up
367
+ */
368
+ cleanupStaleSessions(activeSessionIds) {
369
+ const allSessionIds = Object.keys(this.state.sessions);
370
+ const cleaned = [];
371
+ for (const sessionId of allSessionIds) {
372
+ if (!activeSessionIds.has(sessionId)) {
373
+ const name = this.state.sessions[sessionId]?.name;
374
+ cleaned.push({ id: sessionId, name });
375
+ delete this.state.sessions[sessionId];
376
+ // Also clean up Ralph state for this session
377
+ this.ralphStates.delete(sessionId);
378
+ }
379
+ }
380
+ if (cleaned.length > 0) {
381
+ console.log(`[StateStore] Cleaned up ${cleaned.length} stale session(s) from state`);
382
+ this.save();
383
+ }
384
+ return { count: cleaned.length, cleaned };
385
+ }
386
+ /** Returns all task states keyed by task ID. */
387
+ getTasks() {
388
+ return this.state.tasks;
389
+ }
390
+ /** Returns a task state by ID, or null if not found. */
391
+ getTask(id) {
392
+ return this.state.tasks[id] ?? null;
393
+ }
394
+ /** Sets a task state and triggers a debounced save. */
395
+ setTask(id, task) {
396
+ this.state.tasks[id] = task;
397
+ this.save();
398
+ }
399
+ /** Removes a task state and triggers a debounced save. */
400
+ removeTask(id) {
401
+ delete this.state.tasks[id];
402
+ this.save();
403
+ }
404
+ /** Returns the Ralph Loop state. */
405
+ getRalphLoopState() {
406
+ return this.state.ralphLoop;
407
+ }
408
+ /** Updates Ralph Loop state (partial merge) and triggers a debounced save. */
409
+ setRalphLoopState(ralphLoop) {
410
+ this.state.ralphLoop = { ...this.state.ralphLoop, ...ralphLoop };
411
+ this.save();
412
+ }
413
+ /** Returns the application configuration. */
414
+ getConfig() {
415
+ return this.state.config;
416
+ }
417
+ /** Updates configuration (partial merge) and triggers a debounced save. */
418
+ setConfig(config) {
419
+ this.state.config = { ...this.state.config, ...config };
420
+ this.save();
421
+ }
422
+ /** Resets all state to initial values and saves immediately. */
423
+ reset() {
424
+ this.state = createInitialState();
425
+ this.state.config.stateFilePath = this.filePath;
426
+ this.ralphStates.clear();
427
+ this.saveNow(); // Immediate save for reset operations
428
+ this.saveRalphStatesNow();
429
+ }
430
+ // ========== Global Stats Methods ==========
431
+ /** Returns global stats, creating initial stats if needed. */
432
+ getGlobalStats() {
433
+ if (!this.state.globalStats) {
434
+ this.state.globalStats = createInitialGlobalStats();
435
+ }
436
+ return this.state.globalStats;
437
+ }
438
+ /**
439
+ * Adds tokens and cost to global stats.
440
+ * Call when a session is deleted to preserve its usage in lifetime stats.
441
+ */
442
+ addToGlobalStats(inputTokens, outputTokens, cost) {
443
+ // Sanity check: reject absurdly large values
444
+ if (inputTokens > MAX_SESSION_TOKENS || outputTokens > MAX_SESSION_TOKENS) {
445
+ console.warn(`[StateStore] Rejected absurd global stats: input=${inputTokens}, output=${outputTokens}`);
446
+ return;
447
+ }
448
+ // Reject negative values
449
+ if (inputTokens < 0 || outputTokens < 0 || cost < 0) {
450
+ console.warn(`[StateStore] Rejected negative global stats: input=${inputTokens}, output=${outputTokens}, cost=${cost}`);
451
+ return;
452
+ }
453
+ const stats = this.getGlobalStats();
454
+ stats.totalInputTokens += inputTokens;
455
+ stats.totalOutputTokens += outputTokens;
456
+ stats.totalCost += cost;
457
+ stats.lastUpdatedAt = Date.now();
458
+ this.save();
459
+ }
460
+ /** Increments the total sessions created counter. */
461
+ incrementSessionsCreated() {
462
+ const stats = this.getGlobalStats();
463
+ stats.totalSessionsCreated += 1;
464
+ stats.lastUpdatedAt = Date.now();
465
+ this.save();
466
+ }
467
+ /**
468
+ * Returns aggregate stats combining global (deleted sessions) + active sessions.
469
+ * @param activeSessions Map of active session states
470
+ */
471
+ getAggregateStats(activeSessions) {
472
+ const global = this.getGlobalStats();
473
+ let activeInput = 0;
474
+ let activeOutput = 0;
475
+ let activeCost = 0;
476
+ let activeCount = 0;
477
+ for (const session of Object.values(activeSessions)) {
478
+ activeInput += session.inputTokens ?? 0;
479
+ activeOutput += session.outputTokens ?? 0;
480
+ activeCost += session.totalCost ?? 0;
481
+ activeCount++;
482
+ }
483
+ return {
484
+ totalInputTokens: global.totalInputTokens + activeInput,
485
+ totalOutputTokens: global.totalOutputTokens + activeOutput,
486
+ totalCost: global.totalCost + activeCost,
487
+ totalSessionsCreated: global.totalSessionsCreated,
488
+ activeSessionsCount: activeCount,
489
+ };
490
+ }
491
+ // ========== Token Stats Methods (Daily Tracking) ==========
492
+ /** Maximum days to keep in daily history */
493
+ static MAX_DAILY_HISTORY = 30;
494
+ /**
495
+ * Get or initialize token stats from state.
496
+ */
497
+ getTokenStats() {
498
+ if (!this.state.tokenStats) {
499
+ this.state.tokenStats = {
500
+ daily: [],
501
+ lastUpdated: Date.now(),
502
+ };
503
+ }
504
+ return this.state.tokenStats;
505
+ }
506
+ /**
507
+ * Get today's date string in YYYY-MM-DD format.
508
+ */
509
+ getTodayDateString() {
510
+ const now = new Date();
511
+ return now.toISOString().split('T')[0];
512
+ }
513
+ /**
514
+ * Calculate estimated cost from tokens using Claude Opus pricing.
515
+ * Input: $15/M tokens, Output: $75/M tokens
516
+ */
517
+ calculateEstimatedCost(inputTokens, outputTokens) {
518
+ const inputCost = (inputTokens / 1000000) * 15;
519
+ const outputCost = (outputTokens / 1000000) * 75;
520
+ return inputCost + outputCost;
521
+ }
522
+ // Track unique sessions per day for accurate session count
523
+ dailySessionIds = new Set();
524
+ dailySessionDate = '';
525
+ /**
526
+ * Record token usage for today.
527
+ * Accumulates tokens to today's entry, creating it if needed.
528
+ * @param inputTokens Input tokens to add
529
+ * @param outputTokens Output tokens to add
530
+ * @param sessionId Optional session ID for unique session counting
531
+ */
532
+ recordDailyUsage(inputTokens, outputTokens, sessionId) {
533
+ if (inputTokens <= 0 && outputTokens <= 0)
534
+ return;
535
+ // Sanity check: reject absurdly large values (max 1M tokens per recording)
536
+ // Claude's context window is ~200k, so 1M per recording is already very generous
537
+ const MAX_TOKENS_PER_RECORDING = 1_000_000;
538
+ if (inputTokens > MAX_TOKENS_PER_RECORDING || outputTokens > MAX_TOKENS_PER_RECORDING) {
539
+ console.warn(`[StateStore] Rejected absurd token values: input=${inputTokens}, output=${outputTokens}`);
540
+ return;
541
+ }
542
+ const stats = this.getTokenStats();
543
+ const today = this.getTodayDateString();
544
+ // Reset daily session tracking on date change
545
+ if (this.dailySessionDate !== today) {
546
+ this.dailySessionIds.clear();
547
+ this.dailySessionDate = today;
548
+ }
549
+ // Find or create today's entry
550
+ let todayEntry = stats.daily.find((e) => e.date === today);
551
+ if (!todayEntry) {
552
+ todayEntry = {
553
+ date: today,
554
+ inputTokens: 0,
555
+ outputTokens: 0,
556
+ estimatedCost: 0,
557
+ sessions: 0,
558
+ };
559
+ stats.daily.unshift(todayEntry); // Add to front (most recent first)
560
+ }
561
+ // Accumulate tokens
562
+ todayEntry.inputTokens += inputTokens;
563
+ todayEntry.outputTokens += outputTokens;
564
+ todayEntry.estimatedCost = this.calculateEstimatedCost(todayEntry.inputTokens, todayEntry.outputTokens);
565
+ // Only increment session count for unique sessions
566
+ if (sessionId && !this.dailySessionIds.has(sessionId)) {
567
+ this.dailySessionIds.add(sessionId);
568
+ todayEntry.sessions = this.dailySessionIds.size;
569
+ }
570
+ // Prune old entries (keep last 30 days)
571
+ if (stats.daily.length > StateStore.MAX_DAILY_HISTORY) {
572
+ stats.daily = stats.daily.slice(0, StateStore.MAX_DAILY_HISTORY);
573
+ }
574
+ stats.lastUpdated = Date.now();
575
+ this.save();
576
+ }
577
+ /**
578
+ * Get daily stats for display.
579
+ * @param days Number of days to return (default: 30)
580
+ * @returns Array of daily entries, most recent first
581
+ */
582
+ getDailyStats(days = 30) {
583
+ const stats = this.getTokenStats();
584
+ return stats.daily.slice(0, days);
585
+ }
586
+ // ========== Inner State Methods (Ralph Loop tracking) ==========
587
+ loadRalphStates() {
588
+ try {
589
+ if (existsSync(this.ralphStatePath)) {
590
+ const data = readFileSync(this.ralphStatePath, 'utf-8');
591
+ const parsed = JSON.parse(data);
592
+ for (const [sessionId, state] of Object.entries(parsed)) {
593
+ this.ralphStates.set(sessionId, state);
594
+ }
595
+ }
596
+ }
597
+ catch (err) {
598
+ console.error('Failed to load inner states:', err);
599
+ }
600
+ }
601
+ // Debounced save for inner states
602
+ saveRalphStates() {
603
+ this.ralphStateDirty = true;
604
+ if (this.ralphStateSaveTimeout) {
605
+ return; // Already scheduled
606
+ }
607
+ this.ralphStateSaveTimeout = setTimeout(() => {
608
+ this.saveRalphStatesNow();
609
+ }, SAVE_DEBOUNCE_MS);
610
+ }
611
+ /**
612
+ * Immediate save for inner states using atomic write pattern.
613
+ * Writes to temp file first, then renames to prevent corruption on crash.
614
+ */
615
+ saveRalphStatesNow() {
616
+ if (this.ralphStateSaveTimeout) {
617
+ clearTimeout(this.ralphStateSaveTimeout);
618
+ this.ralphStateSaveTimeout = null;
619
+ }
620
+ if (!this.ralphStateDirty) {
621
+ return;
622
+ }
623
+ // Clear dirty flag only on success to enable retry on failure
624
+ this.ensureDir();
625
+ const data = Object.fromEntries(this.ralphStates);
626
+ // Atomic write: write to temp file, then rename (atomic on POSIX)
627
+ const tempPath = this.ralphStatePath + '.tmp';
628
+ let json;
629
+ try {
630
+ json = JSON.stringify(data);
631
+ }
632
+ catch (err) {
633
+ console.error('[StateStore] Failed to serialize Ralph state (circular reference or invalid data):', err);
634
+ // Keep dirty flag true for retry - don't throw, let caller continue
635
+ return;
636
+ }
637
+ try {
638
+ writeFileSync(tempPath, json, 'utf-8');
639
+ renameSync(tempPath, this.ralphStatePath);
640
+ // Success - clear dirty flag
641
+ this.ralphStateDirty = false;
642
+ }
643
+ catch (err) {
644
+ console.error('[StateStore] Failed to write Ralph state file:', err);
645
+ // Keep dirty flag true for retry on next save
646
+ // Try to clean up temp file on error
647
+ try {
648
+ if (existsSync(tempPath)) {
649
+ unlinkSync(tempPath);
650
+ }
651
+ }
652
+ catch (cleanupErr) {
653
+ console.warn('[StateStore] Failed to cleanup temp file during Ralph state save error:', cleanupErr);
654
+ }
655
+ // Don't throw - let caller continue, retry on next save
656
+ }
657
+ }
658
+ /** Returns inner state for a session, or null if not found. */
659
+ getRalphState(sessionId) {
660
+ return this.ralphStates.get(sessionId) ?? null;
661
+ }
662
+ /** Sets inner state for a session and triggers a debounced save. */
663
+ setRalphState(sessionId, state) {
664
+ this.ralphStates.set(sessionId, state);
665
+ this.saveRalphStates();
666
+ }
667
+ /**
668
+ * Updates inner state for a session (partial merge).
669
+ * Creates initial state if none exists.
670
+ * @returns The updated inner state.
671
+ */
672
+ updateRalphState(sessionId, updates) {
673
+ let state = this.ralphStates.get(sessionId);
674
+ if (!state) {
675
+ state = createInitialRalphSessionState(sessionId);
676
+ }
677
+ state = { ...state, ...updates, lastUpdated: Date.now() };
678
+ this.ralphStates.set(sessionId, state);
679
+ this.saveRalphStates();
680
+ return state;
681
+ }
682
+ /** Removes inner state for a session and triggers a debounced save. */
683
+ removeRalphState(sessionId) {
684
+ if (this.ralphStates.has(sessionId)) {
685
+ this.ralphStates.delete(sessionId);
686
+ this.saveRalphStates();
687
+ }
688
+ }
689
+ /** Returns a copy of all inner states as a Map. */
690
+ getAllRalphStates() {
691
+ return new Map(this.ralphStates);
692
+ }
693
+ /** Flushes all pending saves (main and inner state). Call before shutdown. */
694
+ flushAll() {
695
+ // Save both states, catching errors to ensure both are attempted
696
+ let mainError = null;
697
+ let ralphError = null;
698
+ try {
699
+ this.saveNow();
700
+ }
701
+ catch (err) {
702
+ mainError = err;
703
+ console.error('[StateStore] Error flushing main state:', err);
704
+ }
705
+ try {
706
+ this.saveRalphStatesNow();
707
+ }
708
+ catch (err) {
709
+ ralphError = err;
710
+ console.error('[StateStore] Error flushing Ralph state:', err);
711
+ }
712
+ // Log summary if any errors occurred
713
+ if (mainError || ralphError) {
714
+ console.warn('[StateStore] flushAll completed with errors - some state may not be persisted');
715
+ }
716
+ }
717
+ }
718
+ // Singleton instance
719
+ let storeInstance = null;
720
+ /**
721
+ * Gets or creates the singleton StateStore instance.
722
+ * @param filePath Optional custom file path (only used on first call).
723
+ */
724
+ export function getStore(filePath) {
725
+ if (!storeInstance) {
726
+ storeInstance = new StateStore(filePath);
727
+ }
728
+ return storeInstance;
729
+ }
730
+ //# sourceMappingURL=state-store.js.map