mstro-app 0.2.0 → 0.3.1
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/PRIVACY.md +126 -0
- package/README.md +24 -23
- package/bin/commands/login.js +79 -49
- package/bin/mstro.js +305 -39
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +137 -30
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +2 -2
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts +6 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +59 -4
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +20 -1
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +30 -24
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +20 -2
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +30 -3
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +224 -31
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +6 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +53 -14
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +70 -7
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts +3 -3
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/server.js +3 -2
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/services/analytics.d.ts +2 -2
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js +13 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/files.js +7 -7
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/pathUtils.js +1 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/platform.d.ts +2 -2
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +13 -1
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sentry.d.ts +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +12 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +81 -6
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-utils.d.ts +4 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/file-utils.js +27 -8
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-handlers.d.ts +36 -0
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-handlers.js +797 -0
- package/dist/server/services/websocket/git-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.js +299 -0
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
- package/dist/server/services/websocket/handler-context.d.ts +32 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
- package/dist/server/services/websocket/handler-context.js +4 -0
- package/dist/server/services/websocket/handler-context.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +27 -359
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +68 -2329
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/index.d.ts +1 -1
- package/dist/server/services/websocket/index.d.ts.map +1 -1
- package/dist/server/services/websocket/index.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +10 -0
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/session-handlers.js +508 -0
- package/dist/server/services/websocket/session-handlers.js.map +1 -0
- package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/settings-handlers.js +125 -0
- package/dist/server/services/websocket/settings-handlers.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-handlers.js +131 -0
- package/dist/server/services/websocket/tab-handlers.js.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.js +220 -0
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +63 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.d.ts +22 -2
- package/dist/server/utils/agent-manager.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.js +2 -2
- package/dist/server/utils/agent-manager.js.map +1 -1
- package/dist/server/utils/port-manager.js.map +1 -1
- package/hooks/bouncer.sh +17 -3
- package/package.json +7 -3
- package/server/README.md +176 -159
- package/server/cli/headless/claude-invoker.ts +172 -43
- package/server/cli/headless/mcp-config.ts +8 -8
- package/server/cli/headless/runner.ts +57 -4
- package/server/cli/headless/stall-assessor.ts +25 -0
- package/server/cli/headless/tool-watchdog.ts +33 -25
- package/server/cli/headless/types.ts +11 -2
- package/server/cli/improvisation-session-manager.ts +285 -37
- package/server/index.ts +15 -13
- package/server/mcp/README.md +59 -67
- package/server/mcp/bouncer-cli.ts +73 -20
- package/server/mcp/bouncer-integration.ts +99 -16
- package/server/mcp/security-audit.ts +4 -4
- package/server/mcp/server.ts +6 -5
- package/server/services/analytics.ts +16 -4
- package/server/services/files.ts +13 -13
- package/server/services/pathUtils.ts +2 -2
- package/server/services/platform.ts +17 -6
- package/server/services/sentry.ts +1 -1
- package/server/services/terminal/pty-manager.ts +88 -11
- package/server/services/websocket/file-explorer-handlers.ts +587 -0
- package/server/services/websocket/file-utils.ts +28 -9
- package/server/services/websocket/git-handlers.ts +924 -0
- package/server/services/websocket/git-pr-handlers.ts +363 -0
- package/server/services/websocket/git-worktree-handlers.ts +403 -0
- package/server/services/websocket/handler-context.ts +44 -0
- package/server/services/websocket/handler.ts +85 -2680
- package/server/services/websocket/index.ts +1 -1
- package/server/services/websocket/session-handlers.ts +575 -0
- package/server/services/websocket/settings-handlers.ts +150 -0
- package/server/services/websocket/tab-handlers.ts +150 -0
- package/server/services/websocket/terminal-handlers.ts +277 -0
- package/server/services/websocket/types.ts +137 -0
- package/server/utils/agent-manager.ts +6 -6
- package/server/utils/port-manager.ts +1 -1
- package/bin/release.sh +0 -110
- package/server/services/platform.test.ts +0 -1304
- package/server/services/websocket/handler.test.ts +0 -20
|
@@ -56,41 +56,44 @@ export const DEFAULT_TOOL_TIMEOUT_PROFILES: Record<string, ToolTimeoutProfile> =
|
|
|
56
56
|
useAdaptive: false,
|
|
57
57
|
useHaikuTiebreaker: true,
|
|
58
58
|
},
|
|
59
|
-
// Local filesystem tools —
|
|
59
|
+
// Local filesystem tools — these go through Claude Code's streaming stdio protocol,
|
|
60
|
+
// NOT direct filesystem I/O. Large files/results can take 30-60s+ to stream.
|
|
61
|
+
// Read/Grep have bimodal distributions (tiny vs huge responses) that defeat EMA,
|
|
62
|
+
// so adaptive is disabled for them. Floors are generous to prevent premature kills.
|
|
60
63
|
Read: {
|
|
61
|
-
coldStartMs:
|
|
62
|
-
floorMs:
|
|
63
|
-
ceilingMs: 300_000, // 5 min ceiling (large files, slow mounts)
|
|
64
|
-
useAdaptive:
|
|
65
|
-
useHaikuTiebreaker:
|
|
64
|
+
coldStartMs: 120_000, // 2 min — large files stream slowly through stdio protocol
|
|
65
|
+
floorMs: 60_000, // 1 min minimum — prevents EMA-driven premature kills
|
|
66
|
+
ceilingMs: 300_000, // 5 min ceiling (very large files, slow mounts)
|
|
67
|
+
useAdaptive: false, // bimodal: 1-line file vs 2000-line file defeats EMA
|
|
68
|
+
useHaikuTiebreaker: true, // safety net: assess before killing the whole process
|
|
66
69
|
},
|
|
67
70
|
Grep: {
|
|
68
|
-
coldStartMs:
|
|
69
|
-
floorMs:
|
|
70
|
-
ceilingMs: 300_000,
|
|
71
|
-
useAdaptive:
|
|
72
|
-
useHaikuTiebreaker:
|
|
71
|
+
coldStartMs: 120_000, // 2 min — broad searches return large result sets
|
|
72
|
+
floorMs: 60_000, // 1 min minimum
|
|
73
|
+
ceilingMs: 300_000, // 5 min ceiling
|
|
74
|
+
useAdaptive: false, // bimodal: single-file vs codebase-wide search
|
|
75
|
+
useHaikuTiebreaker: true, // safety net before killing
|
|
73
76
|
},
|
|
74
77
|
Glob: {
|
|
75
|
-
coldStartMs:
|
|
76
|
-
floorMs:
|
|
77
|
-
ceilingMs:
|
|
78
|
+
coldStartMs: 60_000, // 1 min — pattern matching can be slow on large trees
|
|
79
|
+
floorMs: 30_000, // 30s minimum
|
|
80
|
+
ceilingMs: 180_000, // 3 min ceiling
|
|
78
81
|
useAdaptive: true,
|
|
79
|
-
useHaikuTiebreaker:
|
|
82
|
+
useHaikuTiebreaker: true,
|
|
80
83
|
},
|
|
81
84
|
Edit: {
|
|
82
|
-
coldStartMs:
|
|
83
|
-
floorMs:
|
|
84
|
-
ceilingMs:
|
|
85
|
+
coldStartMs: 60_000, // 1 min — edits go through streaming protocol too
|
|
86
|
+
floorMs: 30_000, // 30s minimum
|
|
87
|
+
ceilingMs: 180_000, // 3 min ceiling
|
|
85
88
|
useAdaptive: true,
|
|
86
|
-
useHaikuTiebreaker:
|
|
89
|
+
useHaikuTiebreaker: true,
|
|
87
90
|
},
|
|
88
91
|
Write: {
|
|
89
|
-
coldStartMs:
|
|
90
|
-
floorMs:
|
|
91
|
-
ceilingMs:
|
|
92
|
+
coldStartMs: 60_000, // 1 min
|
|
93
|
+
floorMs: 30_000, // 30s minimum
|
|
94
|
+
ceilingMs: 180_000, // 3 min ceiling
|
|
92
95
|
useAdaptive: true,
|
|
93
|
-
useHaikuTiebreaker:
|
|
96
|
+
useHaikuTiebreaker: true,
|
|
94
97
|
},
|
|
95
98
|
};
|
|
96
99
|
|
|
@@ -106,7 +109,9 @@ export interface ToolWatchdogOptions {
|
|
|
106
109
|
profiles?: Record<string, Partial<ToolTimeoutProfile>>;
|
|
107
110
|
verbose?: boolean;
|
|
108
111
|
/** Called before killing — if returns 'extend', reschedule with extensionMs */
|
|
109
|
-
onTiebreaker?: (toolName: string, toolInput: Record<string, unknown>, elapsedMs: number) => Promise<{ action: 'extend' | 'kill'; extensionMs: number; reason: string }>;
|
|
112
|
+
onTiebreaker?: (toolName: string, toolInput: Record<string, unknown>, elapsedMs: number, tokenSilenceMs?: number) => Promise<{ action: 'extend' | 'kill'; extensionMs: number; reason: string }>;
|
|
113
|
+
/** Returns ms since last token activity. Called at tiebreaker time for fresh data. */
|
|
114
|
+
getTokenSilenceMs?: () => number | undefined;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
interface ActiveWatch {
|
|
@@ -124,10 +129,12 @@ export class ToolWatchdog {
|
|
|
124
129
|
private activeWatches: Map<string, ActiveWatch> = new Map();
|
|
125
130
|
private verbose: boolean;
|
|
126
131
|
private onTiebreaker?: ToolWatchdogOptions['onTiebreaker'];
|
|
132
|
+
private getTokenSilenceMs?: () => number | undefined;
|
|
127
133
|
|
|
128
134
|
constructor(options: ToolWatchdogOptions = {}) {
|
|
129
135
|
this.verbose = options.verbose ?? false;
|
|
130
136
|
this.onTiebreaker = options.onTiebreaker;
|
|
137
|
+
this.getTokenSilenceMs = options.getTokenSilenceMs;
|
|
131
138
|
|
|
132
139
|
// Merge user profiles with defaults
|
|
133
140
|
this.profiles = { ...DEFAULT_TOOL_TIMEOUT_PROFILES };
|
|
@@ -254,7 +261,8 @@ export class ToolWatchdog {
|
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
try {
|
|
257
|
-
const
|
|
264
|
+
const tokenSilenceMs = this.getTokenSilenceMs?.();
|
|
265
|
+
const verdict = await this.onTiebreaker!(toolName, toolInput, elapsedMs, tokenSilenceMs);
|
|
258
266
|
|
|
259
267
|
if (verdict.action === 'extend') {
|
|
260
268
|
if (this.verbose) {
|
|
@@ -19,7 +19,7 @@ export interface ToolUseEvent {
|
|
|
19
19
|
toolId?: string;
|
|
20
20
|
index?: number;
|
|
21
21
|
partialJson?: string;
|
|
22
|
-
completeInput?:
|
|
22
|
+
completeInput?: Record<string, unknown>;
|
|
23
23
|
result?: string;
|
|
24
24
|
isError?: boolean;
|
|
25
25
|
}
|
|
@@ -97,6 +97,8 @@ export interface HeadlessConfig {
|
|
|
97
97
|
outputCallback?: (text: string) => void;
|
|
98
98
|
thinkingCallback?: (text: string) => void;
|
|
99
99
|
toolUseCallback?: (event: ToolUseEvent) => void;
|
|
100
|
+
/** Called with cumulative API token counts as they arrive from the stream */
|
|
101
|
+
tokenUsageCallback?: (usage: { inputTokens: number; outputTokens: number }) => void;
|
|
100
102
|
directPrompt?: string;
|
|
101
103
|
promptContext?: PromptContext;
|
|
102
104
|
continueSession?: boolean;
|
|
@@ -137,6 +139,8 @@ export interface SessionResult {
|
|
|
137
139
|
totalTokens: number;
|
|
138
140
|
sessionId: string;
|
|
139
141
|
error?: string;
|
|
142
|
+
/** Signal name if Claude process was killed (e.g., 'SIGTERM', 'SIGKILL') */
|
|
143
|
+
signalName?: string;
|
|
140
144
|
conflicts?: Array<{
|
|
141
145
|
filePath: string;
|
|
142
146
|
modifiedBy: string[];
|
|
@@ -180,6 +184,8 @@ export interface ExecutionResult {
|
|
|
180
184
|
output: string;
|
|
181
185
|
error?: string;
|
|
182
186
|
exitCode: number;
|
|
187
|
+
/** Signal name if process was killed (e.g., 'SIGTERM', 'SIGKILL') */
|
|
188
|
+
signalName?: string;
|
|
183
189
|
assistantResponse?: string;
|
|
184
190
|
thinkingOutput?: string;
|
|
185
191
|
toolUseHistory?: ToolUseAccumulator[];
|
|
@@ -192,13 +198,16 @@ export interface ExecutionResult {
|
|
|
192
198
|
/** Assistant text buffered during resume assessment — held back until thinking/tool activity
|
|
193
199
|
* confirms Claude has context. Undefined when not in resume mode or buffer was flushed. */
|
|
194
200
|
resumeBufferedOutput?: string;
|
|
201
|
+
/** Actual API token usage from Claude Code stream events (summed across all turns) */
|
|
202
|
+
apiTokenUsage?: { inputTokens: number; outputTokens: number };
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
/** Resolved config with all defaults applied */
|
|
198
|
-
export type ResolvedHeadlessConfig = Omit<Required<HeadlessConfig>, 'outputCallback' | 'thinkingCallback' | 'toolUseCallback' | 'continueSession' | 'claudeSessionId' | 'imageAttachments' | 'model' | 'toolTimeoutProfiles' | 'onToolTimeout' | 'sandboxed'> & {
|
|
206
|
+
export type ResolvedHeadlessConfig = Omit<Required<HeadlessConfig>, 'outputCallback' | 'thinkingCallback' | 'toolUseCallback' | 'tokenUsageCallback' | 'continueSession' | 'claudeSessionId' | 'imageAttachments' | 'model' | 'toolTimeoutProfiles' | 'onToolTimeout' | 'sandboxed'> & {
|
|
199
207
|
outputCallback?: (text: string) => void;
|
|
200
208
|
thinkingCallback?: (text: string) => void;
|
|
201
209
|
toolUseCallback?: (event: ToolUseEvent) => void;
|
|
210
|
+
tokenUsageCallback?: (usage: { inputTokens: number; outputTokens: number }) => void;
|
|
202
211
|
continueSession?: boolean;
|
|
203
212
|
claudeSessionId?: string;
|
|
204
213
|
imageAttachments?: ImageAttachment[];
|
|
@@ -59,6 +59,7 @@ export interface MovementRecord {
|
|
|
59
59
|
toolUseHistory?: ToolUseRecord[];// Tool invocations + results
|
|
60
60
|
errorOutput?: string; // Any errors
|
|
61
61
|
durationMs?: number; // Execution duration in milliseconds
|
|
62
|
+
retryLog?: RetryLogEntry[]; // Auto-retry events during execution
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export interface SessionHistory {
|
|
@@ -71,6 +72,15 @@ export interface SessionHistory {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
/** Entry in the retry log for debugging recovery paths */
|
|
76
|
+
interface RetryLogEntry {
|
|
77
|
+
retryNumber: number;
|
|
78
|
+
path: string;
|
|
79
|
+
reason: string;
|
|
80
|
+
timestamp: number;
|
|
81
|
+
durationMs?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
/** Mutable state for the retry loop in executePrompt */
|
|
75
85
|
interface RetryLoopState {
|
|
76
86
|
currentPrompt: string;
|
|
@@ -83,6 +93,7 @@ interface RetryLoopState {
|
|
|
83
93
|
lastWatchdogCheckpoint: ExecutionCheckpoint | null;
|
|
84
94
|
timedOutTools: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>;
|
|
85
95
|
bestResult: HeadlessRunResult | null;
|
|
96
|
+
retryLog: RetryLogEntry[];
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
/** Type alias for HeadlessRunner execution result */
|
|
@@ -103,7 +114,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
103
114
|
private currentRunner: HeadlessRunner | null = null;
|
|
104
115
|
private options: ImprovisationOptions;
|
|
105
116
|
private pendingApproval?: {
|
|
106
|
-
plan:
|
|
117
|
+
plan: unknown;
|
|
107
118
|
resolve: (approved: boolean) => void;
|
|
108
119
|
};
|
|
109
120
|
private outputQueue: Array<{ text: string; timestamp: number }> = [];
|
|
@@ -118,7 +129,9 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
118
129
|
/** Timestamp when current execution started (for accurate elapsed time across reconnects) */
|
|
119
130
|
private _executionStartTimestamp: number | undefined;
|
|
120
131
|
/** Buffered events during current execution, for replay on reconnect */
|
|
121
|
-
private executionEventLog: Array<{ type: string; data:
|
|
132
|
+
private executionEventLog: Array<{ type: string; data: unknown; timestamp: number }> = [];
|
|
133
|
+
/** Set by cancel() to signal the retry loop to exit */
|
|
134
|
+
private _cancelled: boolean = false;
|
|
122
135
|
|
|
123
136
|
/**
|
|
124
137
|
* Resume from a historical session.
|
|
@@ -304,9 +317,10 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
304
317
|
* Each tab maintains its own claudeSessionId for proper isolation
|
|
305
318
|
* Supports file attachments: text files prepended to prompt, images via stream-json multimodal
|
|
306
319
|
*/
|
|
307
|
-
async executePrompt(userPrompt: string, attachments?: FileAttachment[], options?: { sandboxed?: boolean }): Promise<MovementRecord> {
|
|
320
|
+
async executePrompt(userPrompt: string, attachments?: FileAttachment[], options?: { sandboxed?: boolean; workingDir?: string }): Promise<MovementRecord> {
|
|
308
321
|
const _execStart = Date.now();
|
|
309
322
|
this._isExecuting = true;
|
|
323
|
+
this._cancelled = false;
|
|
310
324
|
this._executionStartTimestamp = _execStart;
|
|
311
325
|
this.executionEventLog = [];
|
|
312
326
|
|
|
@@ -341,38 +355,24 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
341
355
|
lastWatchdogCheckpoint: null,
|
|
342
356
|
timedOutTools: [],
|
|
343
357
|
bestResult: null,
|
|
358
|
+
retryLog: [],
|
|
344
359
|
};
|
|
345
360
|
|
|
346
|
-
|
|
347
|
-
let result: HeadlessRunResult;
|
|
348
|
-
|
|
349
|
-
// eslint-disable-next-line no-constant-condition
|
|
350
|
-
while (true) {
|
|
351
|
-
this.resetIterationState(state);
|
|
361
|
+
let result = await this.runRetryLoop(state, sequenceNumber, promptWithAttachments, imageAttachments, options?.sandboxed, options?.workingDir);
|
|
352
362
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
this.
|
|
356
|
-
result = await runner.run();
|
|
357
|
-
this.currentRunner = null;
|
|
358
|
-
|
|
359
|
-
this.updateBestResult(state, result);
|
|
360
|
-
const nativeTimeouts = result.nativeTimeoutCount ?? 0;
|
|
361
|
-
this.detectResumeContextLoss(result, state, useResume, maxRetries, nativeTimeouts);
|
|
362
|
-
await this.detectNativeTimeoutContextLoss(result, state, maxRetries, nativeTimeouts);
|
|
363
|
-
this.flushPostTimeoutOutput(result, state);
|
|
364
|
-
|
|
365
|
-
if (this.shouldRetryContextLoss(result, state, useResume, nativeTimeouts, maxRetries, promptWithAttachments)) continue;
|
|
366
|
-
if (this.applyToolTimeoutRetry(state, maxRetries, promptWithAttachments)) continue;
|
|
367
|
-
break;
|
|
363
|
+
// If cancelled, emit a minimal movement and return early
|
|
364
|
+
if (this._cancelled) {
|
|
365
|
+
return this.handleCancelledExecution(result, userPrompt, sequenceNumber, _execStart);
|
|
368
366
|
}
|
|
369
367
|
|
|
370
368
|
if (state.contextLost) this.claudeSessionId = undefined;
|
|
371
|
-
result
|
|
369
|
+
// result is guaranteed assigned here: the loop always runs at least once (if _cancelled was
|
|
370
|
+
// true before the loop, we returned in the block above; otherwise runner.run() assigned it).
|
|
371
|
+
result = await this.selectBestResult(state, result!, userPrompt);
|
|
372
372
|
this.captureSessionAndSurfaceErrors(result);
|
|
373
373
|
this.isFirstPrompt = false;
|
|
374
374
|
|
|
375
|
-
const movement = this.buildMovementRecord(result, userPrompt, sequenceNumber, _execStart);
|
|
375
|
+
const movement = this.buildMovementRecord(result, userPrompt, sequenceNumber, _execStart, state.retryLog);
|
|
376
376
|
this.handleConflicts(result);
|
|
377
377
|
this.persistMovement(movement);
|
|
378
378
|
|
|
@@ -383,19 +383,20 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
383
383
|
this.emitMovementComplete(movement, result, _execStart, sequenceNumber);
|
|
384
384
|
return movement;
|
|
385
385
|
|
|
386
|
-
} catch (error:
|
|
386
|
+
} catch (error: unknown) {
|
|
387
387
|
this._isExecuting = false;
|
|
388
388
|
this._executionStartTimestamp = undefined;
|
|
389
389
|
this.executionEventLog = [];
|
|
390
390
|
this.currentRunner = null;
|
|
391
391
|
this.emit('onMovementError', error);
|
|
392
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
392
393
|
trackEvent(AnalyticsEvents.IMPROVISE_MOVEMENT_ERROR, {
|
|
393
|
-
error_message:
|
|
394
|
+
error_message: errorMessage.slice(0, 200),
|
|
394
395
|
sequence_number: this.history.movements.length + 1,
|
|
395
396
|
duration_ms: Date.now() - _execStart,
|
|
396
397
|
model: this.options.model || 'default',
|
|
397
398
|
});
|
|
398
|
-
this.queueOutput(`\n❌ Error: ${
|
|
399
|
+
this.queueOutput(`\n❌ Error: ${errorMessage}\n`);
|
|
399
400
|
this.flushOutputQueue();
|
|
400
401
|
throw error;
|
|
401
402
|
} finally {
|
|
@@ -405,6 +406,85 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
405
406
|
|
|
406
407
|
// ========== Extracted helpers for executePrompt ==========
|
|
407
408
|
|
|
409
|
+
private handleCancelledExecution(
|
|
410
|
+
result: HeadlessRunResult | undefined,
|
|
411
|
+
userPrompt: string,
|
|
412
|
+
sequenceNumber: number,
|
|
413
|
+
execStart: number,
|
|
414
|
+
): MovementRecord {
|
|
415
|
+
this._isExecuting = false;
|
|
416
|
+
this._executionStartTimestamp = undefined;
|
|
417
|
+
this.executionEventLog = [];
|
|
418
|
+
this.currentRunner = null;
|
|
419
|
+
|
|
420
|
+
const cancelledMovement: MovementRecord = {
|
|
421
|
+
id: `prompt-${sequenceNumber}`,
|
|
422
|
+
sequenceNumber,
|
|
423
|
+
userPrompt,
|
|
424
|
+
timestamp: new Date().toISOString(),
|
|
425
|
+
tokensUsed: result ? result.totalTokens : 0,
|
|
426
|
+
summary: '',
|
|
427
|
+
filesModified: [],
|
|
428
|
+
assistantResponse: result?.assistantResponse,
|
|
429
|
+
thinkingOutput: result?.thinkingOutput,
|
|
430
|
+
toolUseHistory: result?.toolUseHistory?.map(t => ({
|
|
431
|
+
toolName: t.toolName,
|
|
432
|
+
toolId: t.toolId,
|
|
433
|
+
toolInput: t.toolInput,
|
|
434
|
+
result: t.result,
|
|
435
|
+
})),
|
|
436
|
+
errorOutput: 'Execution cancelled by user',
|
|
437
|
+
durationMs: Date.now() - execStart,
|
|
438
|
+
};
|
|
439
|
+
this.persistMovement(cancelledMovement);
|
|
440
|
+
const fallbackResult = {
|
|
441
|
+
completed: false, needsHandoff: false, totalTokens: 0, sessionId: '',
|
|
442
|
+
output: '', exitCode: 1, signalName: 'SIGTERM',
|
|
443
|
+
} as HeadlessRunResult;
|
|
444
|
+
this.emitMovementComplete(cancelledMovement, result ?? fallbackResult, execStart, sequenceNumber);
|
|
445
|
+
return cancelledMovement;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private async runRetryLoop(
|
|
449
|
+
state: RetryLoopState,
|
|
450
|
+
sequenceNumber: number,
|
|
451
|
+
promptWithAttachments: string,
|
|
452
|
+
imageAttachments: FileAttachment[] | undefined,
|
|
453
|
+
sandboxed: boolean | undefined,
|
|
454
|
+
workingDirOverride: string | undefined,
|
|
455
|
+
): Promise<HeadlessRunResult | undefined> {
|
|
456
|
+
const maxRetries = 3;
|
|
457
|
+
let result: HeadlessRunResult | undefined;
|
|
458
|
+
|
|
459
|
+
// eslint-disable-next-line no-constant-condition
|
|
460
|
+
while (true) {
|
|
461
|
+
if (this._cancelled) break;
|
|
462
|
+
this.resetIterationState(state);
|
|
463
|
+
|
|
464
|
+
const { useResume, resumeSessionId } = this.determineResumeStrategy(state);
|
|
465
|
+
const runner = this.createExecutionRunner(state, sequenceNumber, useResume, resumeSessionId, imageAttachments, sandboxed, workingDirOverride);
|
|
466
|
+
this.currentRunner = runner;
|
|
467
|
+
result = await runner.run();
|
|
468
|
+
this.currentRunner = null;
|
|
469
|
+
|
|
470
|
+
if (this._cancelled) break;
|
|
471
|
+
|
|
472
|
+
this.updateBestResult(state, result);
|
|
473
|
+
const nativeTimeouts = result.nativeTimeoutCount ?? 0;
|
|
474
|
+
this.detectResumeContextLoss(result, state, useResume, maxRetries, nativeTimeouts);
|
|
475
|
+
await this.detectNativeTimeoutContextLoss(result, state, maxRetries, nativeTimeouts);
|
|
476
|
+
this.flushPostTimeoutOutput(result, state);
|
|
477
|
+
|
|
478
|
+
// Signal crashes checked first: they use --resume (lighter), and context loss
|
|
479
|
+
// recovery would clear the session ID, preventing future --resume attempts.
|
|
480
|
+
if (this.shouldRetrySignalCrash(result, state, maxRetries, promptWithAttachments)) continue;
|
|
481
|
+
if (this.shouldRetryContextLoss(result, state, useResume, nativeTimeouts, maxRetries, promptWithAttachments)) continue;
|
|
482
|
+
if (this.applyToolTimeoutRetry(state, maxRetries, promptWithAttachments)) continue;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
|
|
408
488
|
/** Prepare prompt with attachments and limit image count */
|
|
409
489
|
private preparePromptAndAttachments(
|
|
410
490
|
userPrompt: string,
|
|
@@ -455,9 +535,10 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
455
535
|
resumeSessionId: string | undefined,
|
|
456
536
|
imageAttachments: FileAttachment[] | undefined,
|
|
457
537
|
sandboxed: boolean | undefined,
|
|
538
|
+
workingDirOverride?: string,
|
|
458
539
|
): HeadlessRunner {
|
|
459
540
|
return new HeadlessRunner({
|
|
460
|
-
workingDir: this.options.workingDir,
|
|
541
|
+
workingDir: workingDirOverride || this.options.workingDir,
|
|
461
542
|
tokenBudgetThreshold: this.options.tokenBudgetThreshold,
|
|
462
543
|
maxSessions: this.options.maxSessions,
|
|
463
544
|
verbose: this.options.verbose,
|
|
@@ -482,6 +563,9 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
482
563
|
this.emit('onToolUse', event);
|
|
483
564
|
this.flushOutputQueue();
|
|
484
565
|
},
|
|
566
|
+
tokenUsageCallback: (usage) => {
|
|
567
|
+
this.emit('onTokenUsage', usage);
|
|
568
|
+
},
|
|
485
569
|
directPrompt: state.currentPrompt,
|
|
486
570
|
imageAttachments,
|
|
487
571
|
promptContext: (state.retryNumber === 0 && this.isResumedSession && this.isFirstPrompt)
|
|
@@ -544,7 +628,15 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
544
628
|
): Promise<void> {
|
|
545
629
|
if (state.contextLost) return;
|
|
546
630
|
|
|
547
|
-
|
|
631
|
+
// Deduplicate by toolId: if a toolId has at least one entry with a result,
|
|
632
|
+
// its orphaned duplicates are Claude Code internal retries, not actual timeouts.
|
|
633
|
+
const succeededIds = new Set<string>();
|
|
634
|
+
const allIds = new Set<string>();
|
|
635
|
+
for (const t of result.toolUseHistory ?? []) {
|
|
636
|
+
allIds.add(t.toolId);
|
|
637
|
+
if (t.result !== undefined) succeededIds.add(t.toolId);
|
|
638
|
+
}
|
|
639
|
+
const toolsWithoutResult = [...allIds].filter(id => !succeededIds.has(id)).length;
|
|
548
640
|
const effectiveTimeouts = Math.max(nativeTimeouts, toolsWithoutResult);
|
|
549
641
|
|
|
550
642
|
if (effectiveTimeouts === 0 || !result.assistantResponse || state.checkpointRef.value || state.retryNumber >= maxRetries) {
|
|
@@ -593,6 +685,13 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
593
685
|
}
|
|
594
686
|
this.accumulateToolResults(result, state);
|
|
595
687
|
state.retryNumber++;
|
|
688
|
+
const path = (useResume && nativeTimeouts === 0) ? 'InterMovementRecovery' : 'NativeTimeoutRecovery';
|
|
689
|
+
state.retryLog.push({
|
|
690
|
+
retryNumber: state.retryNumber,
|
|
691
|
+
path,
|
|
692
|
+
reason: `Context lost (${nativeTimeouts} timeouts, ${state.accumulatedToolResults.length} tools preserved)`,
|
|
693
|
+
timestamp: Date.now(),
|
|
694
|
+
});
|
|
596
695
|
if (useResume && nativeTimeouts === 0) {
|
|
597
696
|
this.applyInterMovementRecovery(state, promptWithAttachments);
|
|
598
697
|
} else {
|
|
@@ -601,7 +700,11 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
601
700
|
return true;
|
|
602
701
|
}
|
|
603
702
|
|
|
604
|
-
/** Accumulate completed tool results from a run into the retry state
|
|
703
|
+
/** Accumulate completed tool results from a run into the retry state.
|
|
704
|
+
* Caps at MAX_ACCUMULATED_RESULTS to prevent recovery prompts from exceeding context limits.
|
|
705
|
+
* When the cap is reached, older results are evicted (FIFO) to make room for newer ones. */
|
|
706
|
+
private static readonly MAX_ACCUMULATED_RESULTS = 50;
|
|
707
|
+
|
|
605
708
|
private accumulateToolResults(result: HeadlessRunResult, state: RetryLoopState): void {
|
|
606
709
|
if (!result.toolUseHistory) return;
|
|
607
710
|
for (const t of result.toolUseHistory) {
|
|
@@ -616,11 +719,18 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
616
719
|
});
|
|
617
720
|
}
|
|
618
721
|
}
|
|
722
|
+
// Evict oldest results if over the cap
|
|
723
|
+
const cap = ImprovisationSessionManager.MAX_ACCUMULATED_RESULTS;
|
|
724
|
+
if (state.accumulatedToolResults.length > cap) {
|
|
725
|
+
state.accumulatedToolResults = state.accumulatedToolResults.slice(-cap);
|
|
726
|
+
}
|
|
619
727
|
}
|
|
620
728
|
|
|
621
729
|
/** Handle inter-movement context loss recovery (resume session expired) */
|
|
622
730
|
private applyInterMovementRecovery(state: RetryLoopState, promptWithAttachments: string): void {
|
|
623
|
-
|
|
731
|
+
// Preserve session ID so --resume remains available on subsequent retries.
|
|
732
|
+
// The fresh recovery prompt will be used, but if this attempt also fails,
|
|
733
|
+
// the next retry can still try --resume via shouldRetrySignalCrash.
|
|
624
734
|
const historicalResults = this.extractHistoricalToolResults();
|
|
625
735
|
const allResults = [...historicalResults, ...state.accumulatedToolResults];
|
|
626
736
|
|
|
@@ -668,7 +778,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
668
778
|
);
|
|
669
779
|
this.flushOutputQueue();
|
|
670
780
|
state.freshRecoveryMode = true;
|
|
671
|
-
state.currentPrompt = this.buildFreshRecoveryPrompt(promptWithAttachments, state.accumulatedToolResults);
|
|
781
|
+
state.currentPrompt = this.buildFreshRecoveryPrompt(promptWithAttachments, state.accumulatedToolResults, state.timedOutTools);
|
|
672
782
|
}
|
|
673
783
|
}
|
|
674
784
|
|
|
@@ -692,6 +802,12 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
692
802
|
});
|
|
693
803
|
|
|
694
804
|
const canResumeSession = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
|
|
805
|
+
state.retryLog.push({
|
|
806
|
+
retryNumber: state.retryNumber,
|
|
807
|
+
path: 'ToolTimeout',
|
|
808
|
+
reason: `${cp.hungTool.toolName} timed out after ${cp.hungTool.timeoutMs}ms, ${cp.completedTools.length} tools completed, ${canResumeSession ? 'resuming' : 'fresh start'}`,
|
|
809
|
+
timestamp: Date.now(),
|
|
810
|
+
});
|
|
695
811
|
this.emit('onAutoRetry', {
|
|
696
812
|
retryNumber: state.retryNumber,
|
|
697
813
|
maxRetries,
|
|
@@ -721,6 +837,127 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
721
837
|
return true;
|
|
722
838
|
}
|
|
723
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Detect and retry after a signal crash (e.g., SIGTERM exit code 143).
|
|
842
|
+
* When the Claude process is killed externally (OOM, system signal, internal timeout
|
|
843
|
+
* that bypasses our watchdog), no existing recovery path catches it because contextLost
|
|
844
|
+
* is never set and no checkpoint is created. This adds a dedicated recovery path.
|
|
845
|
+
*/
|
|
846
|
+
private shouldRetrySignalCrash(
|
|
847
|
+
result: HeadlessRunResult,
|
|
848
|
+
state: RetryLoopState,
|
|
849
|
+
maxRetries: number,
|
|
850
|
+
promptWithAttachments: string,
|
|
851
|
+
): boolean {
|
|
852
|
+
// Only trigger for signal-killed processes (exit code 128+) that weren't already
|
|
853
|
+
// handled by context-loss or tool-timeout recovery paths.
|
|
854
|
+
// Must have an actual signal name — regular errors (e.g., auth failures, exit code 1)
|
|
855
|
+
// should NOT be retried as signal crashes.
|
|
856
|
+
const isSignalCrash = !!result.signalName;
|
|
857
|
+
const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
|
|
858
|
+
if ((!isSignalCrash && !exitCodeSignal) || state.retryNumber >= maxRetries) {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
// Don't re-trigger if tool timeout watchdog already handled this iteration
|
|
862
|
+
// (contextLost is NOT checked here — signal crash takes priority over context loss
|
|
863
|
+
// because it uses --resume which is lighter and avoids re-sending accumulated results)
|
|
864
|
+
if (state.checkpointRef.value) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
this.accumulateToolResults(result, state);
|
|
869
|
+
state.retryNumber++;
|
|
870
|
+
|
|
871
|
+
const completedCount = state.accumulatedToolResults.length;
|
|
872
|
+
const signalInfo = result.signalName || 'unknown signal';
|
|
873
|
+
const useResume = !!result.claudeSessionId && state.retryNumber === 1;
|
|
874
|
+
|
|
875
|
+
state.retryLog.push({
|
|
876
|
+
retryNumber: state.retryNumber,
|
|
877
|
+
path: 'SignalCrash',
|
|
878
|
+
reason: `Process killed (${signalInfo}), ${completedCount} tools preserved, ${useResume ? 'resuming' : 'fresh start'}`,
|
|
879
|
+
timestamp: Date.now(),
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
this.emit('onAutoRetry', {
|
|
883
|
+
retryNumber: state.retryNumber,
|
|
884
|
+
maxRetries,
|
|
885
|
+
toolName: `SignalCrash(${signalInfo})`,
|
|
886
|
+
completedCount,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
|
|
890
|
+
retry_number: state.retryNumber,
|
|
891
|
+
hung_tool: `signal_crash:${signalInfo}`,
|
|
892
|
+
completed_tools: completedCount,
|
|
893
|
+
resume_attempted: useResume,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// If we have a session ID, try resuming first (preserves full context)
|
|
897
|
+
if (useResume) {
|
|
898
|
+
this.queueOutput(
|
|
899
|
+
`\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
|
|
900
|
+
);
|
|
901
|
+
this.flushOutputQueue();
|
|
902
|
+
state.contextRecoverySessionId = result.claudeSessionId;
|
|
903
|
+
this.claudeSessionId = result.claudeSessionId;
|
|
904
|
+
state.currentPrompt = this.buildSignalCrashRecoveryPrompt(promptWithAttachments, true);
|
|
905
|
+
} else {
|
|
906
|
+
// Fresh start with accumulated results injected
|
|
907
|
+
this.queueOutput(
|
|
908
|
+
`\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — restarting with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
|
|
909
|
+
);
|
|
910
|
+
this.flushOutputQueue();
|
|
911
|
+
state.freshRecoveryMode = true;
|
|
912
|
+
const allResults = [...this.extractHistoricalToolResults(), ...state.accumulatedToolResults];
|
|
913
|
+
state.currentPrompt = this.buildSignalCrashRecoveryPrompt(promptWithAttachments, false, allResults);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Build a recovery prompt after signal crash */
|
|
920
|
+
private buildSignalCrashRecoveryPrompt(
|
|
921
|
+
originalPrompt: string,
|
|
922
|
+
isResume: boolean,
|
|
923
|
+
toolResults?: ToolUseRecord[],
|
|
924
|
+
): string {
|
|
925
|
+
const parts: string[] = [];
|
|
926
|
+
|
|
927
|
+
if (isResume) {
|
|
928
|
+
parts.push('Your previous execution was interrupted by a system signal (the process was killed externally).');
|
|
929
|
+
parts.push('Your full conversation history is preserved — including all successful tool results.');
|
|
930
|
+
parts.push('');
|
|
931
|
+
parts.push('Review your conversation history above and continue from where you left off.');
|
|
932
|
+
} else {
|
|
933
|
+
parts.push('## AUTOMATIC RETRY — Previous Execution Interrupted');
|
|
934
|
+
parts.push('');
|
|
935
|
+
parts.push('The previous execution was interrupted by a system signal (process killed).');
|
|
936
|
+
if (toolResults && toolResults.length > 0) {
|
|
937
|
+
parts.push(`${toolResults.length} tool results were preserved from prior work.`);
|
|
938
|
+
parts.push('');
|
|
939
|
+
parts.push('### Preserved results:');
|
|
940
|
+
for (const t of toolResults.slice(-20)) {
|
|
941
|
+
const inputSummary = JSON.stringify(t.toolInput).slice(0, 120);
|
|
942
|
+
const resultPreview = (t.result ?? '').slice(0, 200);
|
|
943
|
+
parts.push(`- **${t.toolName}**(${inputSummary}): ${resultPreview}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
parts.push('');
|
|
949
|
+
parts.push('### Original task:');
|
|
950
|
+
parts.push(originalPrompt);
|
|
951
|
+
parts.push('');
|
|
952
|
+
parts.push('INSTRUCTIONS:');
|
|
953
|
+
parts.push('1. Use the results above -- do not re-fetch content you already have');
|
|
954
|
+
parts.push('2. Continue from where you left off');
|
|
955
|
+
parts.push('3. Prefer multiple small, focused tool calls over single large ones');
|
|
956
|
+
parts.push('4. Do NOT spawn Task subagents — do work inline to avoid further interruptions');
|
|
957
|
+
|
|
958
|
+
return parts.join('\n');
|
|
959
|
+
}
|
|
960
|
+
|
|
724
961
|
/** Select the best result across retries using Haiku assessment */
|
|
725
962
|
private async selectBestResult(
|
|
726
963
|
state: RetryLoopState,
|
|
@@ -798,6 +1035,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
798
1035
|
userPrompt: string,
|
|
799
1036
|
sequenceNumber: number,
|
|
800
1037
|
execStart: number,
|
|
1038
|
+
retryLog?: RetryLogEntry[],
|
|
801
1039
|
): MovementRecord {
|
|
802
1040
|
return {
|
|
803
1041
|
id: `prompt-${sequenceNumber}`,
|
|
@@ -819,6 +1057,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
819
1057
|
})),
|
|
820
1058
|
errorOutput: result.error,
|
|
821
1059
|
durationMs: Date.now() - execStart,
|
|
1060
|
+
retryLog: retryLog && retryLog.length > 0 ? retryLog : undefined,
|
|
822
1061
|
};
|
|
823
1062
|
}
|
|
824
1063
|
|
|
@@ -1023,7 +1262,11 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1023
1262
|
* Injects all accumulated tool results from previous attempts so Claude can continue
|
|
1024
1263
|
* the task without re-fetching data it already gathered.
|
|
1025
1264
|
*/
|
|
1026
|
-
private buildFreshRecoveryPrompt(
|
|
1265
|
+
private buildFreshRecoveryPrompt(
|
|
1266
|
+
originalPrompt: string,
|
|
1267
|
+
toolResults: ToolUseRecord[],
|
|
1268
|
+
timedOutTools?: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>,
|
|
1269
|
+
): string {
|
|
1027
1270
|
const parts: string[] = [
|
|
1028
1271
|
'## CONTINUING LONG-RUNNING TASK',
|
|
1029
1272
|
'',
|
|
@@ -1032,6 +1275,10 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1032
1275
|
'',
|
|
1033
1276
|
];
|
|
1034
1277
|
|
|
1278
|
+
if (timedOutTools && timedOutTools.length > 0) {
|
|
1279
|
+
parts.push(...this.formatTimedOutTools(timedOutTools), '');
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1035
1282
|
parts.push(...this.formatToolResults(toolResults));
|
|
1036
1283
|
|
|
1037
1284
|
parts.push('### Original task:');
|
|
@@ -1225,6 +1472,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1225
1472
|
* Cancel current execution
|
|
1226
1473
|
*/
|
|
1227
1474
|
cancel(): void {
|
|
1475
|
+
this._cancelled = true;
|
|
1228
1476
|
if (this.currentRunner) {
|
|
1229
1477
|
this.currentRunner.cleanup();
|
|
1230
1478
|
this.currentRunner = null;
|
|
@@ -1263,7 +1511,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1263
1511
|
* Request user approval for a plan
|
|
1264
1512
|
* Returns a promise that resolves when the user approves/rejects
|
|
1265
1513
|
*/
|
|
1266
|
-
async requestApproval(plan:
|
|
1514
|
+
async requestApproval(plan: unknown): Promise<boolean> {
|
|
1267
1515
|
return new Promise((resolve) => {
|
|
1268
1516
|
this.pendingApproval = { plan, resolve };
|
|
1269
1517
|
this.emit('onApprovalRequired', plan);
|
|
@@ -1312,7 +1560,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1312
1560
|
* Get buffered execution events for replay on reconnect.
|
|
1313
1561
|
* Only meaningful while isExecuting is true.
|
|
1314
1562
|
*/
|
|
1315
|
-
getExecutionEventLog(): Array<{ type: string; data:
|
|
1563
|
+
getExecutionEventLog(): Array<{ type: string; data: unknown; timestamp: number }> {
|
|
1316
1564
|
return this.executionEventLog;
|
|
1317
1565
|
}
|
|
1318
1566
|
|