mstro-app 0.4.52 → 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 (214) 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-worktree-handlers.d.ts.map +1 -1
  119. package/dist/server/services/websocket/git-worktree-handlers.js +28 -2
  120. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  121. package/dist/server/services/websocket/handler-context.d.ts +15 -0
  122. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  123. package/dist/server/services/websocket/handler.d.ts +7 -0
  124. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  125. package/dist/server/services/websocket/handler.js +73 -11
  126. package/dist/server/services/websocket/handler.js.map +1 -1
  127. package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
  128. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
  129. package/dist/server/services/websocket/msg-id-tracker.js +77 -0
  130. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
  131. package/dist/server/services/websocket/quality-handlers.js +15 -3
  132. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  133. package/dist/server/services/websocket/quality-review-agent.js +2 -2
  134. package/dist/server/services/websocket/session-handlers.d.ts +48 -2
  135. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  136. package/dist/server/services/websocket/session-handlers.js +204 -65
  137. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  138. package/dist/server/services/websocket/session-initialization.d.ts +2 -2
  139. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  140. package/dist/server/services/websocket/session-initialization.js +75 -17
  141. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  142. package/dist/server/services/websocket/session-registry.d.ts +29 -1
  143. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  144. package/dist/server/services/websocket/session-registry.js +53 -4
  145. package/dist/server/services/websocket/session-registry.js.map +1 -1
  146. package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
  147. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
  148. package/dist/server/services/websocket/tab-broadcast.js +13 -0
  149. package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
  150. package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
  151. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
  152. package/dist/server/services/websocket/tab-event-buffer.js +107 -0
  153. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
  154. package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
  155. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
  156. package/dist/server/services/websocket/tab-event-replay.js +21 -0
  157. package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
  158. package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
  159. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  160. package/dist/server/services/websocket/tab-handlers.js +2 -9
  161. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  162. package/dist/server/services/websocket/types.d.ts +15 -6
  163. package/dist/server/services/websocket/types.d.ts.map +1 -1
  164. package/dist/server/services/websocket/types.js +6 -4
  165. package/dist/server/services/websocket/types.js.map +1 -1
  166. package/package.json +1 -1
  167. package/server/README.md +1 -1
  168. package/server/cli/headless/claude-invoker-stall.ts +7 -2
  169. package/server/cli/headless/claude-invoker.ts +1 -1
  170. package/server/cli/headless/runner.ts +67 -72
  171. package/server/cli/headless/stall-assessor.ts +9 -4
  172. package/server/cli/headless/types.ts +1 -1
  173. package/server/cli/improvisation-history-store.ts +62 -0
  174. package/server/cli/improvisation-movements.ts +120 -0
  175. package/server/cli/improvisation-output-queue.ts +42 -0
  176. package/server/cli/improvisation-retry.ts +25 -600
  177. package/server/cli/improvisation-session-manager.ts +74 -160
  178. package/server/cli/retry/retry-best-result.ts +70 -0
  179. package/server/cli/retry/retry-context-loss.ts +87 -0
  180. package/server/cli/retry/retry-premature-completion.ts +113 -0
  181. package/server/cli/retry/retry-recovery-strategies.ts +247 -0
  182. package/server/cli/retry/retry-resume-strategy.ts +33 -0
  183. package/server/cli/retry/retry-runner-factory.ts +70 -0
  184. package/server/cli/retry/retry-tool-results.ts +31 -0
  185. package/server/cli/retry/retry-types.ts +32 -0
  186. package/server/index.ts +37 -123
  187. package/server/server-setup.ts +126 -1
  188. package/server/services/plan/agents/assess-stall.md +11 -4
  189. package/server/services/plan/board-config.ts +122 -0
  190. package/server/services/plan/composer.ts +7 -5
  191. package/server/services/plan/executor.ts +214 -467
  192. package/server/services/plan/issue-loader.ts +64 -0
  193. package/server/services/plan/issue-writer.ts +137 -0
  194. package/server/services/plan/output-manager.ts +2 -1
  195. package/server/services/plan/progress-log.ts +92 -0
  196. package/server/services/plan/prompt-builder.ts +73 -35
  197. package/server/services/plan/readiness-planner.ts +50 -0
  198. package/server/services/plan/review-gate.ts +102 -2
  199. package/server/services/platform.ts +163 -58
  200. package/server/services/websocket/file-download-handler.ts +191 -0
  201. package/server/services/websocket/git-worktree-handlers.ts +29 -2
  202. package/server/services/websocket/handler-context.ts +15 -0
  203. package/server/services/websocket/handler.ts +76 -12
  204. package/server/services/websocket/msg-id-tracker.ts +84 -0
  205. package/server/services/websocket/quality-handlers.ts +16 -3
  206. package/server/services/websocket/quality-review-agent.ts +2 -2
  207. package/server/services/websocket/session-handlers.ts +213 -68
  208. package/server/services/websocket/session-initialization.ts +83 -19
  209. package/server/services/websocket/session-registry.ts +61 -4
  210. package/server/services/websocket/tab-broadcast.ts +38 -0
  211. package/server/services/websocket/tab-event-buffer.ts +159 -0
  212. package/server/services/websocket/tab-event-replay.ts +42 -0
  213. package/server/services/websocket/tab-handlers.ts +2 -9
  214. package/server/services/websocket/types.ts +17 -4
