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.
- package/README.md +10 -5
- package/bin/mstro.js +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.js +7 -2
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +1 -1
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +63 -67
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +9 -4
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts +16 -0
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
- package/dist/server/cli/improvisation-history-store.js +52 -0
- package/dist/server/cli/improvisation-history-store.js.map +1 -0
- package/dist/server/cli/improvisation-movements.d.ts +31 -0
- package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
- package/dist/server/cli/improvisation-movements.js +93 -0
- package/dist/server/cli/improvisation-movements.js.map +1 -0
- package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
- package/dist/server/cli/improvisation-output-queue.js +40 -0
- package/dist/server/cli/improvisation-output-queue.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +21 -51
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +18 -433
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +53 -148
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
- package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-best-result.js +61 -0
- package/dist/server/cli/retry/retry-best-result.js.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.js +68 -0
- package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.js +81 -0
- package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js +166 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.js +22 -0
- package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.js +60 -0
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.js +24 -0
- package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
- package/dist/server/cli/retry/retry-types.d.ts +30 -0
- package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-types.js +4 -0
- package/dist/server/cli/retry/retry-types.js.map +1 -0
- package/dist/server/index.js +21 -109
- package/dist/server/index.js.map +1 -1
- package/dist/server/server-setup.d.ts +16 -1
- package/dist/server/server-setup.d.ts.map +1 -1
- package/dist/server/server-setup.js +107 -0
- package/dist/server/server-setup.js.map +1 -1
- package/dist/server/services/plan/board-config.d.ts +21 -0
- package/dist/server/services/plan/board-config.d.ts.map +1 -0
- package/dist/server/services/plan/board-config.js +112 -0
- package/dist/server/services/plan/board-config.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +7 -5
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +48 -48
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +157 -455
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-loader.d.ts +16 -0
- package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
- package/dist/server/services/plan/issue-loader.js +46 -0
- package/dist/server/services/plan/issue-loader.js.map +1 -0
- package/dist/server/services/plan/issue-writer.d.ts +34 -0
- package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
- package/dist/server/services/plan/issue-writer.js +110 -0
- package/dist/server/services/plan/issue-writer.js.map +1 -0
- package/dist/server/services/plan/output-manager.d.ts.map +1 -1
- package/dist/server/services/plan/output-manager.js +2 -1
- package/dist/server/services/plan/output-manager.js.map +1 -1
- package/dist/server/services/plan/progress-log.d.ts +11 -0
- package/dist/server/services/plan/progress-log.d.ts.map +1 -0
- package/dist/server/services/plan/progress-log.js +81 -0
- package/dist/server/services/plan/progress-log.js.map +1 -0
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/prompt-builder.js +48 -31
- package/dist/server/services/plan/prompt-builder.js.map +1 -1
- package/dist/server/services/plan/readiness-planner.d.ts +15 -0
- package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
- package/dist/server/services/plan/readiness-planner.js +41 -0
- package/dist/server/services/plan/readiness-planner.js.map +1 -0
- package/dist/server/services/plan/review-gate.d.ts +31 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +52 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/platform.d.ts +56 -0
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +154 -52
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
- package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
- package/dist/server/services/websocket/file-download-handler.js +165 -0
- package/dist/server/services/websocket/file-download-handler.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +28 -2
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +15 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +7 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +73 -11
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
- package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
- package/dist/server/services/websocket/msg-id-tracker.js +77 -0
- package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.js +15 -3
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +2 -2
- package/dist/server/services/websocket/session-handlers.d.ts +48 -2
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +204 -65
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts +2 -2
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +75 -17
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +29 -1
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +53 -4
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-broadcast.js +13 -0
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.js +107 -0
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.js +21 -0
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +2 -9
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +15 -6
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +6 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/README.md +1 -1
- package/server/cli/headless/claude-invoker-stall.ts +7 -2
- package/server/cli/headless/claude-invoker.ts +1 -1
- package/server/cli/headless/runner.ts +67 -72
- package/server/cli/headless/stall-assessor.ts +9 -4
- package/server/cli/headless/types.ts +1 -1
- package/server/cli/improvisation-history-store.ts +62 -0
- package/server/cli/improvisation-movements.ts +120 -0
- package/server/cli/improvisation-output-queue.ts +42 -0
- package/server/cli/improvisation-retry.ts +25 -600
- package/server/cli/improvisation-session-manager.ts +74 -160
- package/server/cli/retry/retry-best-result.ts +70 -0
- package/server/cli/retry/retry-context-loss.ts +87 -0
- package/server/cli/retry/retry-premature-completion.ts +113 -0
- package/server/cli/retry/retry-recovery-strategies.ts +247 -0
- package/server/cli/retry/retry-resume-strategy.ts +33 -0
- package/server/cli/retry/retry-runner-factory.ts +70 -0
- package/server/cli/retry/retry-tool-results.ts +31 -0
- package/server/cli/retry/retry-types.ts +32 -0
- package/server/index.ts +37 -123
- package/server/server-setup.ts +126 -1
- package/server/services/plan/agents/assess-stall.md +11 -4
- package/server/services/plan/board-config.ts +122 -0
- package/server/services/plan/composer.ts +7 -5
- package/server/services/plan/executor.ts +214 -467
- package/server/services/plan/issue-loader.ts +64 -0
- package/server/services/plan/issue-writer.ts +137 -0
- package/server/services/plan/output-manager.ts +2 -1
- package/server/services/plan/progress-log.ts +92 -0
- package/server/services/plan/prompt-builder.ts +73 -35
- package/server/services/plan/readiness-planner.ts +50 -0
- package/server/services/plan/review-gate.ts +102 -2
- package/server/services/platform.ts +163 -58
- package/server/services/websocket/file-download-handler.ts +191 -0
- package/server/services/websocket/git-worktree-handlers.ts +29 -2
- package/server/services/websocket/handler-context.ts +15 -0
- package/server/services/websocket/handler.ts +76 -12
- package/server/services/websocket/msg-id-tracker.ts +84 -0
- package/server/services/websocket/quality-handlers.ts +16 -3
- package/server/services/websocket/quality-review-agent.ts +2 -2
- package/server/services/websocket/session-handlers.ts +213 -68
- package/server/services/websocket/session-initialization.ts +83 -19
- package/server/services/websocket/session-registry.ts +61 -4
- package/server/services/websocket/tab-broadcast.ts +38 -0
- package/server/services/websocket/tab-event-buffer.ts +159 -0
- package/server/services/websocket/tab-event-replay.ts +42 -0
- package/server/services/websocket/tab-handlers.ts +2 -9
- package/server/services/websocket/types.ts +17 -4
|
@@ -6,17 +6,37 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Optimized for fast, direct prompt execution in Improvise mode.
|
|
8
8
|
* For complex multi-part prompts with parallel/sequential movements, use Compose tab instead.
|
|
9
|
+
*
|
|
10
|
+
* Delegates to focused helpers:
|
|
11
|
+
* - improvisation-output-queue.ts — buffered stdout flush loop
|
|
12
|
+
* - improvisation-history-store.ts — .mstro/history/*.json load/save
|
|
13
|
+
* - improvisation-movements.ts — pure movement-record builders
|
|
14
|
+
* - improvisation-retry.ts — retry decision tree + recovery strategies
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { existsSync,
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
19
|
import { join } from 'node:path';
|
|
14
20
|
import { AnalyticsEvents, trackEvent } from '../services/analytics.js';
|
|
15
21
|
import { herror } from './headless/headless-logger.js';
|
|
16
22
|
import { cleanupAttachments, preparePromptAndAttachments } from './improvisation-attachments.js';
|
|
23
|
+
import {
|
|
24
|
+
ensureHistoryDir,
|
|
25
|
+
loadHistory,
|
|
26
|
+
resolveHistoryPaths,
|
|
27
|
+
saveHistory,
|
|
28
|
+
} from './improvisation-history-store.js';
|
|
29
|
+
import {
|
|
30
|
+
buildCancelledMovement,
|
|
31
|
+
buildErrorMovement,
|
|
32
|
+
buildSuccessMovement,
|
|
33
|
+
CANCELLED_FALLBACK_RESULT,
|
|
34
|
+
shouldAutoContinue,
|
|
35
|
+
} from './improvisation-movements.js';
|
|
36
|
+
import { OutputQueue } from './improvisation-output-queue.js';
|
|
17
37
|
import type { RetryCallbacks, RetrySessionState } from './improvisation-retry.js';
|
|
18
|
-
import {applyToolTimeoutRetry,
|
|
19
|
-
createExecutionRunner,detectNativeTimeoutContextLoss, detectResumeContextLoss,
|
|
38
|
+
import {applyToolTimeoutRetry,
|
|
39
|
+
createExecutionRunner,detectNativeTimeoutContextLoss, detectResumeContextLoss,
|
|
20
40
|
determineResumeStrategy,
|
|
21
41
|
selectBestResult,
|
|
22
42
|
shouldRetryContextLoss,
|
|
@@ -40,8 +60,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
40
60
|
plan: unknown;
|
|
41
61
|
resolve: (approved: boolean) => void;
|
|
42
62
|
};
|
|
43
|
-
private
|
|
44
|
-
private queueTimer: NodeJS.Timeout | null = null;
|
|
63
|
+
private outputBuffer: OutputQueue;
|
|
45
64
|
private isFirstPrompt: boolean = true;
|
|
46
65
|
private claudeSessionId: string | undefined;
|
|
47
66
|
private isResumedSession: boolean = false;
|
|
@@ -54,6 +73,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
54
73
|
private _cancelCompleteEmitted: boolean = false;
|
|
55
74
|
private _currentUserPrompt: string = '';
|
|
56
75
|
private _currentSequenceNumber: number = 0;
|
|
76
|
+
private _hasPersistedToDisk: boolean = false;
|
|
57
77
|
|
|
58
78
|
static resumeFromHistory(workingDir: string, historicalSessionId: string, overrides?: Partial<ImprovisationOptions>): ImprovisationSessionManager {
|
|
59
79
|
const historyDir = join(workingDir, '.mstro', 'history');
|
|
@@ -79,6 +99,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
79
99
|
|
|
80
100
|
manager.isResumedSession = true;
|
|
81
101
|
manager.isFirstPrompt = true;
|
|
102
|
+
manager._hasPersistedToDisk = true;
|
|
82
103
|
if (historyData.claudeSessionId) {
|
|
83
104
|
manager.claudeSessionId = historyData.claudeSessionId;
|
|
84
105
|
}
|
|
@@ -101,33 +122,29 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
101
122
|
};
|
|
102
123
|
|
|
103
124
|
this.sessionId = this.options.sessionId;
|
|
104
|
-
|
|
105
|
-
this.
|
|
125
|
+
const paths = resolveHistoryPaths(this.options.workingDir, this.sessionId);
|
|
126
|
+
this.improviseDir = paths.improviseDir;
|
|
127
|
+
this.historyPath = paths.historyPath;
|
|
128
|
+
ensureHistoryDir(this.improviseDir);
|
|
106
129
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
this.history = loadHistory(this.historyPath, this.sessionId);
|
|
131
|
+
// History is persisted lazily on the first `persistHistory` call (see
|
|
132
|
+
// `executePrompt`). Deferring the initial write keeps the Chat History
|
|
133
|
+
// view from showing "0 prompts" entries for tabs the user opens but
|
|
134
|
+
// never prompts.
|
|
110
135
|
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
113
|
-
this.startQueueProcessor();
|
|
136
|
+
this.outputBuffer = new OutputQueue(text => this.emit('onOutput', text));
|
|
137
|
+
this.outputBuffer.start();
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
// ========== Output Queue ==========
|
|
117
141
|
|
|
118
|
-
private startQueueProcessor(): void {
|
|
119
|
-
this.queueTimer = setInterval(() => { this.flushOutputQueue(); }, 50);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
142
|
private queueOutput(text: string): void {
|
|
123
|
-
this.
|
|
143
|
+
this.outputBuffer.queue_(text);
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
private flushOutputQueue(): void {
|
|
127
|
-
|
|
128
|
-
const item = this.outputQueue.shift();
|
|
129
|
-
if (item) this.emit('onOutput', item.text);
|
|
130
|
-
}
|
|
147
|
+
this.outputBuffer.flush();
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
// ========== Main Execution ==========
|
|
@@ -174,7 +191,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
174
191
|
...(isAutoContinue && { isAutoContinue: true }),
|
|
175
192
|
};
|
|
176
193
|
this.history.movements.push(pendingMovement);
|
|
177
|
-
this.
|
|
194
|
+
this.persistHistory();
|
|
178
195
|
|
|
179
196
|
try {
|
|
180
197
|
this.executionEventLog.push({
|
|
@@ -212,7 +229,11 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
212
229
|
this.captureSessionAndSurfaceErrors(result);
|
|
213
230
|
this.isFirstPrompt = false;
|
|
214
231
|
|
|
215
|
-
const movement =
|
|
232
|
+
const movement = buildSuccessMovement(
|
|
233
|
+
result,
|
|
234
|
+
{ sequenceNumber, userPrompt: displayPrompt, execStart: _execStart, isAutoContinue },
|
|
235
|
+
state.retryLog,
|
|
236
|
+
);
|
|
216
237
|
this.handleConflicts(result);
|
|
217
238
|
this.persistMovement(movement);
|
|
218
239
|
|
|
@@ -345,7 +366,14 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
345
366
|
return false;
|
|
346
367
|
}
|
|
347
368
|
|
|
348
|
-
// ========== Cancel Handling ==========
|
|
369
|
+
// ========== Cancel / Error Handling ==========
|
|
370
|
+
|
|
371
|
+
private resetExecutionState(): void {
|
|
372
|
+
this._isExecuting = false;
|
|
373
|
+
this._executionStartTimestamp = undefined;
|
|
374
|
+
this.executionEventLog = [];
|
|
375
|
+
this.currentRunner = null;
|
|
376
|
+
}
|
|
349
377
|
|
|
350
378
|
private handleCancelledExecution(
|
|
351
379
|
result: HeadlessRunResult | undefined,
|
|
@@ -353,39 +381,16 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
353
381
|
sequenceNumber: number,
|
|
354
382
|
execStart: number,
|
|
355
383
|
): MovementRecord {
|
|
356
|
-
this.
|
|
357
|
-
this._executionStartTimestamp = undefined;
|
|
358
|
-
this.executionEventLog = [];
|
|
359
|
-
this.currentRunner = null;
|
|
384
|
+
this.resetExecutionState();
|
|
360
385
|
|
|
361
386
|
if (this._cancelCompleteEmitted) {
|
|
362
387
|
const existing = this.history.movements.find(m => m.sequenceNumber === sequenceNumber);
|
|
363
388
|
if (existing) return existing;
|
|
364
389
|
}
|
|
365
390
|
|
|
366
|
-
const cancelledMovement
|
|
367
|
-
id: `prompt-${sequenceNumber}`,
|
|
368
|
-
sequenceNumber,
|
|
369
|
-
userPrompt,
|
|
370
|
-
timestamp: new Date().toISOString(),
|
|
371
|
-
tokensUsed: result ? result.totalTokens : 0,
|
|
372
|
-
summary: '',
|
|
373
|
-
filesModified: [],
|
|
374
|
-
assistantResponse: result?.assistantResponse,
|
|
375
|
-
thinkingOutput: result?.thinkingOutput,
|
|
376
|
-
toolUseHistory: result?.toolUseHistory?.map(t => ({
|
|
377
|
-
toolName: t.toolName, toolId: t.toolId, toolInput: t.toolInput,
|
|
378
|
-
result: t.result,
|
|
379
|
-
})),
|
|
380
|
-
errorOutput: 'Execution cancelled by user',
|
|
381
|
-
durationMs: Date.now() - execStart,
|
|
382
|
-
};
|
|
391
|
+
const cancelledMovement = buildCancelledMovement(result, { sequenceNumber, userPrompt, execStart });
|
|
383
392
|
this.persistMovement(cancelledMovement);
|
|
384
|
-
|
|
385
|
-
completed: false, needsHandoff: false, totalTokens: 0, sessionId: '',
|
|
386
|
-
output: '', exitCode: 1, signalName: 'SIGTERM',
|
|
387
|
-
} as HeadlessRunResult;
|
|
388
|
-
this.emitMovementComplete(cancelledMovement, result ?? fallbackResult, execStart, sequenceNumber);
|
|
393
|
+
this.emitMovementComplete(cancelledMovement, result ?? CANCELLED_FALLBACK_RESULT, execStart, sequenceNumber);
|
|
389
394
|
return cancelledMovement;
|
|
390
395
|
}
|
|
391
396
|
|
|
@@ -395,23 +400,10 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
395
400
|
sequenceNumber: number,
|
|
396
401
|
execStart: number,
|
|
397
402
|
): never {
|
|
398
|
-
this.
|
|
399
|
-
this._executionStartTimestamp = undefined;
|
|
400
|
-
this.executionEventLog = [];
|
|
401
|
-
this.currentRunner = null;
|
|
403
|
+
this.resetExecutionState();
|
|
402
404
|
|
|
403
405
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
404
|
-
const errorMovement:
|
|
405
|
-
id: `prompt-${sequenceNumber}`,
|
|
406
|
-
sequenceNumber,
|
|
407
|
-
userPrompt: displayPrompt,
|
|
408
|
-
timestamp: new Date().toISOString(),
|
|
409
|
-
tokensUsed: 0,
|
|
410
|
-
summary: '',
|
|
411
|
-
filesModified: [],
|
|
412
|
-
errorOutput: errorMessage,
|
|
413
|
-
durationMs: Date.now() - execStart,
|
|
414
|
-
};
|
|
406
|
+
const errorMovement = buildErrorMovement(errorMessage, { sequenceNumber, userPrompt: displayPrompt, execStart });
|
|
415
407
|
this.persistMovement(errorMovement);
|
|
416
408
|
|
|
417
409
|
this.emit('onMovementError', error);
|
|
@@ -440,35 +432,6 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
440
432
|
}
|
|
441
433
|
}
|
|
442
434
|
|
|
443
|
-
private buildMovementRecord(
|
|
444
|
-
result: HeadlessRunResult,
|
|
445
|
-
userPrompt: string,
|
|
446
|
-
sequenceNumber: number,
|
|
447
|
-
execStart: number,
|
|
448
|
-
retryLog?: import('./improvisation-types.js').RetryLogEntry[],
|
|
449
|
-
isAutoContinue?: boolean,
|
|
450
|
-
): MovementRecord {
|
|
451
|
-
return {
|
|
452
|
-
id: `prompt-${sequenceNumber}`,
|
|
453
|
-
sequenceNumber,
|
|
454
|
-
userPrompt,
|
|
455
|
-
timestamp: new Date().toISOString(),
|
|
456
|
-
tokensUsed: result.totalTokens,
|
|
457
|
-
summary: '',
|
|
458
|
-
filesModified: [],
|
|
459
|
-
assistantResponse: result.assistantResponse,
|
|
460
|
-
thinkingOutput: result.thinkingOutput,
|
|
461
|
-
toolUseHistory: result.toolUseHistory?.map(t => ({
|
|
462
|
-
toolName: t.toolName, toolId: t.toolId, toolInput: t.toolInput,
|
|
463
|
-
result: t.result, isError: t.isError, duration: t.duration,
|
|
464
|
-
})),
|
|
465
|
-
errorOutput: result.error,
|
|
466
|
-
durationMs: Date.now() - execStart,
|
|
467
|
-
retryLog: retryLog && retryLog.length > 0 ? retryLog : undefined,
|
|
468
|
-
...(isAutoContinue && { isAutoContinue: true }),
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
435
|
private handleConflicts(result: HeadlessRunResult): void {
|
|
473
436
|
if (!result.conflicts || result.conflicts.length === 0) return;
|
|
474
437
|
this.queueOutput(`\n⚠ File conflicts detected: ${result.conflicts.length}`);
|
|
@@ -489,7 +452,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
489
452
|
this.history.movements.push(movement);
|
|
490
453
|
this.history.totalTokens += movement.tokensUsed;
|
|
491
454
|
}
|
|
492
|
-
this.
|
|
455
|
+
this.persistHistory();
|
|
493
456
|
}
|
|
494
457
|
|
|
495
458
|
private emitMovementComplete(movement: MovementRecord, result: HeadlessRunResult, execStart: number, sequenceNumber: number): void {
|
|
@@ -515,26 +478,10 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
515
478
|
const isStallKill = !this._cancelled && !!result.signalName;
|
|
516
479
|
if (isStallKill && this._autoContinueCount < ImprovisationSessionManager.MAX_AUTO_CONTINUES) {
|
|
517
480
|
this.scheduleAutoContinue('Process stalled');
|
|
518
|
-
} else if (
|
|
481
|
+
} else if (shouldAutoContinue(result, this._autoContinueCount, ImprovisationSessionManager.MAX_AUTO_CONTINUES, this._cancelled)) {
|
|
519
482
|
this.scheduleAutoContinue();
|
|
520
483
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
private shouldAutoContinue(result: HeadlessRunResult, _userPrompt: string): boolean {
|
|
524
|
-
if (this._autoContinueCount >= ImprovisationSessionManager.MAX_AUTO_CONTINUES) return false;
|
|
525
|
-
if (this._cancelled) return false;
|
|
526
|
-
if (!result.completed || result.signalName) return false;
|
|
527
|
-
if (result.stopReason !== 'end_turn') return false;
|
|
528
|
-
|
|
529
|
-
const thinkingLen = result.thinkingOutput?.length ?? 0;
|
|
530
|
-
const responseLen = result.assistantResponse?.length ?? 0;
|
|
531
|
-
const successfulToolCalls = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
532
|
-
|
|
533
|
-
if (thinkingLen < 500 || responseLen > 1000) return false;
|
|
534
|
-
// When the agent executed tool calls and produced a non-trivial response,
|
|
535
|
-
// long thinking is expected — the work happened in the tools, not the text.
|
|
536
|
-
if (successfulToolCalls > 0 && responseLen > 200) return false;
|
|
537
|
-
return thinkingLen >= responseLen * 3;
|
|
484
|
+
void userPrompt;
|
|
538
485
|
}
|
|
539
486
|
|
|
540
487
|
private scheduleAutoContinue(reason?: string): void {
|
|
@@ -555,27 +502,12 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
555
502
|
|
|
556
503
|
// ========== History I/O ==========
|
|
557
504
|
|
|
558
|
-
private
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
} catch (error) {
|
|
564
|
-
herror('Failed to load history:', error);
|
|
565
|
-
}
|
|
505
|
+
private persistHistory(): void {
|
|
506
|
+
saveHistory(this.historyPath, this.history);
|
|
507
|
+
if (!this._hasPersistedToDisk) {
|
|
508
|
+
this._hasPersistedToDisk = true;
|
|
509
|
+
this.emit('onHistoryPersisted');
|
|
566
510
|
}
|
|
567
|
-
return {
|
|
568
|
-
sessionId: this.sessionId,
|
|
569
|
-
startedAt: new Date().toISOString(),
|
|
570
|
-
lastActivityAt: new Date().toISOString(),
|
|
571
|
-
totalTokens: 0,
|
|
572
|
-
movements: [],
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
private saveHistory(): void {
|
|
577
|
-
this.history.lastActivityAt = new Date().toISOString();
|
|
578
|
-
writeFileSync(this.historyPath, JSON.stringify(this.history, null, 2));
|
|
579
511
|
}
|
|
580
512
|
|
|
581
513
|
getHistory(): SessionHistory {
|
|
@@ -592,7 +524,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
592
524
|
this.currentRunner = null;
|
|
593
525
|
}
|
|
594
526
|
|
|
595
|
-
this.
|
|
527
|
+
this.outputBuffer.destroy();
|
|
596
528
|
|
|
597
529
|
if (this._isExecuting && !this._cancelCompleteEmitted) {
|
|
598
530
|
this._cancelCompleteEmitted = true;
|
|
@@ -600,38 +532,20 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
600
532
|
this._isExecuting = false;
|
|
601
533
|
this._executionStartTimestamp = undefined;
|
|
602
534
|
|
|
603
|
-
const cancelledMovement
|
|
604
|
-
id: `prompt-${this._currentSequenceNumber}`,
|
|
535
|
+
const cancelledMovement = buildCancelledMovement(undefined, {
|
|
605
536
|
sequenceNumber: this._currentSequenceNumber,
|
|
606
537
|
userPrompt: this._currentUserPrompt,
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
summary: '',
|
|
610
|
-
filesModified: [],
|
|
611
|
-
errorOutput: 'Execution cancelled by user',
|
|
612
|
-
durationMs: Date.now() - execStart,
|
|
613
|
-
};
|
|
538
|
+
execStart,
|
|
539
|
+
});
|
|
614
540
|
this.persistMovement(cancelledMovement);
|
|
615
|
-
|
|
616
|
-
const fallbackResult = {
|
|
617
|
-
completed: false, needsHandoff: false, totalTokens: 0, sessionId: '',
|
|
618
|
-
output: '', exitCode: 1, signalName: 'SIGTERM',
|
|
619
|
-
} as HeadlessRunResult;
|
|
620
|
-
this.emitMovementComplete(cancelledMovement, fallbackResult, execStart, this._currentSequenceNumber);
|
|
541
|
+
this.emitMovementComplete(cancelledMovement, CANCELLED_FALLBACK_RESULT, execStart, this._currentSequenceNumber);
|
|
621
542
|
}
|
|
622
543
|
|
|
623
544
|
this.flushOutputQueue();
|
|
624
545
|
}
|
|
625
546
|
|
|
626
|
-
private destroyQueueTimer(): void {
|
|
627
|
-
if (this.queueTimer) {
|
|
628
|
-
clearInterval(this.queueTimer);
|
|
629
|
-
this.queueTimer = null;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
547
|
destroy(): void {
|
|
634
|
-
this.
|
|
548
|
+
this.outputBuffer.destroy();
|
|
635
549
|
this.flushOutputQueue();
|
|
636
550
|
}
|
|
637
551
|
|
|
@@ -642,7 +556,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
642
556
|
this.isFirstPrompt = true;
|
|
643
557
|
this.claudeSessionId = undefined;
|
|
644
558
|
cleanupAttachments(this.options.workingDir, this.sessionId);
|
|
645
|
-
this.
|
|
559
|
+
this.persistHistory();
|
|
646
560
|
this.emit('onSessionUpdate', this.getHistory());
|
|
647
561
|
}
|
|
648
562
|
|
|
@@ -684,7 +598,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
684
598
|
}
|
|
685
599
|
|
|
686
600
|
startNewSession(overrides?: Partial<ImprovisationOptions>): ImprovisationSessionManager {
|
|
687
|
-
this.
|
|
601
|
+
this.persistHistory();
|
|
688
602
|
return new ImprovisationSessionManager({
|
|
689
603
|
...this.options,
|
|
690
604
|
sessionId: `improv-${Date.now()}`,
|
|
@@ -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
|
+
* Picks the best result across retry attempts. Prefers Haiku's judgment
|
|
6
|
+
* when available; falls back to a numeric score when Haiku is unreachable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { hlog } from '../headless/headless-logger.js';
|
|
10
|
+
import { assessBestResult } from '../headless/stall-assessor.js';
|
|
11
|
+
import type { HeadlessRunResult, RetryLoopState } from '../improvisation-types.js';
|
|
12
|
+
import { scoreRunResult } from '../improvisation-types.js';
|
|
13
|
+
|
|
14
|
+
/** Select the best result across retries using Haiku assessment */
|
|
15
|
+
export async function selectBestResult(
|
|
16
|
+
state: RetryLoopState,
|
|
17
|
+
result: HeadlessRunResult,
|
|
18
|
+
userPrompt: string,
|
|
19
|
+
verbose: boolean,
|
|
20
|
+
): Promise<HeadlessRunResult> {
|
|
21
|
+
if (!state.bestResult || state.bestResult === result || state.retryNumber === 0) {
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
26
|
+
const bestToolCount = state.bestResult.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
27
|
+
const currentToolCount = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const verdict = await assessBestResult({
|
|
31
|
+
originalPrompt: userPrompt,
|
|
32
|
+
resultA: {
|
|
33
|
+
successfulToolCalls: bestToolCount,
|
|
34
|
+
responseLength: state.bestResult.assistantResponse?.length ?? 0,
|
|
35
|
+
hasThinking: !!state.bestResult.thinkingOutput,
|
|
36
|
+
responseTail: (state.bestResult.assistantResponse ?? '').slice(-500),
|
|
37
|
+
},
|
|
38
|
+
resultB: {
|
|
39
|
+
successfulToolCalls: currentToolCount,
|
|
40
|
+
responseLength: result.assistantResponse?.length ?? 0,
|
|
41
|
+
hasThinking: !!result.thinkingOutput,
|
|
42
|
+
responseTail: (result.assistantResponse ?? '').slice(-500),
|
|
43
|
+
},
|
|
44
|
+
}, claudeCmd, verbose);
|
|
45
|
+
|
|
46
|
+
if (verdict.winner === 'A') {
|
|
47
|
+
if (verbose) hlog(`[BEST-RESULT] Haiku picked earlier attempt: ${verdict.reason}`);
|
|
48
|
+
return mergeResultSessionId(state.bestResult, result.claudeSessionId);
|
|
49
|
+
}
|
|
50
|
+
if (verbose) hlog(`[BEST-RESULT] Haiku picked final attempt: ${verdict.reason}`);
|
|
51
|
+
return result;
|
|
52
|
+
} catch {
|
|
53
|
+
return fallbackBestResult(state.bestResult, result, verbose);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mergeResultSessionId(result: HeadlessRunResult, sessionId: string | undefined): HeadlessRunResult {
|
|
58
|
+
if (sessionId) return { ...result, claudeSessionId: sessionId };
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fallbackBestResult(bestResult: HeadlessRunResult, result: HeadlessRunResult, verbose: boolean): HeadlessRunResult {
|
|
63
|
+
if (scoreRunResult(bestResult) > scoreRunResult(result)) {
|
|
64
|
+
if (verbose) {
|
|
65
|
+
hlog(`[BEST-RESULT] Haiku unavailable, numeric fallback: earlier attempt (score ${scoreRunResult(bestResult)} vs ${scoreRunResult(result)})`);
|
|
66
|
+
}
|
|
67
|
+
return mergeResultSessionId(bestResult, result.claudeSessionId);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context-loss detection: figures out when a run's output indicates the
|
|
6
|
+
* Claude session dropped its memory, either on `--resume` (Path 1) or after
|
|
7
|
+
* native tool timeouts scrambled the conversation (Path 2).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { hlog } from '../headless/headless-logger.js';
|
|
11
|
+
import { assessContextLoss, type ContextLossContext } from '../headless/stall-assessor.js';
|
|
12
|
+
import type { HeadlessRunResult, RetryLoopState } from '../improvisation-types.js';
|
|
13
|
+
|
|
14
|
+
const WRITE_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
15
|
+
|
|
16
|
+
/** Detect resume context loss (Path 1): session expired on --resume */
|
|
17
|
+
export function detectResumeContextLoss(
|
|
18
|
+
result: HeadlessRunResult,
|
|
19
|
+
state: RetryLoopState,
|
|
20
|
+
useResume: boolean,
|
|
21
|
+
maxRetries: number,
|
|
22
|
+
nativeTimeouts: number,
|
|
23
|
+
verbose: boolean,
|
|
24
|
+
): void {
|
|
25
|
+
if (!useResume || state.checkpointRef.value || state.retryNumber >= maxRetries || nativeTimeouts > 0) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!result.assistantResponse || result.assistantResponse.trim().length === 0) {
|
|
29
|
+
state.contextLost = true;
|
|
30
|
+
if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: null/empty response');
|
|
31
|
+
} else if (result.resumeBufferedOutput !== undefined) {
|
|
32
|
+
state.contextLost = true;
|
|
33
|
+
if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: buffer never flushed (no thinking/tools)');
|
|
34
|
+
} else if (
|
|
35
|
+
(!result.toolUseHistory || result.toolUseHistory.length === 0) &&
|
|
36
|
+
!result.thinkingOutput &&
|
|
37
|
+
result.assistantResponse.length < 500
|
|
38
|
+
) {
|
|
39
|
+
state.contextLost = true;
|
|
40
|
+
if (verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: no tools, no thinking, short response');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Detect native timeout context loss (Path 2): tool timeouts caused confusion */
|
|
45
|
+
export async function detectNativeTimeoutContextLoss(
|
|
46
|
+
result: HeadlessRunResult,
|
|
47
|
+
state: RetryLoopState,
|
|
48
|
+
maxRetries: number,
|
|
49
|
+
nativeTimeouts: number,
|
|
50
|
+
verbose: boolean,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
if (state.contextLost) return;
|
|
53
|
+
|
|
54
|
+
const { effectiveTimeouts } = computeEffectiveTimeouts(result, nativeTimeouts);
|
|
55
|
+
if (effectiveTimeouts === 0 || !result.assistantResponse || state.checkpointRef.value || state.retryNumber >= maxRetries) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const contextLossCtx: ContextLossContext = {
|
|
60
|
+
assistantResponse: result.assistantResponse,
|
|
61
|
+
effectiveTimeouts,
|
|
62
|
+
nativeTimeoutCount: nativeTimeouts,
|
|
63
|
+
successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
|
|
64
|
+
thinkingOutputLength: result.thinkingOutput?.length ?? 0,
|
|
65
|
+
hasSuccessfulWrite: result.toolUseHistory?.some(
|
|
66
|
+
t => WRITE_TOOL_NAMES.has(t.toolName) && t.result !== undefined && !t.isError
|
|
67
|
+
) ?? false,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
71
|
+
const verdict = await assessContextLoss(contextLossCtx, claudeCmd, verbose);
|
|
72
|
+
state.contextLost = verdict.contextLost;
|
|
73
|
+
if (verbose) {
|
|
74
|
+
hlog(`[CONTEXT-RECOVERY] Haiku verdict: ${state.contextLost ? 'LOST' : 'OK'} — ${verdict.reason}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function computeEffectiveTimeouts(result: HeadlessRunResult, nativeTimeouts: number): { effectiveTimeouts: number } {
|
|
79
|
+
const succeededIds = new Set<string>();
|
|
80
|
+
const allIds = new Set<string>();
|
|
81
|
+
for (const t of result.toolUseHistory ?? []) {
|
|
82
|
+
allIds.add(t.toolId);
|
|
83
|
+
if (t.result !== undefined) succeededIds.add(t.toolId);
|
|
84
|
+
}
|
|
85
|
+
const toolsWithoutResult = [...allIds].filter(id => !succeededIds.has(id)).length;
|
|
86
|
+
return { effectiveTimeouts: Math.max(nativeTimeouts, toolsWithoutResult) };
|
|
87
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detects when a claimed-complete run is actually unfinished (hit
|
|
6
|
+
* max_tokens, abandoned mid-task, or Haiku says the end_turn response is
|
|
7
|
+
* a stop short of the goal) and triggers a continuation retry.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AnalyticsEvents, trackEvent } from '../../services/analytics.js';
|
|
11
|
+
import { hlog } from '../headless/headless-logger.js';
|
|
12
|
+
import { extractFinalTextBlock, isResponseAbandoned } from '../headless/retry-strategies.js';
|
|
13
|
+
import { assessPrematureCompletion } from '../headless/stall-assessor.js';
|
|
14
|
+
import type { HeadlessRunResult, RetryLoopState } from '../improvisation-types.js';
|
|
15
|
+
import type { RetryCallbacks, RetrySessionState } from './retry-types.js';
|
|
16
|
+
|
|
17
|
+
/** Guard checks for premature completion */
|
|
18
|
+
function isPrematureCompletionCandidate(
|
|
19
|
+
result: HeadlessRunResult,
|
|
20
|
+
state: RetryLoopState,
|
|
21
|
+
maxRetries: number,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (!result.completed || result.signalName || state.retryNumber >= maxRetries) return false;
|
|
24
|
+
if (state.checkpointRef.value || state.contextLost) return false;
|
|
25
|
+
if (!result.claudeSessionId || !result.stopReason) return false;
|
|
26
|
+
return result.stopReason === 'max_tokens' || result.stopReason === 'end_turn';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Use Haiku to assess whether an end_turn response is genuinely complete */
|
|
30
|
+
async function assessEndTurnCompletion(result: HeadlessRunResult, verbose: boolean): Promise<boolean> {
|
|
31
|
+
if (!result.assistantResponse) return false;
|
|
32
|
+
|
|
33
|
+
const successfulToolCalls = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
34
|
+
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
35
|
+
const verdict = await assessPrematureCompletion({
|
|
36
|
+
responseTail: extractFinalTextBlock(result.assistantResponse, 800),
|
|
37
|
+
successfulToolCalls,
|
|
38
|
+
hasThinking: !!result.thinkingOutput,
|
|
39
|
+
responseLength: result.assistantResponse.length,
|
|
40
|
+
}, claudeCmd, verbose);
|
|
41
|
+
|
|
42
|
+
if (verbose) {
|
|
43
|
+
hlog(`[PREMATURE-COMPLETION] Haiku verdict: ${verdict.isIncomplete ? 'INCOMPLETE' : 'COMPLETE'} — ${verdict.reason}`);
|
|
44
|
+
}
|
|
45
|
+
return verdict.isIncomplete;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Apply premature completion retry */
|
|
49
|
+
function applyPrematureCompletionRetry(
|
|
50
|
+
result: HeadlessRunResult,
|
|
51
|
+
state: RetryLoopState,
|
|
52
|
+
session: RetrySessionState,
|
|
53
|
+
maxRetries: number,
|
|
54
|
+
stopReason: string,
|
|
55
|
+
isMaxTokens: boolean,
|
|
56
|
+
callbacks: RetryCallbacks,
|
|
57
|
+
): void {
|
|
58
|
+
state.retryNumber++;
|
|
59
|
+
const reason = isMaxTokens ? 'Output limit reached' : 'Task appears unfinished (AI assessment)';
|
|
60
|
+
|
|
61
|
+
state.retryLog.push({
|
|
62
|
+
retryNumber: state.retryNumber,
|
|
63
|
+
path: 'PrematureCompletion',
|
|
64
|
+
reason,
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
callbacks.emit('onAutoRetry', {
|
|
69
|
+
retryNumber: state.retryNumber,
|
|
70
|
+
maxRetries,
|
|
71
|
+
toolName: `PrematureCompletion(${stopReason})`,
|
|
72
|
+
completedCount: result.toolUseHistory?.length ?? 0,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
|
|
76
|
+
retry_number: state.retryNumber,
|
|
77
|
+
hung_tool: `premature_completion:${stopReason}`,
|
|
78
|
+
completed_tools: result.toolUseHistory?.length ?? 0,
|
|
79
|
+
resume_attempted: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
callbacks.queueOutput(
|
|
83
|
+
`\n${reason} — resuming session (retry ${state.retryNumber}/${maxRetries}).\n`
|
|
84
|
+
);
|
|
85
|
+
callbacks.flushOutputQueue();
|
|
86
|
+
|
|
87
|
+
state.contextRecoverySessionId = result.claudeSessionId;
|
|
88
|
+
session.claudeSessionId = result.claudeSessionId;
|
|
89
|
+
state.currentPrompt = 'continue';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Detect and retry premature completion. Returns true if loop should continue. */
|
|
93
|
+
export async function shouldRetryPrematureCompletion(
|
|
94
|
+
result: HeadlessRunResult,
|
|
95
|
+
state: RetryLoopState,
|
|
96
|
+
session: RetrySessionState,
|
|
97
|
+
maxRetries: number,
|
|
98
|
+
callbacks: RetryCallbacks,
|
|
99
|
+
): Promise<boolean> {
|
|
100
|
+
if (!isPrematureCompletionCandidate(result, state, maxRetries)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stopReason = result.stopReason!;
|
|
105
|
+
const isMaxTokens = stopReason === 'max_tokens';
|
|
106
|
+
const abandoned = isResponseAbandoned(result);
|
|
107
|
+
const isIncomplete = isMaxTokens || abandoned || await assessEndTurnCompletion(result, session.options.verbose);
|
|
108
|
+
|
|
109
|
+
if (!isIncomplete) return false;
|
|
110
|
+
|
|
111
|
+
applyPrematureCompletionRetry(result, state, session, maxRetries, stopReason, isMaxTokens, callbacks);
|
|
112
|
+
return true;
|
|
113
|
+
}
|