mstro-app 0.4.51 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +10 -5
  2. package/bin/mstro.js +1 -1
  3. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
  4. package/dist/server/cli/headless/claude-invoker-stall.js +7 -2
  5. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +1 -1
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  9. package/dist/server/cli/headless/runner.js +63 -67
  10. package/dist/server/cli/headless/runner.js.map +1 -1
  11. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  12. package/dist/server/cli/headless/stall-assessor.js +9 -4
  13. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  14. package/dist/server/cli/improvisation-history-store.d.ts +16 -0
  15. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
  16. package/dist/server/cli/improvisation-history-store.js +52 -0
  17. package/dist/server/cli/improvisation-history-store.js.map +1 -0
  18. package/dist/server/cli/improvisation-movements.d.ts +31 -0
  19. package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
  20. package/dist/server/cli/improvisation-movements.js +93 -0
  21. package/dist/server/cli/improvisation-movements.js.map +1 -0
  22. package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
  23. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
  24. package/dist/server/cli/improvisation-output-queue.js +40 -0
  25. package/dist/server/cli/improvisation-output-queue.js.map +1 -0
  26. package/dist/server/cli/improvisation-retry.d.ts +21 -51
  27. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  28. package/dist/server/cli/improvisation-retry.js +18 -433
  29. package/dist/server/cli/improvisation-retry.js.map +1 -1
  30. package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
  31. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  32. package/dist/server/cli/improvisation-session-manager.js +53 -148
  33. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  34. package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
  35. package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
  36. package/dist/server/cli/retry/retry-best-result.js +61 -0
  37. package/dist/server/cli/retry/retry-best-result.js.map +1 -0
  38. package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
  39. package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
  40. package/dist/server/cli/retry/retry-context-loss.js +68 -0
  41. package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
  42. package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
  43. package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
  44. package/dist/server/cli/retry/retry-premature-completion.js +81 -0
  45. package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
  46. package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
  47. package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
  48. package/dist/server/cli/retry/retry-recovery-strategies.js +166 -0
  49. package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
  50. package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
  51. package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
  52. package/dist/server/cli/retry/retry-resume-strategy.js +22 -0
  53. package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
  54. package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
  55. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
  56. package/dist/server/cli/retry/retry-runner-factory.js +60 -0
  57. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
  58. package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
  59. package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
  60. package/dist/server/cli/retry/retry-tool-results.js +24 -0
  61. package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
  62. package/dist/server/cli/retry/retry-types.d.ts +30 -0
  63. package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
  64. package/dist/server/cli/retry/retry-types.js +4 -0
  65. package/dist/server/cli/retry/retry-types.js.map +1 -0
  66. package/dist/server/index.js +21 -109
  67. package/dist/server/index.js.map +1 -1
  68. package/dist/server/server-setup.d.ts +16 -1
  69. package/dist/server/server-setup.d.ts.map +1 -1
  70. package/dist/server/server-setup.js +107 -0
  71. package/dist/server/server-setup.js.map +1 -1
  72. package/dist/server/services/plan/board-config.d.ts +21 -0
  73. package/dist/server/services/plan/board-config.d.ts.map +1 -0
  74. package/dist/server/services/plan/board-config.js +112 -0
  75. package/dist/server/services/plan/board-config.js.map +1 -0
  76. package/dist/server/services/plan/composer.d.ts +1 -1
  77. package/dist/server/services/plan/composer.d.ts.map +1 -1
  78. package/dist/server/services/plan/composer.js +7 -5
  79. package/dist/server/services/plan/composer.js.map +1 -1
  80. package/dist/server/services/plan/executor.d.ts +48 -48
  81. package/dist/server/services/plan/executor.d.ts.map +1 -1
  82. package/dist/server/services/plan/executor.js +157 -455
  83. package/dist/server/services/plan/executor.js.map +1 -1
  84. package/dist/server/services/plan/issue-loader.d.ts +16 -0
  85. package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
  86. package/dist/server/services/plan/issue-loader.js +46 -0
  87. package/dist/server/services/plan/issue-loader.js.map +1 -0
  88. package/dist/server/services/plan/issue-writer.d.ts +34 -0
  89. package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
  90. package/dist/server/services/plan/issue-writer.js +110 -0
  91. package/dist/server/services/plan/issue-writer.js.map +1 -0
  92. package/dist/server/services/plan/output-manager.d.ts.map +1 -1
  93. package/dist/server/services/plan/output-manager.js +2 -1
  94. package/dist/server/services/plan/output-manager.js.map +1 -1
  95. package/dist/server/services/plan/progress-log.d.ts +11 -0
  96. package/dist/server/services/plan/progress-log.d.ts.map +1 -0
  97. package/dist/server/services/plan/progress-log.js +81 -0
  98. package/dist/server/services/plan/progress-log.js.map +1 -0
  99. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  100. package/dist/server/services/plan/prompt-builder.js +48 -31
  101. package/dist/server/services/plan/prompt-builder.js.map +1 -1
  102. package/dist/server/services/plan/readiness-planner.d.ts +15 -0
  103. package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
  104. package/dist/server/services/plan/readiness-planner.js +41 -0
  105. package/dist/server/services/plan/readiness-planner.js.map +1 -0
  106. package/dist/server/services/plan/review-gate.d.ts +31 -0
  107. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  108. package/dist/server/services/plan/review-gate.js +52 -2
  109. package/dist/server/services/plan/review-gate.js.map +1 -1
  110. package/dist/server/services/platform.d.ts +56 -0
  111. package/dist/server/services/platform.d.ts.map +1 -1
  112. package/dist/server/services/platform.js +154 -52
  113. package/dist/server/services/platform.js.map +1 -1
  114. package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
  115. package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
  116. package/dist/server/services/websocket/file-download-handler.js +165 -0
  117. package/dist/server/services/websocket/file-download-handler.js.map +1 -0
  118. package/dist/server/services/websocket/git-branch-handlers.d.ts +1 -1
  119. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  120. package/dist/server/services/websocket/git-branch-handlers.js +21 -1
  121. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  122. package/dist/server/services/websocket/git-handlers.js +1 -1
  123. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  124. package/dist/server/services/websocket/git-worktree-handlers.d.ts +2 -0
  125. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  126. package/dist/server/services/websocket/git-worktree-handlers.js +30 -4
  127. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  128. package/dist/server/services/websocket/handler-context.d.ts +15 -0
  129. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  130. package/dist/server/services/websocket/handler.d.ts +7 -0
  131. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  132. package/dist/server/services/websocket/handler.js +73 -11
  133. package/dist/server/services/websocket/handler.js.map +1 -1
  134. package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
  135. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
  136. package/dist/server/services/websocket/msg-id-tracker.js +77 -0
  137. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
  138. package/dist/server/services/websocket/quality-handlers.js +15 -3
  139. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  140. package/dist/server/services/websocket/quality-review-agent.js +2 -2
  141. package/dist/server/services/websocket/session-handlers.d.ts +48 -2
  142. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  143. package/dist/server/services/websocket/session-handlers.js +204 -65
  144. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  145. package/dist/server/services/websocket/session-initialization.d.ts +2 -2
  146. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  147. package/dist/server/services/websocket/session-initialization.js +75 -17
  148. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  149. package/dist/server/services/websocket/session-registry.d.ts +29 -1
  150. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  151. package/dist/server/services/websocket/session-registry.js +53 -4
  152. package/dist/server/services/websocket/session-registry.js.map +1 -1
  153. package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
  154. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
  155. package/dist/server/services/websocket/tab-broadcast.js +13 -0
  156. package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
  157. package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
  158. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
  159. package/dist/server/services/websocket/tab-event-buffer.js +107 -0
  160. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
  161. package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
  162. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
  163. package/dist/server/services/websocket/tab-event-replay.js +21 -0
  164. package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
  165. package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
  166. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  167. package/dist/server/services/websocket/tab-handlers.js +2 -9
  168. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  169. package/dist/server/services/websocket/types.d.ts +15 -6
  170. package/dist/server/services/websocket/types.d.ts.map +1 -1
  171. package/dist/server/services/websocket/types.js +6 -4
  172. package/dist/server/services/websocket/types.js.map +1 -1
  173. package/package.json +1 -1
  174. package/server/README.md +1 -1
  175. package/server/cli/headless/claude-invoker-stall.ts +7 -2
  176. package/server/cli/headless/claude-invoker.ts +1 -1
  177. package/server/cli/headless/runner.ts +67 -72
  178. package/server/cli/headless/stall-assessor.ts +9 -4
  179. package/server/cli/headless/types.ts +1 -1
  180. package/server/cli/improvisation-history-store.ts +62 -0
  181. package/server/cli/improvisation-movements.ts +120 -0
  182. package/server/cli/improvisation-output-queue.ts +42 -0
  183. package/server/cli/improvisation-retry.ts +25 -600
  184. package/server/cli/improvisation-session-manager.ts +74 -160
  185. package/server/cli/retry/retry-best-result.ts +70 -0
  186. package/server/cli/retry/retry-context-loss.ts +87 -0
  187. package/server/cli/retry/retry-premature-completion.ts +113 -0
  188. package/server/cli/retry/retry-recovery-strategies.ts +247 -0
  189. package/server/cli/retry/retry-resume-strategy.ts +33 -0
  190. package/server/cli/retry/retry-runner-factory.ts +70 -0
  191. package/server/cli/retry/retry-tool-results.ts +31 -0
  192. package/server/cli/retry/retry-types.ts +32 -0
  193. package/server/index.ts +37 -123
  194. package/server/server-setup.ts +126 -1
  195. package/server/services/plan/agents/assess-stall.md +11 -4
  196. package/server/services/plan/board-config.ts +122 -0
  197. package/server/services/plan/composer.ts +7 -5
  198. package/server/services/plan/executor.ts +214 -467
  199. package/server/services/plan/issue-loader.ts +64 -0
  200. package/server/services/plan/issue-writer.ts +137 -0
  201. package/server/services/plan/output-manager.ts +2 -1
  202. package/server/services/plan/progress-log.ts +92 -0
  203. package/server/services/plan/prompt-builder.ts +73 -35
  204. package/server/services/plan/readiness-planner.ts +50 -0
  205. package/server/services/plan/review-gate.ts +102 -2
  206. package/server/services/platform.ts +163 -58
  207. package/server/services/websocket/file-download-handler.ts +191 -0
  208. package/server/services/websocket/git-branch-handlers.ts +28 -1
  209. package/server/services/websocket/git-handlers.ts +1 -1
  210. package/server/services/websocket/git-worktree-handlers.ts +31 -4
  211. package/server/services/websocket/handler-context.ts +15 -0
  212. package/server/services/websocket/handler.ts +76 -12
  213. package/server/services/websocket/msg-id-tracker.ts +84 -0
  214. package/server/services/websocket/quality-handlers.ts +16 -3
  215. package/server/services/websocket/quality-review-agent.ts +2 -2
  216. package/server/services/websocket/session-handlers.ts +213 -68
  217. package/server/services/websocket/session-initialization.ts +83 -19
  218. package/server/services/websocket/session-registry.ts +61 -4
  219. package/server/services/websocket/tab-broadcast.ts +38 -0
  220. package/server/services/websocket/tab-event-buffer.ts +159 -0
  221. package/server/services/websocket/tab-event-replay.ts +42 -0
  222. package/server/services/websocket/tab-handlers.ts +2 -9
  223. package/server/services/websocket/types.ts +17 -4
