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
@@ -13,8 +13,9 @@ import { type ClaudeInvokerOptions, executeClaudeCommand } from './claude-invoke
13
13
  import { estimateTokensFromOutput } from './output-utils.js';
14
14
  import { enrichPromptWithContext } from './prompt-utils.js';
15
15
  import type {
16
+ ExecutionResult,
16
17
  HeadlessConfig,
17
- PromptContext,
18
+ PromptContext,
18
19
  ResolvedHeadlessConfig,
19
20
  SessionResult,
20
21
  } from './types.js';
@@ -35,6 +36,64 @@ export function killProcessGroup(pid: number, signal: NodeJS.Signals): void {
35
36
  }
36
37
  }
37
38
 
39
+ /** Shared result fields carried over from an ExecutionResult into a SessionResult. */
40
+ function sharedResultFields(result: ExecutionResult) {
41
+ return {
42
+ signalName: result.signalName,
43
+ assistantResponse: result.assistantResponse,
44
+ thinkingOutput: result.thinkingOutput,
45
+ toolUseHistory: result.toolUseHistory,
46
+ claudeSessionId: result.claudeSessionId,
47
+ nativeTimeoutCount: result.nativeTimeoutCount,
48
+ postTimeoutOutput: result.postTimeoutOutput,
49
+ resumeBufferedOutput: result.resumeBufferedOutput,
50
+ stopReason: result.stopReason,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Signal exits (128+) with meaningful output are successful completions —
56
+ * Claude finished its work but the process was killed by signal (e.g. stall watchdog SIGTERM).
57
+ */
58
+ function isSignalExitWithOutput(result: ExecutionResult): boolean {
59
+ if (result.exitCode < 128) return false;
60
+ return !!(result.assistantResponse || (result.toolUseHistory && result.toolUseHistory.length > 0));
61
+ }
62
+
63
+ /** Build meaningful error message: prefer stderr, fall back to non-JSON stdout lines. */
64
+ function deriveErrorMessage(result: ExecutionResult): string {
65
+ if (result.error) return result.error;
66
+ if (result.output) {
67
+ const plainLines = result.output.split('\n')
68
+ .filter(l => l.trim() && !l.trim().startsWith('{'))
69
+ .join('\n')
70
+ .trim();
71
+ if (plainLines) return plainLines.slice(0, 500);
72
+ }
73
+ return `Claude exited with code ${result.exitCode}`;
74
+ }
75
+
76
+ function buildSuccessResult(sessionId: string, result: ExecutionResult): SessionResult {
77
+ return {
78
+ completed: true,
79
+ needsHandoff: false,
80
+ totalTokens: estimateTokensFromOutput(result.output),
81
+ sessionId,
82
+ ...sharedResultFields(result),
83
+ };
84
+ }
85
+
86
+ function buildErrorResult(sessionId: string, result: ExecutionResult): SessionResult {
87
+ return {
88
+ completed: false,
89
+ needsHandoff: false,
90
+ totalTokens: 0,
91
+ sessionId,
92
+ error: deriveErrorMessage(result),
93
+ ...sharedResultFields(result),
94
+ };
95
+ }
96
+
38
97
  export class HeadlessRunner {
39
98
  private config: ResolvedHeadlessConfig;
40
99
  private runningProcesses: Map<number, ChildProcess> = new Map();
@@ -62,7 +121,7 @@ export class HeadlessRunner {
62
121
  stallKillMs: config.stallKillMs ?? 1_800_000,
63
122
  stallAssessEnabled: config.stallAssessEnabled !== false,
64
123
  stallMaxExtensions: config.stallMaxExtensions ?? 3,
65
- stallHardCapMs: config.stallHardCapMs ?? 3_600_000,
124
+ stallHardCapMs: config.stallHardCapMs ?? 14_400_000,
66
125
  model: config.model,
67
126
  effortLevel: config.effortLevel,
68
127
  toolTimeoutProfiles: config.toolTimeoutProfiles,
@@ -103,77 +162,13 @@ export class HeadlessRunner {
103
162
 
104
163
  const result = await this.executePromptCommand(enrichedPrompt, 'main', 1);
105
164
 
106
- if (result.exitCode !== 0) {
107
- // Signal exits (128+) with meaningful output are successful completions —
108
- // Claude finished its work but the process was killed by signal (e.g., stall watchdog SIGTERM)
109
- const isSignalExit = result.exitCode >= 128;
110
- const hasOutput = !!(result.assistantResponse || (result.toolUseHistory && result.toolUseHistory.length > 0));
111
-
112
- if (isSignalExit && hasOutput) {
113
- const tokens = estimateTokensFromOutput(result.output);
114
- return {
115
- completed: true,
116
- needsHandoff: false,
117
- totalTokens: tokens,
118
- sessionId,
119
- signalName: result.signalName,
120
- assistantResponse: result.assistantResponse,
121
- thinkingOutput: result.thinkingOutput,
122
- toolUseHistory: result.toolUseHistory,
123
- claudeSessionId: result.claudeSessionId,
124
- nativeTimeoutCount: result.nativeTimeoutCount,
125
- postTimeoutOutput: result.postTimeoutOutput,
126
- resumeBufferedOutput: result.resumeBufferedOutput,
127
- stopReason: result.stopReason,
128
- };
129
- }
130
-
131
- // Build meaningful error: prefer stderr, fall back to non-JSON stdout lines
132
- let errorMessage = result.error;
133
- if (!errorMessage && result.output) {
134
- const plainLines = result.output.split('\n')
135
- .filter(l => l.trim() && !l.trim().startsWith('{'))
136
- .join('\n')
137
- .trim();
138
- if (plainLines) {
139
- errorMessage = plainLines.slice(0, 500);
140
- }
141
- }
142
- return {
143
- completed: false,
144
- needsHandoff: false,
145
- totalTokens: 0,
146
- sessionId,
147
- error: errorMessage || `Claude exited with code ${result.exitCode}`,
148
- signalName: result.signalName,
149
- assistantResponse: result.assistantResponse,
150
- thinkingOutput: result.thinkingOutput,
151
- toolUseHistory: result.toolUseHistory,
152
- claudeSessionId: result.claudeSessionId,
153
- nativeTimeoutCount: result.nativeTimeoutCount,
154
- postTimeoutOutput: result.postTimeoutOutput,
155
- resumeBufferedOutput: result.resumeBufferedOutput,
156
- stopReason: result.stopReason,
157
- };
165
+ if (result.exitCode === 0) {
166
+ return buildSuccessResult(sessionId, result);
158
167
  }
159
-
160
- const tokens = estimateTokensFromOutput(result.output);
161
-
162
- return {
163
- completed: true,
164
- needsHandoff: false,
165
- totalTokens: tokens,
166
- sessionId,
167
- signalName: result.signalName,
168
- assistantResponse: result.assistantResponse,
169
- thinkingOutput: result.thinkingOutput,
170
- toolUseHistory: result.toolUseHistory,
171
- claudeSessionId: result.claudeSessionId,
172
- nativeTimeoutCount: result.nativeTimeoutCount,
173
- postTimeoutOutput: result.postTimeoutOutput,
174
- resumeBufferedOutput: result.resumeBufferedOutput,
175
- stopReason: result.stopReason,
176
- };
168
+ if (isSignalExitWithOutput(result)) {
169
+ return buildSuccessResult(sessionId, result);
170
+ }
171
+ return buildErrorResult(sessionId, result);
177
172
  }
178
173
 
179
174
  /**
@@ -139,7 +139,7 @@ function buildAssessmentPrompt(ctx: StallContext): string {
139
139
  if (fromSkill) return fromSkill;
140
140
 
141
141
  return [
142
- 'You are a process health monitor. A Claude Code subprocess has been silent (no stdout) and you must determine if it is working or stalled.',
142
+ 'You are a process health monitor. A Claude Code subprocess has gone silent (no stdout) and you must determine if it is working or stalled.',
143
143
  '',
144
144
  `Silent for: ${silenceMin} minutes`,
145
145
  `Total runtime: ${totalMin} minutes`,
@@ -150,10 +150,15 @@ function buildAssessmentPrompt(ctx: StallContext): string {
150
150
  tokenLine,
151
151
  `Task being executed: ${promptPreview}`,
152
152
  '',
153
+ 'Weigh BOTH silence and total runtime against task complexity.',
154
+ '- Simple tasks (single Read/Write, one-liner edit, ls a directory) should finish in minutes. If total runtime has already far exceeded what the task should need, verdict STALLED even if silence is short.',
155
+ '- Complex tasks (agent teams, multi-step migrations, large refactors, dependency installs) can legitimately run hours. Extend generously when pending tool activity or subagents justify it.',
156
+ '- Recent token activity = process is alive; favor WORKING.',
157
+ '',
153
158
  'Respond in EXACTLY this format (3 lines, no extra text):',
154
159
  'VERDICT: WORKING or STALLED',
155
- 'MINUTES: <number 5-30, only if WORKING, how many more minutes to allow>',
156
- 'REASON: <brief one-line explanation>',
160
+ 'MINUTES: <integer 5-180, only if WORKING, how many more minutes to allow>',
161
+ 'REASON: <brief one-line explanation that references task complexity vs elapsed time>',
157
162
  ].filter(Boolean).join('\n');
158
163
  }
159
164
 
@@ -169,7 +174,7 @@ function parseAssessmentResponse(output: string): StallVerdict {
169
174
  verdict = trimmed.slice('VERDICT:'.length).trim().toUpperCase();
170
175
  } else if (trimmed.startsWith('MINUTES:')) {
171
176
  const parsed = parseInt(trimmed.slice('MINUTES:'.length).trim(), 10);
172
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 60) {
177
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 180) {
173
178
  minutes = parsed;
174
179
  }
175
180
  } else if (trimmed.startsWith('REASON:')) {
@@ -108,7 +108,7 @@ export interface HeadlessConfig {
108
108
  stallKillMs?: number; // No stdout before kill (default: 1800000 = 30 min)
109
109
  stallAssessEnabled?: boolean; // Use Haiku to assess stalls (default: true)
110
110
  stallMaxExtensions?: number; // Max number of Haiku-granted extensions (default: 3)
111
- stallHardCapMs?: number; // Absolute wall-clock kill cap (default: 3600000 = 60 min)
111
+ stallHardCapMs?: number; // Wall-clock backstop; only fires after stall signals have flagged the run (default: 14400000 = 4 h)
112
112
  /** Claude model for main execution (e.g., 'opus', 'sonnet'). 'default' = no --model flag. */
113
113
  model?: string;
114
114
  /**
@@ -0,0 +1,62 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Persistence helpers for improvisation session history.
6
+ *
7
+ * Resolves the `.mstro/history/<timestamp>.json` location for a session
8
+ * and reads/writes its JSON payload. No in-memory state — callers pass
9
+ * the current `SessionHistory` object.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { herror } from './headless/headless-logger.js';
15
+ import type { SessionHistory } from './improvisation-types.js';
16
+
17
+ export interface HistoryPaths {
18
+ improviseDir: string;
19
+ historyPath: string;
20
+ }
21
+
22
+ export function resolveHistoryPaths(workingDir: string, sessionId: string): HistoryPaths {
23
+ const improviseDir = join(workingDir, '.mstro', 'history');
24
+ const historyPath = join(improviseDir, `${sessionId.replace('improv-', '')}.json`);
25
+ return { improviseDir, historyPath };
26
+ }
27
+
28
+ /** Create the `.mstro/history/` directory if missing. */
29
+ export function ensureHistoryDir(improviseDir: string): void {
30
+ if (!existsSync(improviseDir)) {
31
+ mkdirSync(improviseDir, { recursive: true });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Load a session's history JSON. Returns a fresh empty history if the file
37
+ * doesn't exist or is unreadable (errors are logged but not thrown).
38
+ */
39
+ export function loadHistory(historyPath: string, sessionId: string): SessionHistory {
40
+ if (existsSync(historyPath)) {
41
+ try {
42
+ const data = readFileSync(historyPath, 'utf-8');
43
+ return JSON.parse(data) as SessionHistory;
44
+ } catch (error) {
45
+ herror('Failed to load history:', error);
46
+ }
47
+ }
48
+ const now = new Date().toISOString();
49
+ return {
50
+ sessionId,
51
+ startedAt: now,
52
+ lastActivityAt: now,
53
+ totalTokens: 0,
54
+ movements: [],
55
+ };
56
+ }
57
+
58
+ /** Write history to disk after bumping `lastActivityAt`. */
59
+ export function saveHistory(historyPath: string, history: SessionHistory): void {
60
+ history.lastActivityAt = new Date().toISOString();
61
+ writeFileSync(historyPath, JSON.stringify(history, null, 2));
62
+ }
@@ -0,0 +1,120 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Pure builders for the MovementRecord snapshots that session-manager
6
+ * writes after each executePrompt outcome — normal completion, user
7
+ * cancel, hard error — plus the auto-continue decision heuristic.
8
+ */
9
+
10
+ import type { HeadlessRunResult, MovementRecord, RetryLogEntry } from './improvisation-types.js';
11
+
12
+ /**
13
+ * Fallback `HeadlessRunResult` used when a cancellation fires before any
14
+ * run produces one. Shape matches the minimum fields the emit helpers
15
+ * look at downstream.
16
+ */
17
+ export const CANCELLED_FALLBACK_RESULT: HeadlessRunResult = {
18
+ completed: false, needsHandoff: false, totalTokens: 0, sessionId: '',
19
+ output: '', exitCode: 1, signalName: 'SIGTERM',
20
+ } as HeadlessRunResult;
21
+
22
+ export interface MovementBuildArgs {
23
+ sequenceNumber: number;
24
+ userPrompt: string;
25
+ execStart: number;
26
+ isAutoContinue?: boolean;
27
+ }
28
+
29
+ /** Build a MovementRecord representing a successful execution. */
30
+ export function buildSuccessMovement(
31
+ result: HeadlessRunResult,
32
+ args: MovementBuildArgs,
33
+ retryLog: RetryLogEntry[] | undefined,
34
+ ): MovementRecord {
35
+ return {
36
+ id: `prompt-${args.sequenceNumber}`,
37
+ sequenceNumber: args.sequenceNumber,
38
+ userPrompt: args.userPrompt,
39
+ timestamp: new Date().toISOString(),
40
+ tokensUsed: result.totalTokens,
41
+ summary: '',
42
+ filesModified: [],
43
+ assistantResponse: result.assistantResponse,
44
+ thinkingOutput: result.thinkingOutput,
45
+ toolUseHistory: result.toolUseHistory?.map(t => ({
46
+ toolName: t.toolName, toolId: t.toolId, toolInput: t.toolInput,
47
+ result: t.result, isError: t.isError, duration: t.duration,
48
+ })),
49
+ errorOutput: result.error,
50
+ durationMs: Date.now() - args.execStart,
51
+ retryLog: retryLog && retryLog.length > 0 ? retryLog : undefined,
52
+ ...(args.isAutoContinue && { isAutoContinue: true }),
53
+ };
54
+ }
55
+
56
+ /** Build a MovementRecord representing a user-initiated cancel mid-run. */
57
+ export function buildCancelledMovement(
58
+ result: HeadlessRunResult | undefined,
59
+ args: MovementBuildArgs,
60
+ ): MovementRecord {
61
+ return {
62
+ id: `prompt-${args.sequenceNumber}`,
63
+ sequenceNumber: args.sequenceNumber,
64
+ userPrompt: args.userPrompt,
65
+ timestamp: new Date().toISOString(),
66
+ tokensUsed: result ? result.totalTokens : 0,
67
+ summary: '',
68
+ filesModified: [],
69
+ assistantResponse: result?.assistantResponse,
70
+ thinkingOutput: result?.thinkingOutput,
71
+ toolUseHistory: result?.toolUseHistory?.map(t => ({
72
+ toolName: t.toolName, toolId: t.toolId, toolInput: t.toolInput,
73
+ result: t.result,
74
+ })),
75
+ errorOutput: 'Execution cancelled by user',
76
+ durationMs: Date.now() - args.execStart,
77
+ };
78
+ }
79
+
80
+ /** Build a MovementRecord for a thrown error inside executePrompt. */
81
+ export function buildErrorMovement(errorMessage: string, args: MovementBuildArgs): MovementRecord {
82
+ return {
83
+ id: `prompt-${args.sequenceNumber}`,
84
+ sequenceNumber: args.sequenceNumber,
85
+ userPrompt: args.userPrompt,
86
+ timestamp: new Date().toISOString(),
87
+ tokensUsed: 0,
88
+ summary: '',
89
+ filesModified: [],
90
+ errorOutput: errorMessage,
91
+ durationMs: Date.now() - args.execStart,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Heuristic for auto-continuing "end_turn" runs that appear incomplete:
97
+ * lots of thinking, not much visible output, and no tool work that
98
+ * already justifies the short response.
99
+ */
100
+ export function shouldAutoContinue(
101
+ result: HeadlessRunResult,
102
+ autoContinueCount: number,
103
+ maxAutoContinues: number,
104
+ cancelled: boolean,
105
+ ): boolean {
106
+ if (autoContinueCount >= maxAutoContinues) return false;
107
+ if (cancelled) return false;
108
+ if (!result.completed || result.signalName) return false;
109
+ if (result.stopReason !== 'end_turn') return false;
110
+
111
+ const thinkingLen = result.thinkingOutput?.length ?? 0;
112
+ const responseLen = result.assistantResponse?.length ?? 0;
113
+ const successfulToolCalls = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
114
+
115
+ if (thinkingLen < 500 || responseLen > 1000) return false;
116
+ // When the agent executed tool calls and produced a non-trivial response,
117
+ // long thinking is expected — the work happened in the tools, not the text.
118
+ if (successfulToolCalls > 0 && responseLen > 200) return false;
119
+ return thinkingLen >= responseLen * 3;
120
+ }
@@ -0,0 +1,42 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Small FIFO output buffer with a fixed-interval flush timer, used by the
6
+ * improvisation session manager to coalesce rapid stdout writes into
7
+ * steady `onOutput` emissions.
8
+ */
9
+
10
+ const FLUSH_INTERVAL_MS = 50;
11
+
12
+ export class OutputQueue {
13
+ private queue: Array<{ text: string; timestamp: number }> = [];
14
+ private timer: NodeJS.Timeout | null = null;
15
+
16
+ constructor(private readonly onEmit: (text: string) => void) {}
17
+
18
+ start(): void {
19
+ if (this.timer) return;
20
+ this.timer = setInterval(() => { this.flush(); }, FLUSH_INTERVAL_MS);
21
+ }
22
+
23
+ queue_(text: string): void {
24
+ this.queue.push({ text, timestamp: Date.now() });
25
+ }
26
+
27
+ /** Drain all buffered entries, emitting each via `onEmit` in order. */
28
+ flush(): void {
29
+ while (this.queue.length > 0) {
30
+ const item = this.queue.shift();
31
+ if (item) this.onEmit(item.text);
32
+ }
33
+ }
34
+
35
+ /** Stop the flush timer. Does NOT drain; call `flush()` first if needed. */
36
+ destroy(): void {
37
+ if (this.timer) {
38
+ clearInterval(this.timer);
39
+ this.timer = null;
40
+ }
41
+ }
42
+ }