@@ -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
+ }
@@ -2,605 +2,30 @@
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
4
  /**
5
- * Retry and recovery logic for improvisation sessions.
6
- * Handles context loss, tool timeouts, signal crashes, and premature completion.
5
+ * Retry and recovery logic for improvisation sessions — barrel re-export.
6
+ *
7
+ * Implementation lives in focused modules under ./retry/:
8
+ * - retry-types.ts — RetryCallbacks, RetrySessionState
9
+ * - retry-resume-strategy.ts — whether/how to --resume
10
+ * - retry-runner-factory.ts — HeadlessRunner factory for a single iteration
11
+ * - retry-context-loss.ts — Path 1 + Path 2 context loss detection
12
+ * - retry-tool-results.ts — accumulate tool results across retries
13
+ * - retry-recovery-strategies.ts — context-loss, tool-timeout, signal-crash retries
14
+ * - retry-premature-completion.ts — detect + recover from unfinished end_turn
15
+ * - retry-best-result.ts — pick the best result across retries
7
16
  */
8
17
 
9
- import { AnalyticsEvents, trackEvent } from '../services/analytics.js';
10
- import { hlog } from './headless/headless-logger.js';
11
- import { HeadlessRunner } from './headless/index.js';
12
- import { extractFinalTextBlock, isResponseAbandoned } from './headless/retry-strategies.js';
13
- import { assessBestResult, assessContextLoss, assessPrematureCompletion, type ContextLossContext } from './headless/stall-assessor.js';
14
- import type { ExecutionCheckpoint } from './headless/types.js';
15
- import type { FileAttachment, HeadlessRunResult, ImprovisationOptions, MovementRecord, RetryLoopState, SessionHistory } from './improvisation-types.js';
16
- import { scoreRunResult } from './improvisation-types.js';
17
- import {
18
- buildContextRecoveryPrompt,
19
- buildFreshRecoveryPrompt,
20
- buildHistoricalContext,
21
- buildInterMovementRecoveryPrompt,
22
- buildResumeRetryPrompt,
23
- buildRetryPrompt,
24
- buildSignalCrashRecoveryPrompt,
25
- extractHistoricalToolResults,
26
- } from './prompt-builders.js';
27
-
28
- /** Callbacks the retry logic needs from the session manager */
29
- export interface RetryCallbacks {
30
- isCancelled: () => boolean;
31
- queueOutput: (text: string) => void;
32
- flushOutputQueue: () => void;
33
- emit: (event: string, ...args: unknown[]) => void;
34
- addEventLog: (entry: { type: string; data: unknown; timestamp: number }) => void;
35
- setRunner: (runner: HeadlessRunner | null) => void;
36
- }
37
-
38
- /** Session state the retry logic reads/writes */
39
- export interface RetrySessionState {
40
- options: ImprovisationOptions;
41
- claudeSessionId: string | undefined;
42
- isFirstPrompt: boolean;
43
- isResumedSession: boolean;
44
- history: SessionHistory;
45
- executionStartTimestamp: number | undefined;
46
- }
47
-
48
- // ========== Resume Strategy ==========
49
-
50
- /** Determine whether to use --resume and which session ID */
51
- export function determineResumeStrategy(
52
- state: RetryLoopState,
53
- session: RetrySessionState,
54
- ): { useResume: boolean; resumeSessionId: string | undefined } {
55
- if (state.freshRecoveryMode) {
56
- state.freshRecoveryMode = false;
57
- return { useResume: false, resumeSessionId: undefined };
58
- }
59
- if (state.contextRecoverySessionId) {
60
- const id = state.contextRecoverySessionId;
61
- state.contextRecoverySessionId = undefined;
62
- return { useResume: true, resumeSessionId: id };
63
- }
64
- if (state.retryNumber === 0) {
65
- return { useResume: !session.isFirstPrompt, resumeSessionId: session.claudeSessionId };
66
- }
67
- if (state.lastWatchdogCheckpoint?.inProgressTools.length === 0 && state.lastWatchdogCheckpoint.claudeSessionId) {
68
- return { useResume: true, resumeSessionId: state.lastWatchdogCheckpoint.claudeSessionId };
69
- }
70
- return { useResume: false, resumeSessionId: undefined };
71
- }
72
-
73
- // ========== Runner Creation ==========
74
-
75
- /** Create HeadlessRunner for one retry iteration */
76
- export function createExecutionRunner(
77
- state: RetryLoopState,
78
- session: RetrySessionState,
79
- callbacks: RetryCallbacks,
80
- sequenceNumber: number,
81
- useResume: boolean,
82
- resumeSessionId: string | undefined,
83
- imageAttachments: FileAttachment[] | undefined,
84
- workingDirOverride?: string,
85
- ): HeadlessRunner {
86
- return new HeadlessRunner({
87
- workingDir: workingDirOverride || session.options.workingDir,
88
- tokenBudgetThreshold: session.options.tokenBudgetThreshold,
89
- maxSessions: session.options.maxSessions,
90
- verbose: session.options.verbose,
91
- noColor: session.options.noColor,
92
- model: session.options.model,
93
- effortLevel: session.options.effortLevel,
94
- improvisationMode: true,
95
- movementNumber: sequenceNumber,
96
- continueSession: useResume,
97
- claudeSessionId: resumeSessionId,
98
- outputCallback: (text: string) => {
99
- if (callbacks.isCancelled()) return;
100
- callbacks.addEventLog({ type: 'output', data: { text, timestamp: Date.now() }, timestamp: Date.now() });
101
- callbacks.queueOutput(text);
102
- callbacks.flushOutputQueue();
103
- },
104
- thinkingCallback: (text: string) => {
105
- if (callbacks.isCancelled()) return;
106
- callbacks.addEventLog({ type: 'thinking', data: { text }, timestamp: Date.now() });
107
- callbacks.emit('onThinking', text);
108
- callbacks.flushOutputQueue();
109
- },
110
- toolUseCallback: (event) => {
111
- if (callbacks.isCancelled()) return;
112
- callbacks.addEventLog({ type: 'toolUse', data: { ...event, timestamp: Date.now() }, timestamp: Date.now() });
113
- callbacks.emit('onToolUse', event);
114
- callbacks.flushOutputQueue();
115
- },
116
- tokenUsageCallback: (usage) => {
117
- if (callbacks.isCancelled()) return;
118
- callbacks.emit('onTokenUsage', usage);
119
- },
120
- directPrompt: state.currentPrompt,
121
- imageAttachments,
122
- promptContext: (state.retryNumber === 0 && session.isResumedSession && session.isFirstPrompt)
123
- ? { accumulatedKnowledge: buildHistoricalContext(session.history.movements), filesModified: [] }
124
- : undefined,
125
- onToolTimeout: (checkpoint: ExecutionCheckpoint) => {
126
- state.checkpointRef.value = checkpoint;
127
- },
128
- });
129
- }
130
-
131
- // ========== Context Loss Detection ==========
132
-
133
- /** Detect resume context loss (Path 1): session expired on --resume */
134
- export function detectResumeContextLoss(
135
- result: HeadlessRunResult,
136
- state: RetryLoopState,
137
- useResume: boolean,
138
- maxRetries: number,
139
- nativeTimeouts: number,
140
- verbose: boolean,
141
- ): void {
142
- if (!useResume || state.checkpointRef.value || state.retryNumber >= maxRetries || nativeTimeouts > 0) {
143
- return;
144
- }
145
- if (!result.assistantResponse || result.assistantResponse.trim().length === 0) {
146
- state.contextLost = true;
147
- if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: null/empty response');
148
- } else if (result.resumeBufferedOutput !== undefined) {
149
- state.contextLost = true;
150
- if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: buffer never flushed (no thinking/tools)');
151
- } else if (
152
- (!result.toolUseHistory || result.toolUseHistory.length === 0) &&
153
- !result.thinkingOutput &&
154
- result.assistantResponse.length < 500
155
- ) {
156
- state.contextLost = true;
157
- if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: no tools, no thinking, short response');
158
- }
159
- }
160
-
161
- /** Detect native timeout context loss (Path 2): tool timeouts caused confusion */
162
- export async function detectNativeTimeoutContextLoss(
163
- result: HeadlessRunResult,
164
- state: RetryLoopState,
165
- maxRetries: number,
166
- nativeTimeouts: number,
167
- verbose: boolean,
168
- ): Promise<void> {
169
- if (state.contextLost) return;
170
-
171
- const succeededIds = new Set<string>();
172
- const allIds = new Set<string>();
173
- for (const t of result.toolUseHistory ?? []) {
174
- allIds.add(t.toolId);
175
- if (t.result !== undefined) succeededIds.add(t.toolId);
176
- }
177
- const toolsWithoutResult = [...allIds].filter(id => !succeededIds.has(id)).length;
178
- const effectiveTimeouts = Math.max(nativeTimeouts, toolsWithoutResult);
179
-
180
- if (effectiveTimeouts === 0 || !result.assistantResponse || state.checkpointRef.value || state.retryNumber >= maxRetries) {
181
- return;
182
- }
183
-
184
- const writeToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
185
- const contextLossCtx: ContextLossContext = {
186
- assistantResponse: result.assistantResponse,
187
- effectiveTimeouts,
188
- nativeTimeoutCount: nativeTimeouts,
189
- successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
190
- thinkingOutputLength: result.thinkingOutput?.length ?? 0,
191
- hasSuccessfulWrite: result.toolUseHistory?.some(
192
- t => writeToolNames.has(t.toolName) && t.result !== undefined && !t.isError
193
- ) ?? false,
194
- };
195
-
196
- const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
197
- const verdict = await assessContextLoss(contextLossCtx, claudeCmd, verbose);
198
- state.contextLost = verdict.contextLost;
199
- if (verbose) {
200
- hlog(`[CONTEXT-RECOVERY] Haiku verdict: ${state.contextLost ? 'LOST' : 'OK'} — ${verdict.reason}`);
201
- }
202
- }
203
-
204
- // ========== Tool Result Accumulation ==========
205
-
206
- const MAX_ACCUMULATED_RESULTS = 50;
207
-
208
- /** Accumulate completed tool results from a run into the retry state */
209
- export function accumulateToolResults(result: HeadlessRunResult, state: RetryLoopState): void {
210
- if (!result.toolUseHistory) return;
211
- for (const t of result.toolUseHistory) {
212
- if (t.result !== undefined) {
213
- state.accumulatedToolResults.push({
214
- toolName: t.toolName,
215
- toolId: t.toolId,
216
- toolInput: t.toolInput,
217
- result: t.result,
218
- isError: t.isError,
219
- duration: t.duration,
220
- });
221
- }
222
- }
223
- if (state.accumulatedToolResults.length > MAX_ACCUMULATED_RESULTS) {
224
- state.accumulatedToolResults = state.accumulatedToolResults.slice(-MAX_ACCUMULATED_RESULTS);
225
- }
226
- }
227
-
228
- // ========== Recovery Strategies ==========
229
-
230
- /** Handle inter-movement context loss recovery (resume session expired) */
231
- export function applyInterMovementRecovery(
232
- state: RetryLoopState,
233
- promptWithAttachments: string,
234
- history: MovementRecord[],
235
- callbacks: RetryCallbacks,
236
- ): void {
237
- const historicalResults = extractHistoricalToolResults(history);
238
- const allResults = [...historicalResults, ...state.accumulatedToolResults];
239
-
240
- callbacks.emit('onAutoRetry', {
241
- retryNumber: state.retryNumber,
242
- maxRetries: 3,
243
- toolName: 'InterMovementRecovery',
244
- completedCount: allResults.length,
245
- });
246
- callbacks.queueOutput(
247
- `\n[[MSTRO_CONTEXT_RECOVERY]] Session context expired — continuing with ${allResults.length} preserved results from prior work (retry ${state.retryNumber}/3).\n`
248
- );
249
- callbacks.flushOutputQueue();
250
-
251
- state.freshRecoveryMode = true;
252
- state.currentPrompt = buildInterMovementRecoveryPrompt(promptWithAttachments, allResults, history);
253
- }
254
-
255
- /** Handle native-timeout context loss recovery (tool timeouts caused confusion) */
256
- export function applyNativeTimeoutRecovery(
257
- result: HeadlessRunResult,
258
- state: RetryLoopState,
259
- promptWithAttachments: string,
260
- session: RetrySessionState,
261
- callbacks: RetryCallbacks,
262
- ): void {
263
- const completedCount = state.accumulatedToolResults.length;
264
-
265
- callbacks.emit('onAutoRetry', {
266
- retryNumber: state.retryNumber,
267
- maxRetries: 3,
268
- toolName: 'ContextRecovery',
269
- completedCount,
270
- });
271
-
272
- if (result.claudeSessionId && state.retryNumber === 1) {
273
- callbacks.queueOutput(
274
- `\n[[MSTRO_CONTEXT_RECOVERY]] Context loss detected — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/3).\n`
275
- );
276
- callbacks.flushOutputQueue();
277
- state.contextRecoverySessionId = result.claudeSessionId;
278
- session.claudeSessionId = result.claudeSessionId;
279
- state.currentPrompt = buildContextRecoveryPrompt(promptWithAttachments);
280
- } else {
281
- callbacks.queueOutput(
282
- `\n[[MSTRO_CONTEXT_RECOVERY]] Continuing with fresh context — ${completedCount} preserved results injected (retry ${state.retryNumber}/3).\n`
283
- );
284
- callbacks.flushOutputQueue();
285
- state.freshRecoveryMode = true;
286
- state.currentPrompt = buildFreshRecoveryPrompt(promptWithAttachments, state.accumulatedToolResults, state.timedOutTools);
287
- }
288
- }
289
-
290
- /** Check if context loss recovery should trigger. Returns true if loop should continue. */
291
- export function shouldRetryContextLoss(
292
- result: HeadlessRunResult,
293
- state: RetryLoopState,
294
- session: RetrySessionState,
295
- useResume: boolean,
296
- nativeTimeouts: number,
297
- maxRetries: number,
298
- promptWithAttachments: string,
299
- callbacks: RetryCallbacks,
300
- ): boolean {
301
- if (state.checkpointRef.value || state.retryNumber >= maxRetries || !state.contextLost) {
302
- return false;
303
- }
304
- accumulateToolResults(result, state);
305
- state.retryNumber++;
306
- const path = (useResume && nativeTimeouts === 0) ? 'InterMovementRecovery' : 'NativeTimeoutRecovery';
307
- state.retryLog.push({
308
- retryNumber: state.retryNumber,
309
- path,
310
- reason: `Context lost (${nativeTimeouts} timeouts, ${state.accumulatedToolResults.length} tools preserved)`,
311
- timestamp: Date.now(),
312
- });
313
- if (useResume && nativeTimeouts === 0) {
314
- applyInterMovementRecovery(state, promptWithAttachments, session.history.movements, callbacks);
315
- } else {
316
- applyNativeTimeoutRecovery(result, state, promptWithAttachments, session, callbacks);
317
- }
318
- return true;
319
- }
320
-
321
- /** Handle tool timeout checkpoint. Returns true if loop should continue. */
322
- export function applyToolTimeoutRetry(
323
- state: RetryLoopState,
324
- maxRetries: number,
325
- promptWithAttachments: string,
326
- callbacks: RetryCallbacks,
327
- model: string | undefined,
328
- effortLevel: string | undefined,
329
- ): boolean {
330
- if (!state.checkpointRef.value || state.retryNumber >= maxRetries) {
331
- return false;
332
- }
333
-
334
- const cp: ExecutionCheckpoint = state.checkpointRef.value;
335
- state.retryNumber++;
336
-
337
- state.timedOutTools.push({
338
- toolName: cp.hungTool.toolName,
339
- input: cp.hungTool.input ?? {},
340
- timeoutMs: cp.hungTool.timeoutMs,
341
- });
342
-
343
- const canResumeSession = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
344
- state.retryLog.push({
345
- retryNumber: state.retryNumber,
346
- path: 'ToolTimeout',
347
- reason: `${cp.hungTool.toolName} timed out after ${cp.hungTool.timeoutMs}ms, ${cp.completedTools.length} tools completed, ${canResumeSession ? 'resuming' : 'fresh start'}`,
348
- timestamp: Date.now(),
349
- });
350
- callbacks.emit('onAutoRetry', {
351
- retryNumber: state.retryNumber,
352
- maxRetries,
353
- toolName: cp.hungTool.toolName,
354
- url: cp.hungTool.url,
355
- completedCount: cp.completedTools.length,
356
- });
357
-
358
- trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
359
- retry_number: state.retryNumber,
360
- hung_tool: cp.hungTool.toolName,
361
- hung_url: cp.hungTool.url?.slice(0, 200),
362
- completed_tools: cp.completedTools.length,
363
- elapsed_ms: cp.elapsedMs,
364
- resume_attempted: canResumeSession,
365
- model: model || 'default',
366
- effort_level: effortLevel || 'auto',
367
- });
368
-
369
- state.currentPrompt = canResumeSession
370
- ? buildResumeRetryPrompt(cp, state.timedOutTools)
371
- : buildRetryPrompt(cp, promptWithAttachments, state.timedOutTools);
372
-
373
- callbacks.queueOutput(
374
- `\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`
375
- );
376
- callbacks.flushOutputQueue();
377
-
378
- return true;
379
- }
380
-
381
- /** Detect and retry after a signal crash. Returns true if loop should continue. */
382
- export function shouldRetrySignalCrash(
383
- result: HeadlessRunResult,
384
- state: RetryLoopState,
385
- session: RetrySessionState,
386
- maxRetries: number,
387
- promptWithAttachments: string,
388
- callbacks: RetryCallbacks,
389
- ): boolean {
390
- const isSignalCrash = !!result.signalName;
391
- const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
392
- if ((!isSignalCrash && !exitCodeSignal) || state.retryNumber >= maxRetries) {
393
- return false;
394
- }
395
- if (state.checkpointRef.value) {
396
- return false;
397
- }
398
-
399
- accumulateToolResults(result, state);
400
- state.retryNumber++;
401
-
402
- const completedCount = state.accumulatedToolResults.length;
403
- const signalInfo = result.signalName || 'unknown signal';
404
- const useResume = !!result.claudeSessionId && state.retryNumber === 1;
405
-
406
- state.retryLog.push({
407
- retryNumber: state.retryNumber,
408
- path: 'SignalCrash',
409
- reason: `Process killed (${signalInfo}), ${completedCount} tools preserved, ${useResume ? 'resuming' : 'fresh start'}`,
410
- timestamp: Date.now(),
411
- });
412
-
413
- callbacks.emit('onAutoRetry', {
414
- retryNumber: state.retryNumber,
415
- maxRetries,
416
- toolName: `SignalCrash(${signalInfo})`,
417
- completedCount,
418
- });
419
-
420
- trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
421
- retry_number: state.retryNumber,
422
- hung_tool: `signal_crash:${signalInfo}`,
423
- completed_tools: completedCount,
424
- resume_attempted: useResume,
425
- });
426
-
427
- if (useResume) {
428
- callbacks.queueOutput(
429
- `\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
430
- );
431
- callbacks.flushOutputQueue();
432
- state.contextRecoverySessionId = result.claudeSessionId;
433
- session.claudeSessionId = result.claudeSessionId;
434
- state.currentPrompt = buildSignalCrashRecoveryPrompt(promptWithAttachments, true);
435
- } else {
436
- callbacks.queueOutput(
437
- `\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — restarting with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
438
- );
439
- callbacks.flushOutputQueue();
440
- state.freshRecoveryMode = true;
441
- const allResults = [...extractHistoricalToolResults(session.history.movements), ...state.accumulatedToolResults];
442
- state.currentPrompt = buildSignalCrashRecoveryPrompt(promptWithAttachments, false, allResults);
443
- }
444
-
445
- return true;
446
- }
447
-
448
- // ========== Premature Completion ==========
449
-
450
- /** Guard checks for premature completion */
451
- function isPrematureCompletionCandidate(
452
- result: HeadlessRunResult,
453
- state: RetryLoopState,
454
- maxRetries: number,
455
- ): boolean {
456
- if (!result.completed || result.signalName || state.retryNumber >= maxRetries) return false;
457
- if (state.checkpointRef.value || state.contextLost) return false;
458
- if (!result.claudeSessionId || !result.stopReason) return false;
459
- return result.stopReason === 'max_tokens' || result.stopReason === 'end_turn';
460
- }
461
-
462
- /** Use Haiku to assess whether an end_turn response is genuinely complete */
463
- async function assessEndTurnCompletion(result: HeadlessRunResult, verbose: boolean): Promise<boolean> {
464
- if (!result.assistantResponse) return false;
465
-
466
- const successfulToolCalls = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
467
- const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
468
- const verdict = await assessPrematureCompletion({
469
- responseTail: extractFinalTextBlock(result.assistantResponse, 800),
470
- successfulToolCalls,
471
- hasThinking: !!result.thinkingOutput,
472
- responseLength: result.assistantResponse.length,
473
- }, claudeCmd, verbose);
474
-
475
- if (verbose) {
476
- hlog(`[PREMATURE-COMPLETION] Haiku verdict: ${verdict.isIncomplete ? 'INCOMPLETE' : 'COMPLETE'} — ${verdict.reason}`);
477
- }
478
- return verdict.isIncomplete;
479
- }
480
-
481
- /** Apply premature completion retry */
482
- function applyPrematureCompletionRetry(
483
- result: HeadlessRunResult,
484
- state: RetryLoopState,
485
- session: RetrySessionState,
486
- maxRetries: number,
487
- stopReason: string,
488
- isMaxTokens: boolean,
489
- callbacks: RetryCallbacks,
490
- ): void {
491
- state.retryNumber++;
492
- const reason = isMaxTokens ? 'Output limit reached' : 'Task appears unfinished (AI assessment)';
493
-
494
- state.retryLog.push({
495
- retryNumber: state.retryNumber,
496
- path: 'PrematureCompletion',
497
- reason,
498
- timestamp: Date.now(),
499
- });
500
-
501
- callbacks.emit('onAutoRetry', {
502
- retryNumber: state.retryNumber,
503
- maxRetries,
504
- toolName: `PrematureCompletion(${stopReason})`,
505
- completedCount: result.toolUseHistory?.length ?? 0,
506
- });
507
-
508
- trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
509
- retry_number: state.retryNumber,
510
- hung_tool: `premature_completion:${stopReason}`,
511
- completed_tools: result.toolUseHistory?.length ?? 0,
512
- resume_attempted: true,
513
- });
514
-
515
- callbacks.queueOutput(
516
- `\n${reason} — resuming session (retry ${state.retryNumber}/${maxRetries}).\n`
517
- );
518
- callbacks.flushOutputQueue();
519
-
520
- state.contextRecoverySessionId = result.claudeSessionId;
521
- session.claudeSessionId = result.claudeSessionId;
522
- state.currentPrompt = 'continue';
523
- }
524
-
525
- /** Detect and retry premature completion. Returns true if loop should continue. */
526
- export async function shouldRetryPrematureCompletion(
527
- result: HeadlessRunResult,
528
- state: RetryLoopState,
529
- session: RetrySessionState,
530
- maxRetries: number,
531
- callbacks: RetryCallbacks,
532
- ): Promise<boolean> {
533
- if (!isPrematureCompletionCandidate(result, state, maxRetries)) {
534
- return false;
535
- }
536
-
537
- const stopReason = result.stopReason!;
538
- const isMaxTokens = stopReason === 'max_tokens';
539
- const abandoned = isResponseAbandoned(result);
540
- const isIncomplete = isMaxTokens || abandoned || await assessEndTurnCompletion(result, session.options.verbose);
541
-
542
- if (!isIncomplete) return false;
543
-
544
- applyPrematureCompletionRetry(result, state, session, maxRetries, stopReason, isMaxTokens, callbacks);
545
- return true;
546
- }
547
-
548
- // ========== Best Result Selection ==========
549
-
550
- /** Select the best result across retries using Haiku assessment */
551
- export async function selectBestResult(
552
- state: RetryLoopState,
553
- result: HeadlessRunResult,
554
- userPrompt: string,
555
- verbose: boolean,
556
- ): Promise<HeadlessRunResult> {
557
- if (!state.bestResult || state.bestResult === result || state.retryNumber === 0) {
558
- return result;
559
- }
560
-
561
- const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
562
- const bestToolCount = state.bestResult.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
563
- const currentToolCount = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
564
-
565
- try {
566
- const verdict = await assessBestResult({
567
- originalPrompt: userPrompt,
568
- resultA: {
569
- successfulToolCalls: bestToolCount,
570
- responseLength: state.bestResult.assistantResponse?.length ?? 0,
571
- hasThinking: !!state.bestResult.thinkingOutput,
572
- responseTail: (state.bestResult.assistantResponse ?? '').slice(-500),
573
- },
574
- resultB: {
575
- successfulToolCalls: currentToolCount,
576
- responseLength: result.assistantResponse?.length ?? 0,
577
- hasThinking: !!result.thinkingOutput,
578
- responseTail: (result.assistantResponse ?? '').slice(-500),
579
- },
580
- }, claudeCmd, verbose);
581
-
582
- if (verdict.winner === 'A') {
583
- if (verbose) hlog(`[BEST-RESULT] Haiku picked earlier attempt: ${verdict.reason}`);
584
- return mergeResultSessionId(state.bestResult, result.claudeSessionId);
585
- }
586
- if (verbose) hlog(`[BEST-RESULT] Haiku picked final attempt: ${verdict.reason}`);
587
- return result;
588
- } catch {
589
- return fallbackBestResult(state.bestResult, result, verbose);
590
- }
591
- }
592
-
593
- function mergeResultSessionId(result: HeadlessRunResult, sessionId: string | undefined): HeadlessRunResult {
594
- if (sessionId) return { ...result, claudeSessionId: sessionId };
595
- return result;
596
- }
597
-
598
- function fallbackBestResult(bestResult: HeadlessRunResult, result: HeadlessRunResult, verbose: boolean): HeadlessRunResult {
599
- if (scoreRunResult(bestResult) > scoreRunResult(result)) {
600
- if (verbose) {
601
- hlog(`[BEST-RESULT] Haiku unavailable, numeric fallback: earlier attempt (score ${scoreRunResult(bestResult)} vs ${scoreRunResult(result)})`);
602
- }
603
- return mergeResultSessionId(bestResult, result.claudeSessionId);
604
- }
605
- return result;
606
- }
18
+ export { selectBestResult } from './retry/retry-best-result.js';
19
+ export { detectNativeTimeoutContextLoss, detectResumeContextLoss } from './retry/retry-context-loss.js';
20
+ export { shouldRetryPrematureCompletion } from './retry/retry-premature-completion.js';
21
+ export {
22
+ applyInterMovementRecovery,
23
+ applyNativeTimeoutRecovery,
24
+ applyToolTimeoutRetry,
25
+ shouldRetryContextLoss,
26
+ shouldRetrySignalCrash,
27
+ } from './retry/retry-recovery-strategies.js';
28
+ export { determineResumeStrategy } from './retry/retry-resume-strategy.js';
29
+ export { createExecutionRunner } from './retry/retry-runner-factory.js';
30
+ export { accumulateToolResults } from './retry/retry-tool-results.js';
31
+ export type { RetryCallbacks, RetrySessionState } from './retry/retry-types.js';