@@ -0,0 +1,247 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Recovery strategies: context-loss (inter-movement / native-timeout),
6
+ * tool-timeout checkpoint retry, and signal-crash retry. Each `shouldRetry*`
7
+ * or `apply*Retry` function returns true when the retry loop should loop.
8
+ */
9
+
10
+ import { AnalyticsEvents, trackEvent } from '../../services/analytics.js';
11
+ import type { ExecutionCheckpoint } from '../headless/types.js';
12
+ import type { HeadlessRunResult, MovementRecord, RetryLoopState } from '../improvisation-types.js';
13
+ import {
14
+ buildContextRecoveryPrompt,
15
+ buildFreshRecoveryPrompt,
16
+ buildInterMovementRecoveryPrompt,
17
+ buildResumeRetryPrompt,
18
+ buildRetryPrompt,
19
+ buildSignalCrashRecoveryPrompt,
20
+ extractHistoricalToolResults,
21
+ } from '../prompt-builders.js';
22
+ import { accumulateToolResults } from './retry-tool-results.js';
23
+ import type { RetryCallbacks, RetrySessionState } from './retry-types.js';
24
+
25
+ // ── Context-loss recovery ────────────────────────────────────
26
+
27
+ /** Handle inter-movement context loss recovery (resume session expired) */
28
+ export function applyInterMovementRecovery(
29
+ state: RetryLoopState,
30
+ promptWithAttachments: string,
31
+ history: MovementRecord[],
32
+ callbacks: RetryCallbacks,
33
+ ): void {
34
+ const historicalResults = extractHistoricalToolResults(history);
35
+ const allResults = [...historicalResults, ...state.accumulatedToolResults];
36
+
37
+ callbacks.emit('onAutoRetry', {
38
+ retryNumber: state.retryNumber,
39
+ maxRetries: 3,
40
+ toolName: 'InterMovementRecovery',
41
+ completedCount: allResults.length,
42
+ });
43
+ callbacks.queueOutput(
44
+ `\n[[MSTRO_CONTEXT_RECOVERY]] Session context expired — continuing with ${allResults.length} preserved results from prior work (retry ${state.retryNumber}/3).\n`
45
+ );
46
+ callbacks.flushOutputQueue();
47
+
48
+ state.freshRecoveryMode = true;
49
+ state.currentPrompt = buildInterMovementRecoveryPrompt(promptWithAttachments, allResults, history);
50
+ }
51
+
52
+ /** Handle native-timeout context loss recovery (tool timeouts caused confusion) */
53
+ export function applyNativeTimeoutRecovery(
54
+ result: HeadlessRunResult,
55
+ state: RetryLoopState,
56
+ promptWithAttachments: string,
57
+ session: RetrySessionState,
58
+ callbacks: RetryCallbacks,
59
+ ): void {
60
+ const completedCount = state.accumulatedToolResults.length;
61
+
62
+ callbacks.emit('onAutoRetry', {
63
+ retryNumber: state.retryNumber,
64
+ maxRetries: 3,
65
+ toolName: 'ContextRecovery',
66
+ completedCount,
67
+ });
68
+
69
+ if (result.claudeSessionId && state.retryNumber === 1) {
70
+ callbacks.queueOutput(
71
+ `\n[[MSTRO_CONTEXT_RECOVERY]] Context loss detected — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/3).\n`
72
+ );
73
+ callbacks.flushOutputQueue();
74
+ state.contextRecoverySessionId = result.claudeSessionId;
75
+ session.claudeSessionId = result.claudeSessionId;
76
+ state.currentPrompt = buildContextRecoveryPrompt(promptWithAttachments);
77
+ } else {
78
+ callbacks.queueOutput(
79
+ `\n[[MSTRO_CONTEXT_RECOVERY]] Continuing with fresh context — ${completedCount} preserved results injected (retry ${state.retryNumber}/3).\n`
80
+ );
81
+ callbacks.flushOutputQueue();
82
+ state.freshRecoveryMode = true;
83
+ state.currentPrompt = buildFreshRecoveryPrompt(promptWithAttachments, state.accumulatedToolResults, state.timedOutTools);
84
+ }
85
+ }
86
+
87
+ /** Check if context loss recovery should trigger. Returns true if loop should continue. */
88
+ export function shouldRetryContextLoss(
89
+ result: HeadlessRunResult,
90
+ state: RetryLoopState,
91
+ session: RetrySessionState,
92
+ useResume: boolean,
93
+ nativeTimeouts: number,
94
+ maxRetries: number,
95
+ promptWithAttachments: string,
96
+ callbacks: RetryCallbacks,
97
+ ): boolean {
98
+ if (state.checkpointRef.value || state.retryNumber >= maxRetries || !state.contextLost) {
99
+ return false;
100
+ }
101
+ accumulateToolResults(result, state);
102
+ state.retryNumber++;
103
+ const path = (useResume && nativeTimeouts === 0) ? 'InterMovementRecovery' : 'NativeTimeoutRecovery';
104
+ state.retryLog.push({
105
+ retryNumber: state.retryNumber,
106
+ path,
107
+ reason: `Context lost (${nativeTimeouts} timeouts, ${state.accumulatedToolResults.length} tools preserved)`,
108
+ timestamp: Date.now(),
109
+ });
110
+ if (useResume && nativeTimeouts === 0) {
111
+ applyInterMovementRecovery(state, promptWithAttachments, session.history.movements, callbacks);
112
+ } else {
113
+ applyNativeTimeoutRecovery(result, state, promptWithAttachments, session, callbacks);
114
+ }
115
+ return true;
116
+ }
117
+
118
+ // ── Tool-timeout retry ───────────────────────────────────────
119
+
120
+ /** Handle tool timeout checkpoint. Returns true if loop should continue. */
121
+ export function applyToolTimeoutRetry(
122
+ state: RetryLoopState,
123
+ maxRetries: number,
124
+ promptWithAttachments: string,
125
+ callbacks: RetryCallbacks,
126
+ model: string | undefined,
127
+ effortLevel: string | undefined,
128
+ ): boolean {
129
+ if (!state.checkpointRef.value || state.retryNumber >= maxRetries) {
130
+ return false;
131
+ }
132
+
133
+ const cp: ExecutionCheckpoint = state.checkpointRef.value;
134
+ state.retryNumber++;
135
+
136
+ state.timedOutTools.push({
137
+ toolName: cp.hungTool.toolName,
138
+ input: cp.hungTool.input ?? {},
139
+ timeoutMs: cp.hungTool.timeoutMs,
140
+ });
141
+
142
+ const canResumeSession = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
143
+ state.retryLog.push({
144
+ retryNumber: state.retryNumber,
145
+ path: 'ToolTimeout',
146
+ reason: `${cp.hungTool.toolName} timed out after ${cp.hungTool.timeoutMs}ms, ${cp.completedTools.length} tools completed, ${canResumeSession ? 'resuming' : 'fresh start'}`,
147
+ timestamp: Date.now(),
148
+ });
149
+ callbacks.emit('onAutoRetry', {
150
+ retryNumber: state.retryNumber,
151
+ maxRetries,
152
+ toolName: cp.hungTool.toolName,
153
+ url: cp.hungTool.url,
154
+ completedCount: cp.completedTools.length,
155
+ });
156
+
157
+ trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
158
+ retry_number: state.retryNumber,
159
+ hung_tool: cp.hungTool.toolName,
160
+ hung_url: cp.hungTool.url?.slice(0, 200),
161
+ completed_tools: cp.completedTools.length,
162
+ elapsed_ms: cp.elapsedMs,
163
+ resume_attempted: canResumeSession,
164
+ model: model || 'default',
165
+ effort_level: effortLevel || 'auto',
166
+ });
167
+
168
+ state.currentPrompt = canResumeSession
169
+ ? buildResumeRetryPrompt(cp, state.timedOutTools)
170
+ : buildRetryPrompt(cp, promptWithAttachments, state.timedOutTools);
171
+
172
+ callbacks.queueOutput(
173
+ `\n[[MSTRO_AUTO_RETRY]] Auto-retry ${state.retryNumber}/${maxRetries}: ${canResumeSession ? 'Resuming session' : 'Continuing'} with ${cp.completedTools.length} successful results, skipping failed ${cp.hungTool.toolName}.\n`
174
+ );
175
+ callbacks.flushOutputQueue();
176
+
177
+ return true;
178
+ }
179
+
180
+ // ── Signal-crash retry ───────────────────────────────────────
181
+
182
+ /** Detect and retry after a signal crash. Returns true if loop should continue. */
183
+ export function shouldRetrySignalCrash(
184
+ result: HeadlessRunResult,
185
+ state: RetryLoopState,
186
+ session: RetrySessionState,
187
+ maxRetries: number,
188
+ promptWithAttachments: string,
189
+ callbacks: RetryCallbacks,
190
+ ): boolean {
191
+ const isSignalCrash = !!result.signalName;
192
+ const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
193
+ if ((!isSignalCrash && !exitCodeSignal) || state.retryNumber >= maxRetries) {
194
+ return false;
195
+ }
196
+ if (state.checkpointRef.value) {
197
+ return false;
198
+ }
199
+
200
+ accumulateToolResults(result, state);
201
+ state.retryNumber++;
202
+
203
+ const completedCount = state.accumulatedToolResults.length;
204
+ const signalInfo = result.signalName || 'unknown signal';
205
+ const useResume = !!result.claudeSessionId && state.retryNumber === 1;
206
+
207
+ state.retryLog.push({
208
+ retryNumber: state.retryNumber,
209
+ path: 'SignalCrash',
210
+ reason: `Process killed (${signalInfo}), ${completedCount} tools preserved, ${useResume ? 'resuming' : 'fresh start'}`,
211
+ timestamp: Date.now(),
212
+ });
213
+
214
+ callbacks.emit('onAutoRetry', {
215
+ retryNumber: state.retryNumber,
216
+ maxRetries,
217
+ toolName: `SignalCrash(${signalInfo})`,
218
+ completedCount,
219
+ });
220
+
221
+ trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
222
+ retry_number: state.retryNumber,
223
+ hung_tool: `signal_crash:${signalInfo}`,
224
+ completed_tools: completedCount,
225
+ resume_attempted: useResume,
226
+ });
227
+
228
+ if (useResume) {
229
+ callbacks.queueOutput(
230
+ `\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
231
+ );
232
+ callbacks.flushOutputQueue();
233
+ state.contextRecoverySessionId = result.claudeSessionId;
234
+ session.claudeSessionId = result.claudeSessionId;
235
+ state.currentPrompt = buildSignalCrashRecoveryPrompt(promptWithAttachments, true);
236
+ } else {
237
+ callbacks.queueOutput(
238
+ `\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — restarting with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
239
+ );
240
+ callbacks.flushOutputQueue();
241
+ state.freshRecoveryMode = true;
242
+ const allResults = [...extractHistoricalToolResults(session.history.movements), ...state.accumulatedToolResults];
243
+ state.currentPrompt = buildSignalCrashRecoveryPrompt(promptWithAttachments, false, allResults);
244
+ }
245
+
246
+ return true;
247
+ }
@@ -0,0 +1,33 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Picks how the next retry iteration should resume execution:
6
+ * whether to pass `--resume`, and which Claude session ID (if any) to use.
7
+ */
8
+
9
+ import type { RetryLoopState } from '../improvisation-types.js';
10
+ import type { RetrySessionState } from './retry-types.js';
11
+
12
+ /** Determine whether to use --resume and which session ID */
13
+ export function determineResumeStrategy(
14
+ state: RetryLoopState,
15
+ session: RetrySessionState,
16
+ ): { useResume: boolean; resumeSessionId: string | undefined } {
17
+ if (state.freshRecoveryMode) {
18
+ state.freshRecoveryMode = false;
19
+ return { useResume: false, resumeSessionId: undefined };
20
+ }
21
+ if (state.contextRecoverySessionId) {
22
+ const id = state.contextRecoverySessionId;
23
+ state.contextRecoverySessionId = undefined;
24
+ return { useResume: true, resumeSessionId: id };
25
+ }
26
+ if (state.retryNumber === 0) {
27
+ return { useResume: !session.isFirstPrompt, resumeSessionId: session.claudeSessionId };
28
+ }
29
+ if (state.lastWatchdogCheckpoint?.inProgressTools.length === 0 && state.lastWatchdogCheckpoint.claudeSessionId) {
30
+ return { useResume: true, resumeSessionId: state.lastWatchdogCheckpoint.claudeSessionId };
31
+ }
32
+ return { useResume: false, resumeSessionId: undefined };
33
+ }
@@ -0,0 +1,70 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Factory for the HeadlessRunner used in a single retry iteration.
6
+ * Wires session options into HeadlessRunner callbacks that respect the
7
+ * shared cancel flag and forward streaming events via `callbacks`.
8
+ */
9
+
10
+ import { HeadlessRunner } from '../headless/index.js';
11
+ import type { ExecutionCheckpoint } from '../headless/types.js';
12
+ import type { FileAttachment, RetryLoopState } from '../improvisation-types.js';
13
+ import { buildHistoricalContext } from '../prompt-builders.js';
14
+ import type { RetryCallbacks, RetrySessionState } from './retry-types.js';
15
+
16
+ /** Create HeadlessRunner for one retry iteration */
17
+ export function createExecutionRunner(
18
+ state: RetryLoopState,
19
+ session: RetrySessionState,
20
+ callbacks: RetryCallbacks,
21
+ sequenceNumber: number,
22
+ useResume: boolean,
23
+ resumeSessionId: string | undefined,
24
+ imageAttachments: FileAttachment[] | undefined,
25
+ workingDirOverride?: string,
26
+ ): HeadlessRunner {
27
+ return new HeadlessRunner({
28
+ workingDir: workingDirOverride || session.options.workingDir,
29
+ tokenBudgetThreshold: session.options.tokenBudgetThreshold,
30
+ maxSessions: session.options.maxSessions,
31
+ verbose: session.options.verbose,
32
+ noColor: session.options.noColor,
33
+ model: session.options.model,
34
+ effortLevel: session.options.effortLevel,
35
+ improvisationMode: true,
36
+ movementNumber: sequenceNumber,
37
+ continueSession: useResume,
38
+ claudeSessionId: resumeSessionId,
39
+ outputCallback: (text: string) => {
40
+ if (callbacks.isCancelled()) return;
41
+ callbacks.addEventLog({ type: 'output', data: { text, timestamp: Date.now() }, timestamp: Date.now() });
42
+ callbacks.queueOutput(text);
43
+ callbacks.flushOutputQueue();
44
+ },
45
+ thinkingCallback: (text: string) => {
46
+ if (callbacks.isCancelled()) return;
47
+ callbacks.addEventLog({ type: 'thinking', data: { text }, timestamp: Date.now() });
48
+ callbacks.emit('onThinking', text);
49
+ callbacks.flushOutputQueue();
50
+ },
51
+ toolUseCallback: (event) => {
52
+ if (callbacks.isCancelled()) return;
53
+ callbacks.addEventLog({ type: 'toolUse', data: { ...event, timestamp: Date.now() }, timestamp: Date.now() });
54
+ callbacks.emit('onToolUse', event);
55
+ callbacks.flushOutputQueue();
56
+ },
57
+ tokenUsageCallback: (usage) => {
58
+ if (callbacks.isCancelled()) return;
59
+ callbacks.emit('onTokenUsage', usage);
60
+ },
61
+ directPrompt: state.currentPrompt,
62
+ imageAttachments,
63
+ promptContext: (state.retryNumber === 0 && session.isResumedSession && session.isFirstPrompt)
64
+ ? { accumulatedKnowledge: buildHistoricalContext(session.history.movements), filesModified: [] }
65
+ : undefined,
66
+ onToolTimeout: (checkpoint: ExecutionCheckpoint) => {
67
+ state.checkpointRef.value = checkpoint;
68
+ },
69
+ });
70
+ }
@@ -0,0 +1,31 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Running buffer of tool results preserved across retries so recovery
6
+ * prompts can re-inject completed work instead of redoing it.
7
+ */
8
+
9
+ import type { HeadlessRunResult, RetryLoopState } from '../improvisation-types.js';
10
+
11
+ export const MAX_ACCUMULATED_RESULTS = 50;
12
+
13
+ /** Accumulate completed tool results from a run into the retry state */
14
+ export function accumulateToolResults(result: HeadlessRunResult, state: RetryLoopState): void {
15
+ if (!result.toolUseHistory) return;
16
+ for (const t of result.toolUseHistory) {
17
+ if (t.result !== undefined) {
18
+ state.accumulatedToolResults.push({
19
+ toolName: t.toolName,
20
+ toolId: t.toolId,
21
+ toolInput: t.toolInput,
22
+ result: t.result,
23
+ isError: t.isError,
24
+ duration: t.duration,
25
+ });
26
+ }
27
+ }
28
+ if (state.accumulatedToolResults.length > MAX_ACCUMULATED_RESULTS) {
29
+ state.accumulatedToolResults = state.accumulatedToolResults.slice(-MAX_ACCUMULATED_RESULTS);
30
+ }
31
+ }
@@ -0,0 +1,32 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Shared types used across the retry modules. Defined separately so each
6
+ * strategy file can import them without creating circular dependencies.
7
+ */
8
+
9
+ import type { HeadlessRunner } from '../headless/index.js';
10
+ import type { ImprovisationOptions, SessionHistory } from '../improvisation-types.js';
11
+
12
+ /** Callbacks the retry logic needs from the session manager */
13
+ export interface RetryCallbacks {
14
+ isCancelled: () => boolean;
15
+ queueOutput: (text: string) => void;
16
+ flushOutputQueue: () => void;
17
+ emit: (event: string, ...args: unknown[]) => void;
18
+ addEventLog: (entry: { type: string; data: unknown; timestamp: number }) => void;
19
+ setRunner: (runner: HeadlessRunner | null) => void;
20
+ }
21
+
22
+ /** Session state the retry logic reads/writes */
23
+ export interface RetrySessionState {
24
+ options: ImprovisationOptions;
25
+ claudeSessionId: string | undefined;
26
+ isFirstPrompt: boolean;
27
+ isResumedSession: boolean;
28
+ history: SessionHistory;
29
+ executionStartTimestamp: number | undefined;
30
+ }
31
+
32
+ export const MAX_RETRIES = 3;
package/server/index.ts CHANGED
@@ -9,14 +9,14 @@
9
9
 
10
10
  import { randomBytes } from 'node:crypto'
11
11
  import { readFileSync } from 'node:fs'
12
- import type { IncomingMessage, Server } from 'node:http'
12
+ import type { Server } from 'node:http'
13
13
  import { homedir } from 'node:os'
14
14
  import { basename, join } from 'node:path'
15
15
  import { serve } from '@hono/node-server'
16
16
  import { type Context, Hono, type Next } from 'hono'
17
17
  import { cors } from 'hono/cors'
18
18
  import { logger } from 'hono/logger'
19
- import { type WebSocket as NodeWebSocket, WebSocketServer } from 'ws'
19
+ import { WebSocketServer } from 'ws'
20
20
  import {
21
21
  createFileRoutes,
22
22
  createImproviseRoutes,
@@ -24,16 +24,20 @@ import {
24
24
  createNotificationRoutes,
25
25
  createShutdownRoute
26
26
  } from './routes/index.js'
27
- import { createPlatformRelayContext, ensureClaudeSettings, setTerminalTitle, wrapWebSocket } from './server-setup.js'
27
+ import {
28
+ attachLocalWebSocketRouting,
29
+ createPlatformRelay,
30
+ ensureClaudeSettings,
31
+ registerProcessErrorHandlers,
32
+ setTerminalTitle
33
+ } from './server-setup.js'
28
34
  import { AnalyticsEvents, initAnalytics, shutdownAnalytics, trackEvent } from './services/analytics.js'
29
35
  import { AuthService } from './services/auth.js'
30
36
  import { FileService } from './services/files.js'
31
37
  import { InstanceRegistry, type MstroInstance } from './services/instances.js'
32
- import { PlatformConnection } from './services/platform.js'
33
38
  import { captureException, flushSentry, initSentry } from './services/sentry.js'
34
39
  import { getPTYManager, reloadPty } from './services/terminal/pty-manager.js'
35
40
  import { WebSocketImproviseHandler } from './services/websocket/index.js'
36
- import type { WSContext } from './services/websocket/types.js'
37
41
  import { findAvailablePort } from './utils/port.js'
38
42
 
39
43
  // Configuration
@@ -129,136 +133,46 @@ app.onError((err, c) => {
129
133
 
130
134
  // ── Server Startup ────────────────────────────────────────────
131
135
 
132
- async function startServer() {
133
- initSentry()
134
- await initAnalytics()
135
-
136
- const PORT = await findAvailablePort(REQUESTED_PORT, 20)
137
- _currentInstance = instanceRegistry.register(PORT, WORKING_DIR)
138
-
139
- const server = serve({ fetch: app.fetch, port: PORT })
140
- const wss = new WebSocketServer({ server: server as Server })
141
-
142
- wss.on('connection', (ws: NodeWebSocket, req: IncomingMessage) => {
143
- const url = new URL(req.url || '/', `http://localhost:${PORT}`)
144
- if (url.pathname !== '/ws') {
145
- ws.close(1008, 'Invalid WebSocket path')
146
- return
147
- }
148
-
149
- const wsToken = url.searchParams.get('token')
150
- if (!wsToken || !authService.validateLocalToken(wsToken)) {
151
- ws.close(4001, 'Unauthorized')
152
- return
153
- }
154
-
155
- const workingDir = WORKING_DIR
156
- const wrappedWs = wrapWebSocket(ws, workingDir)
157
- wsHandler.handleConnection(wrappedWs, workingDir)
158
-
159
- ws.on('message', (data: Buffer | string) => {
160
- let message = typeof data === 'string' ? data : data.toString('utf-8')
161
- // Strip _permission from local WebSocket messages — only the platform relay
162
- // should inject permission metadata. Local connections are always the machine owner.
163
- if (message.includes('_permission')) {
164
- try {
165
- const parsed = JSON.parse(message)
166
- if ('_permission' in parsed) {
167
- delete parsed._permission
168
- message = JSON.stringify(parsed)
169
- }
170
- } catch { /* not JSON — pass through */ }
171
- }
172
- wsHandler.handleMessage(wrappedWs, message, workingDir)
173
- })
174
- ws.on('close', () => wsHandler.handleClose(wrappedWs))
175
- ws.on('error', (error: Error) => {
176
- console.error('[WebSocket] Error:', error)
177
- captureException(error, { context: 'websocket.connection' })
178
- })
179
- })
180
-
136
+ function logStartupBanner(port: number): void {
181
137
  const home = homedir()
182
138
  const displayDir = WORKING_DIR.startsWith(home) ? `~${WORKING_DIR.slice(home.length)}` : WORKING_DIR
183
139
  console.log(`App: ${displayDir}`)
184
- trackEvent(AnalyticsEvents.SERVER_STARTED, { port: PORT, working_dir_basename: basename(WORKING_DIR) })
185
-
186
- // Platform relay
187
- let platformRelayContext: WSContext | null = null
188
- let pendingRelayMessages: unknown[] = []
189
-
190
- const platformConnection = new PlatformConnection(WORKING_DIR, {
191
- onConnected: () => {
192
- console.log(`Connected: https://mstro.app`)
193
- wsHandler.setUsageReporter((report) => {
194
- platformConnection.send({ type: 'reportUsage', data: report })
195
- })
196
- },
197
- onDisconnected: () => {
198
- if (platformRelayContext) {
199
- wsHandler.handleClose(platformRelayContext)
200
- platformRelayContext = null
201
- }
202
- pendingRelayMessages = []
203
- },
204
- onWebConnected: () => {
205
- // Clean up previous relay context to prevent duplicate broadcasts
206
- if (platformRelayContext) {
207
- wsHandler.handleClose(platformRelayContext)
208
- }
209
- platformRelayContext = createPlatformRelayContext(
210
- (message) => platformConnection.send(message),
211
- WORKING_DIR
212
- )
213
- wsHandler.handleConnection(platformRelayContext, WORKING_DIR)
214
- if (pendingRelayMessages.length > 0) {
215
- for (const message of pendingRelayMessages) {
216
- wsHandler.handleMessage(platformRelayContext, JSON.stringify(message), WORKING_DIR)
217
- }
218
- pendingRelayMessages = []
219
- }
220
- },
221
- onWebDisconnected: () => {
222
- if (platformRelayContext) {
223
- wsHandler.handleClose(platformRelayContext)
224
- platformRelayContext = null
225
- }
226
- pendingRelayMessages = []
227
- },
228
- onRelayedMessage: (message) => {
229
- if (platformRelayContext) {
230
- wsHandler.handleMessage(platformRelayContext, JSON.stringify(message), WORKING_DIR)
231
- } else {
232
- // Cap pending messages to prevent unbounded memory growth while disconnected
233
- if (pendingRelayMessages.length < 100) {
234
- pendingRelayMessages.push(message)
235
- }
236
- }
237
- }
238
- })
239
- platformConnection.connect()
240
-
241
- // Process-level error handling
242
- process.on('uncaughtException', (err) => {
243
- console.error('[Server] Uncaught exception:', err)
244
- captureException(err, { context: 'uncaughtException' })
245
- })
246
- process.on('unhandledRejection', (reason) => {
247
- console.error('[Server] Unhandled rejection:', reason)
248
- captureException(reason instanceof Error ? reason : new Error(String(reason)), { context: 'unhandledRejection' })
249
- })
140
+ trackEvent(AnalyticsEvents.SERVER_STARTED, { port, working_dir_basename: basename(WORKING_DIR) })
141
+ }
250
142
 
251
- const gracefulShutdown = async () => {
143
+ function makeGracefulShutdown(deps: {
144
+ platformConnection: { disconnect: () => void }
145
+ wss: WebSocketServer
146
+ }) {
147
+ return async () => {
252
148
  trackEvent(AnalyticsEvents.SERVER_STOPPED)
253
149
  await Promise.all([shutdownAnalytics(), flushSentry()])
254
- platformConnection.disconnect()
150
+ deps.platformConnection.disconnect()
255
151
  instanceRegistry.unregister()
256
152
  getPTYManager().closeAll()
257
- wss.close()
153
+ deps.wss.close()
258
154
  console.log('\n\n👋 Shutting down gracefully...\n')
259
155
  process.exit(0)
260
156
  }
157
+ }
158
+
159
+ async function startServer() {
160
+ initSentry()
161
+ await initAnalytics()
162
+
163
+ const port = await findAvailablePort(REQUESTED_PORT, 20)
164
+ _currentInstance = instanceRegistry.register(port, WORKING_DIR)
165
+
166
+ const server = serve({ fetch: app.fetch, port })
167
+ const wss = new WebSocketServer({ server: server as Server })
168
+
169
+ attachLocalWebSocketRouting({ wss, port, workingDir: WORKING_DIR, authService, wsHandler })
170
+ logStartupBanner(port)
171
+
172
+ const platformConnection = createPlatformRelay(WORKING_DIR, wsHandler)
173
+ registerProcessErrorHandlers()
261
174
 
175
+ const gracefulShutdown = makeGracefulShutdown({ platformConnection, wss })
262
176
  process.on('SIGINT', gracefulShutdown)
263
177
  process.on('SIGTERM', gracefulShutdown)
264
178
  }