mstro-app 0.4.3 → 0.4.4
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/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-process.js +140 -0
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
- package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +10 -807
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
- package/dist/server/cli/headless/haiku-assessments.js +281 -0
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
- package/dist/server/cli/headless/headless-logger.d.ts +3 -2
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +28 -5
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
- package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
- package/dist/server/cli/headless/native-timeout-detector.js +99 -0
- package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +65 -457
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/improvisation-attachments.d.ts +21 -0
- package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
- package/dist/server/cli/improvisation-attachments.js +116 -0
- package/dist/server/cli/improvisation-attachments.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +52 -0
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
- package/dist/server/cli/improvisation-retry.js +434 -0
- package/dist/server/cli/improvisation-retry.js.map +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +117 -1079
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +86 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -0
- package/dist/server/cli/improvisation-types.js +10 -0
- package/dist/server/cli/improvisation-types.js.map +1 -0
- package/dist/server/cli/prompt-builders.d.ts +68 -0
- package/dist/server/cli/prompt-builders.d.ts.map +1 -0
- package/dist/server/cli/prompt-builders.js +312 -0
- package/dist/server/cli/prompt-builders.js.map +1 -0
- package/dist/server/index.js +33 -212
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-haiku.js +152 -0
- package/dist/server/mcp/bouncer-haiku.js.map +1 -0
- package/dist/server/mcp/bouncer-integration.d.ts +3 -4
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +50 -196
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-analysis.d.ts +38 -0
- package/dist/server/mcp/security-analysis.d.ts.map +1 -0
- package/dist/server/mcp/security-analysis.js +183 -0
- package/dist/server/mcp/security-analysis.js.map +1 -0
- package/dist/server/mcp/security-audit.d.ts +1 -1
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts +1 -25
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +55 -260
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/server-setup.d.ts +22 -0
- package/dist/server/server-setup.d.ts.map +1 -0
- package/dist/server/server-setup.js +101 -0
- package/dist/server/server-setup.js.map +1 -0
- package/dist/server/services/file-explorer-ops.d.ts +24 -0
- package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
- package/dist/server/services/file-explorer-ops.js +211 -0
- package/dist/server/services/file-explorer-ops.js.map +1 -0
- package/dist/server/services/files.d.ts +2 -85
- package/dist/server/services/files.d.ts.map +1 -1
- package/dist/server/services/files.js +7 -427
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +2 -1
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts +20 -0
- package/dist/server/services/plan/parser-core.d.ts.map +1 -0
- package/dist/server/services/plan/parser-core.js +350 -0
- package/dist/server/services/plan/parser-core.js.map +1 -0
- package/dist/server/services/plan/parser-migration.d.ts +5 -0
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
- package/dist/server/services/plan/parser-migration.js +124 -0
- package/dist/server/services/plan/parser-migration.js.map +1 -0
- package/dist/server/services/plan/parser.d.ts +0 -8
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +50 -569
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts +2 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +2 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +2 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts +24 -0
- package/dist/server/services/platform-credentials.d.ts.map +1 -0
- package/dist/server/services/platform-credentials.js +68 -0
- package/dist/server/services/platform-credentials.js.map +1 -0
- package/dist/server/services/platform.d.ts +1 -31
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +10 -119
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +7 -97
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +53 -266
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/terminal/pty-utils.d.ts +57 -0
- package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
- package/dist/server/services/terminal/pty-utils.js +141 -0
- package/dist/server/services/terminal/pty-utils.js.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.js +153 -0
- package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-search-handlers.js +238 -0
- package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-utils.js +3 -3
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-branch-handlers.js +110 -0
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.js +123 -0
- package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-handlers.d.ts +2 -31
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +35 -541
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-log-handlers.js +128 -0
- package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +13 -53
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-tag-handlers.js +76 -0
- package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-utils.d.ts +43 -0
- package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
- package/dist/server/services/websocket/git-utils.js +201 -0
- package/dist/server/services/websocket/git-utils.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +37 -126
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-board-handlers.js +218 -0
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +6 -925
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-helpers.js +199 -0
- package/dist/server/services/websocket/plan-helpers.js.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
- package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-complexity.js +262 -0
- package/dist/server/services/websocket/quality-complexity.js.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.js +140 -0
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +34 -346
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-linting.d.ts +9 -0
- package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-linting.js +178 -0
- package/dist/server/services/websocket/quality-linting.js.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.js +206 -0
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-service.d.ts +3 -51
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +9 -651
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +23 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-tools.js +208 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -0
- package/dist/server/services/websocket/quality-types.d.ts +59 -0
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-types.js +101 -0
- package/dist/server/services/websocket/quality-types.js.map +1 -0
- package/dist/server/services/websocket/session-handlers.d.ts +3 -4
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +3 -378
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.d.ts +4 -0
- package/dist/server/services/websocket/session-history.d.ts.map +1 -0
- package/dist/server/services/websocket/session-history.js +208 -0
- package/dist/server/services/websocket/session-history.js.map +1 -0
- package/dist/server/services/websocket/session-initialization.d.ts +5 -0
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
- package/dist/server/services/websocket/session-initialization.js +163 -0
- package/dist/server/services/websocket/session-initialization.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +12 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-process.ts +204 -0
- package/server/cli/headless/claude-invoker-stall.ts +164 -0
- package/server/cli/headless/claude-invoker-stream.ts +353 -0
- package/server/cli/headless/claude-invoker-tools.ts +187 -0
- package/server/cli/headless/claude-invoker.ts +15 -1096
- package/server/cli/headless/haiku-assessments.ts +365 -0
- package/server/cli/headless/headless-logger.ts +26 -5
- package/server/cli/headless/native-timeout-detector.ts +117 -0
- package/server/cli/headless/stall-assessor.ts +65 -618
- package/server/cli/improvisation-attachments.ts +148 -0
- package/server/cli/improvisation-retry.ts +602 -0
- package/server/cli/improvisation-session-manager.ts +140 -1349
- package/server/cli/improvisation-types.ts +98 -0
- package/server/cli/prompt-builders.ts +370 -0
- package/server/index.ts +35 -246
- package/server/mcp/bouncer-haiku.ts +182 -0
- package/server/mcp/bouncer-integration.ts +87 -248
- package/server/mcp/security-analysis.ts +217 -0
- package/server/mcp/security-audit.ts +1 -1
- package/server/mcp/security-patterns.ts +60 -283
- package/server/server-setup.ts +114 -0
- package/server/services/file-explorer-ops.ts +293 -0
- package/server/services/files.ts +20 -532
- package/server/services/plan/composer.ts +2 -1
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/parser-core.ts +406 -0
- package/server/services/plan/parser-migration.ts +128 -0
- package/server/services/plan/parser.ts +52 -620
- package/server/services/plan/review-gate.ts +4 -2
- package/server/services/plan/types.ts +2 -0
- package/server/services/platform-credentials.ts +83 -0
- package/server/services/platform.ts +15 -141
- package/server/services/terminal/pty-manager.ts +66 -313
- package/server/services/terminal/pty-utils.ts +176 -0
- package/server/services/websocket/file-definition-handlers.ts +165 -0
- package/server/services/websocket/file-explorer-handlers.ts +37 -452
- package/server/services/websocket/file-search-handlers.ts +291 -0
- package/server/services/websocket/file-utils.ts +3 -3
- package/server/services/websocket/git-branch-handlers.ts +130 -0
- package/server/services/websocket/git-diff-handlers.ts +140 -0
- package/server/services/websocket/git-handlers.ts +40 -625
- package/server/services/websocket/git-log-handlers.ts +149 -0
- package/server/services/websocket/git-pr-handlers.ts +17 -62
- package/server/services/websocket/git-tag-handlers.ts +91 -0
- package/server/services/websocket/git-utils.ts +230 -0
- package/server/services/websocket/handler.ts +39 -126
- package/server/services/websocket/plan-board-handlers.ts +277 -0
- package/server/services/websocket/plan-execution-handlers.ts +184 -0
- package/server/services/websocket/plan-handlers.ts +8 -1114
- package/server/services/websocket/plan-helpers.ts +215 -0
- package/server/services/websocket/plan-issue-handlers.ts +204 -0
- package/server/services/websocket/plan-sprint-handlers.ts +252 -0
- package/server/services/websocket/quality-complexity.ts +294 -0
- package/server/services/websocket/quality-fix-agent.ts +181 -0
- package/server/services/websocket/quality-handlers.ts +36 -404
- package/server/services/websocket/quality-linting.ts +187 -0
- package/server/services/websocket/quality-review-agent.ts +246 -0
- package/server/services/websocket/quality-service.ts +11 -762
- package/server/services/websocket/quality-tools.ts +209 -0
- package/server/services/websocket/quality-types.ts +169 -0
- package/server/services/websocket/session-handlers.ts +5 -437
- package/server/services/websocket/session-history.ts +222 -0
- package/server/services/websocket/session-initialization.ts +209 -0
- package/server/services/websocket/types.ts +17 -0
|
@@ -9,110 +9,32 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { AnalyticsEvents, trackEvent } from '../services/analytics.js';
|
|
15
|
-
import { herror
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// File attachment for multimodal prompts (images)
|
|
32
|
-
export interface FileAttachment {
|
|
33
|
-
fileName: string; // Display name (e.g., "screenshot.png")
|
|
34
|
-
filePath: string; // Full path on disk (for context)
|
|
35
|
-
content: string; // Base64 for images
|
|
36
|
-
isImage: boolean; // True for image files
|
|
37
|
-
mimeType?: string; // MIME type for images (e.g., "image/png")
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ToolUseRecord {
|
|
41
|
-
toolName: string;
|
|
42
|
-
toolId: string;
|
|
43
|
-
toolInput: Record<string, unknown>;
|
|
44
|
-
result?: string;
|
|
45
|
-
isError?: boolean;
|
|
46
|
-
duration?: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface MovementRecord {
|
|
50
|
-
id: string;
|
|
51
|
-
sequenceNumber: number;
|
|
52
|
-
userPrompt: string;
|
|
53
|
-
timestamp: string;
|
|
54
|
-
tokensUsed: number;
|
|
55
|
-
summary: string;
|
|
56
|
-
filesModified: string[];
|
|
57
|
-
// NEW: Persisted output fields
|
|
58
|
-
assistantResponse?: string; // Claude's text output
|
|
59
|
-
thinkingOutput?: string; // Extended thinking
|
|
60
|
-
toolUseHistory?: ToolUseRecord[];// Tool invocations + results
|
|
61
|
-
errorOutput?: string; // Any errors
|
|
62
|
-
durationMs?: number; // Execution duration in milliseconds
|
|
63
|
-
retryLog?: RetryLogEntry[]; // Auto-retry events during execution
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface SessionHistory {
|
|
67
|
-
sessionId: string;
|
|
68
|
-
startedAt: string;
|
|
69
|
-
lastActivityAt: string;
|
|
70
|
-
totalTokens: number;
|
|
71
|
-
movements: MovementRecord[];
|
|
72
|
-
claudeSessionId?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/** Entry in the retry log for debugging recovery paths */
|
|
77
|
-
interface RetryLogEntry {
|
|
78
|
-
retryNumber: number;
|
|
79
|
-
path: string;
|
|
80
|
-
reason: string;
|
|
81
|
-
timestamp: number;
|
|
82
|
-
durationMs?: number;
|
|
83
|
-
}
|
|
15
|
+
import { herror } from './headless/headless-logger.js';
|
|
16
|
+
import { cleanupAttachments, preparePromptAndAttachments } from './improvisation-attachments.js';
|
|
17
|
+
import type { RetryCallbacks, RetrySessionState } from './improvisation-retry.js';
|
|
18
|
+
import {applyToolTimeoutRetry,
|
|
19
|
+
createExecutionRunner,detectNativeTimeoutContextLoss, detectResumeContextLoss,
|
|
20
|
+
determineResumeStrategy,
|
|
21
|
+
selectBestResult,
|
|
22
|
+
shouldRetryContextLoss,
|
|
23
|
+
shouldRetryPrematureCompletion,
|
|
24
|
+
shouldRetrySignalCrash
|
|
25
|
+
} from './improvisation-retry.js';
|
|
26
|
+
import type { FileAttachment, HeadlessRunResult, ImprovisationOptions, MovementRecord, RetryLoopState, SessionHistory } from './improvisation-types.js';
|
|
27
|
+
import { scoreRunResult } from './improvisation-types.js';
|
|
28
|
+
|
|
29
|
+
// Re-export types consumed by other packages
|
|
30
|
+
export type { FileAttachment, ImprovisationOptions, MovementRecord, SessionHistory, ToolUseRecord } from './improvisation-types.js';
|
|
84
31
|
|
|
85
|
-
/** Mutable state for the retry loop in executePrompt */
|
|
86
|
-
interface RetryLoopState {
|
|
87
|
-
currentPrompt: string;
|
|
88
|
-
retryNumber: number;
|
|
89
|
-
checkpointRef: { value: ExecutionCheckpoint | null };
|
|
90
|
-
contextRecoverySessionId: string | undefined;
|
|
91
|
-
freshRecoveryMode: boolean;
|
|
92
|
-
accumulatedToolResults: ToolUseRecord[];
|
|
93
|
-
contextLost: boolean;
|
|
94
|
-
lastWatchdogCheckpoint: ExecutionCheckpoint | null;
|
|
95
|
-
timedOutTools: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>;
|
|
96
|
-
bestResult: HeadlessRunResult | null;
|
|
97
|
-
retryLog: RetryLogEntry[];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Type alias for HeadlessRunner execution result */
|
|
101
|
-
type HeadlessRunResult = Awaited<ReturnType<HeadlessRunner['run']>>;
|
|
102
|
-
|
|
103
|
-
/** Score a run result for best-result tracking (higher = more productive) */
|
|
104
|
-
function scoreRunResult(r: HeadlessRunResult): number {
|
|
105
|
-
const toolCount = r.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
106
|
-
const responseLen = Math.min((r.assistantResponse?.length ?? 0) / 50, 100);
|
|
107
|
-
const hasThinking = r.thinkingOutput ? 20 : 0;
|
|
108
|
-
return toolCount * 10 + responseLen + hasThinking;
|
|
109
|
-
}
|
|
110
32
|
export class ImprovisationSessionManager extends EventEmitter {
|
|
111
33
|
private sessionId: string;
|
|
112
34
|
private improviseDir: string;
|
|
113
35
|
private historyPath: string;
|
|
114
36
|
private history: SessionHistory;
|
|
115
|
-
private currentRunner: HeadlessRunner | null = null;
|
|
37
|
+
private currentRunner: import('./headless/index.js').HeadlessRunner | null = null;
|
|
116
38
|
private options: ImprovisationOptions;
|
|
117
39
|
private pendingApproval?: {
|
|
118
40
|
plan: unknown;
|
|
@@ -120,35 +42,21 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
120
42
|
};
|
|
121
43
|
private outputQueue: Array<{ text: string; timestamp: number }> = [];
|
|
122
44
|
private queueTimer: NodeJS.Timeout | null = null;
|
|
123
|
-
private isFirstPrompt: boolean = true;
|
|
124
|
-
private claudeSessionId: string | undefined;
|
|
125
|
-
private isResumedSession: boolean = false;
|
|
45
|
+
private isFirstPrompt: boolean = true;
|
|
46
|
+
private claudeSessionId: string | undefined;
|
|
47
|
+
private isResumedSession: boolean = false;
|
|
126
48
|
accumulatedKnowledge: string = '';
|
|
127
49
|
|
|
128
|
-
/** Whether a prompt is currently executing */
|
|
129
50
|
private _isExecuting: boolean = false;
|
|
130
|
-
/** Timestamp when current execution started (for accurate elapsed time across reconnects) */
|
|
131
51
|
private _executionStartTimestamp: number | undefined;
|
|
132
|
-
/** Buffered events during current execution, for replay on reconnect */
|
|
133
52
|
private executionEventLog: Array<{ type: string; data: unknown; timestamp: number }> = [];
|
|
134
|
-
/** Set by cancel() to signal the retry loop to exit */
|
|
135
53
|
private _cancelled: boolean = false;
|
|
136
|
-
/** True when cancel() has already emitted movementComplete (prevents double-emit) */
|
|
137
54
|
private _cancelCompleteEmitted: boolean = false;
|
|
138
|
-
/** Current execution's user prompt (for cancel to build movement record) */
|
|
139
55
|
private _currentUserPrompt: string = '';
|
|
140
|
-
/** Current execution's sequence number (for cancel to build movement record) */
|
|
141
56
|
private _currentSequenceNumber: number = 0;
|
|
142
57
|
|
|
143
|
-
/**
|
|
144
|
-
* Resume from a historical session.
|
|
145
|
-
* Creates a new session manager that continues the conversation from a previous session.
|
|
146
|
-
* The first prompt will include context from the historical session.
|
|
147
|
-
*/
|
|
148
58
|
static resumeFromHistory(workingDir: string, historicalSessionId: string, overrides?: Partial<ImprovisationOptions>): ImprovisationSessionManager {
|
|
149
59
|
const historyDir = join(workingDir, '.mstro', 'history');
|
|
150
|
-
|
|
151
|
-
// Extract timestamp from session ID (format: improv-1234567890123 or just 1234567890123)
|
|
152
60
|
const timestamp = historicalSessionId.replace('improv-', '');
|
|
153
61
|
const historyPath = join(historyDir, `${timestamp}.json`);
|
|
154
62
|
|
|
@@ -156,31 +64,21 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
156
64
|
throw new Error(`Historical session not found: ${historicalSessionId}`);
|
|
157
65
|
}
|
|
158
66
|
|
|
159
|
-
// Read the historical session
|
|
160
67
|
const historyData = JSON.parse(readFileSync(historyPath, 'utf-8')) as SessionHistory;
|
|
161
|
-
|
|
162
|
-
// Create a new session manager with the SAME session ID
|
|
163
|
-
// This ensures we continue writing to the same history file
|
|
164
68
|
const manager = new ImprovisationSessionManager({
|
|
165
69
|
workingDir,
|
|
166
70
|
sessionId: historyData.sessionId,
|
|
167
71
|
...overrides,
|
|
168
72
|
});
|
|
169
73
|
|
|
170
|
-
// Load the historical data
|
|
171
74
|
manager.history = historyData;
|
|
172
|
-
|
|
173
|
-
// Build accumulated knowledge from historical movements
|
|
174
75
|
manager.accumulatedKnowledge = historyData.movements
|
|
175
76
|
.filter(m => m.summary)
|
|
176
77
|
.map(m => m.summary)
|
|
177
78
|
.join('\n\n');
|
|
178
79
|
|
|
179
|
-
// Restore Claude session ID if available so we can --resume the actual conversation
|
|
180
|
-
// NOTE: Always mark as resumed session so historical context can be injected as fallback
|
|
181
|
-
// if the Claude CLI session has expired (e.g., client was restarted)
|
|
182
80
|
manager.isResumedSession = true;
|
|
183
|
-
manager.isFirstPrompt = true;
|
|
81
|
+
manager.isFirstPrompt = true;
|
|
184
82
|
if (historyData.claudeSessionId) {
|
|
185
83
|
manager.claudeSessionId = historyData.claudeSessionId;
|
|
186
84
|
}
|
|
@@ -205,132 +103,33 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
205
103
|
this.improviseDir = join(this.options.workingDir, '.mstro', 'history');
|
|
206
104
|
this.historyPath = join(this.improviseDir, `${this.sessionId.replace('improv-', '')}.json`);
|
|
207
105
|
|
|
208
|
-
// Ensure history directory exists
|
|
209
106
|
if (!existsSync(this.improviseDir)) {
|
|
210
107
|
mkdirSync(this.improviseDir, { recursive: true });
|
|
211
108
|
}
|
|
212
109
|
|
|
213
|
-
// Load or initialize history
|
|
214
110
|
this.history = this.loadHistory();
|
|
215
|
-
|
|
216
|
-
// Start output queue processor
|
|
217
111
|
this.startQueueProcessor();
|
|
218
112
|
}
|
|
219
113
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
*/
|
|
114
|
+
// ========== Output Queue ==========
|
|
115
|
+
|
|
223
116
|
private startQueueProcessor(): void {
|
|
224
|
-
this.queueTimer = setInterval(() => {
|
|
225
|
-
this.flushOutputQueue();
|
|
226
|
-
}, 10); // Process queue every 10ms for near-instant output
|
|
117
|
+
this.queueTimer = setInterval(() => { this.flushOutputQueue(); }, 10);
|
|
227
118
|
}
|
|
228
119
|
|
|
229
|
-
/**
|
|
230
|
-
* Queue output for immediate processing
|
|
231
|
-
*/
|
|
232
120
|
private queueOutput(text: string): void {
|
|
233
121
|
this.outputQueue.push({ text, timestamp: Date.now() });
|
|
234
122
|
}
|
|
235
123
|
|
|
236
|
-
/**
|
|
237
|
-
* Flush all queued output immediately
|
|
238
|
-
*/
|
|
239
124
|
private flushOutputQueue(): void {
|
|
240
125
|
while (this.outputQueue.length > 0) {
|
|
241
126
|
const item = this.outputQueue.shift();
|
|
242
|
-
if (item)
|
|
243
|
-
this.emit('onOutput', item.text);
|
|
244
|
-
}
|
|
127
|
+
if (item) this.emit('onOutput', item.text);
|
|
245
128
|
}
|
|
246
129
|
}
|
|
247
130
|
|
|
248
|
-
|
|
249
|
-
* Build prompt with text file attachments prepended and disk path references
|
|
250
|
-
* Format: each text file is shown as @path followed by content in code block
|
|
251
|
-
*/
|
|
252
|
-
private buildPromptWithAttachments(userPrompt: string, attachments?: FileAttachment[], diskPaths?: string[]): string {
|
|
253
|
-
if ((!attachments || attachments.length === 0) && (!diskPaths || diskPaths.length === 0)) {
|
|
254
|
-
return userPrompt;
|
|
255
|
-
}
|
|
131
|
+
// ========== Main Execution ==========
|
|
256
132
|
|
|
257
|
-
const parts: string[] = [];
|
|
258
|
-
|
|
259
|
-
// Filter to text files only (non-images)
|
|
260
|
-
if (attachments) {
|
|
261
|
-
const textFiles = attachments.filter(a => !a.isImage);
|
|
262
|
-
for (const file of textFiles) {
|
|
263
|
-
parts.push(`@${file.filePath}\n\`\`\`\n${file.content}\n\`\`\``);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Add disk path references for all persisted files
|
|
268
|
-
if (diskPaths && diskPaths.length > 0) {
|
|
269
|
-
parts.push(`Attached files saved to disk:\n${diskPaths.map(p => `- ${p}`).join('\n')}`);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (parts.length === 0) {
|
|
273
|
-
return userPrompt;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return `${parts.join('\n\n')}\n\n${userPrompt}`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Write attachments to disk at .mstro/tmp/attachments/{sessionId}/
|
|
281
|
-
* Returns array of absolute file paths for each persisted attachment.
|
|
282
|
-
*/
|
|
283
|
-
private persistAttachments(attachments: FileAttachment[]): string[] {
|
|
284
|
-
if (attachments.length === 0) return [];
|
|
285
|
-
|
|
286
|
-
const attachDir = join(this.options.workingDir, '.mstro', 'tmp', 'attachments', this.sessionId);
|
|
287
|
-
if (!existsSync(attachDir)) {
|
|
288
|
-
mkdirSync(attachDir, { recursive: true });
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const paths: string[] = [];
|
|
292
|
-
for (const attachment of attachments) {
|
|
293
|
-
// Pre-uploaded files are already on disk from chunked upload
|
|
294
|
-
if ((attachment as FileAttachment & { _preUploaded?: boolean })._preUploaded) {
|
|
295
|
-
if (existsSync(attachment.filePath)) {
|
|
296
|
-
paths.push(attachment.filePath);
|
|
297
|
-
}
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const filePath = join(attachDir, attachment.fileName);
|
|
301
|
-
try {
|
|
302
|
-
// All paste content arrives as base64 — decode to binary
|
|
303
|
-
writeFileSync(filePath, Buffer.from(attachment.content, 'base64'));
|
|
304
|
-
paths.push(filePath);
|
|
305
|
-
} catch (err) {
|
|
306
|
-
herror(`Failed to persist attachment ${attachment.fileName}:`, err);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return paths;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Clean up persisted attachments for this session
|
|
315
|
-
*/
|
|
316
|
-
private cleanupAttachments(): void {
|
|
317
|
-
const attachDir = join(this.options.workingDir, '.mstro', 'tmp', 'attachments', this.sessionId);
|
|
318
|
-
if (existsSync(attachDir)) {
|
|
319
|
-
try {
|
|
320
|
-
rmSync(attachDir, { recursive: true, force: true });
|
|
321
|
-
} catch {
|
|
322
|
-
// Ignore cleanup errors
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Execute a user prompt directly (Improvise mode - no score decomposition)
|
|
330
|
-
* Uses persistent Claude sessions via --resume <sessionId> for conversation continuity
|
|
331
|
-
* Each tab maintains its own claudeSessionId for proper isolation
|
|
332
|
-
* Supports file attachments: text files prepended to prompt, images via stream-json multimodal
|
|
333
|
-
*/
|
|
334
133
|
async executePrompt(userPrompt: string, attachments?: FileAttachment[], options?: { sandboxed?: boolean; workingDir?: string }): Promise<MovementRecord> {
|
|
335
134
|
const _execStart = Date.now();
|
|
336
135
|
this._isExecuting = true;
|
|
@@ -360,7 +159,10 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
360
159
|
timestamp: Date.now(),
|
|
361
160
|
});
|
|
362
161
|
|
|
363
|
-
const { prompt: promptWithAttachments, imageAttachments } =
|
|
162
|
+
const { prompt: promptWithAttachments, imageAttachments } = preparePromptAndAttachments(
|
|
163
|
+
userPrompt, attachments, this.options.workingDir, this.sessionId,
|
|
164
|
+
(msg) => { this.queueOutput(msg); this.flushOutputQueue(); },
|
|
165
|
+
);
|
|
364
166
|
const state: RetryLoopState = {
|
|
365
167
|
currentPrompt: promptWithAttachments,
|
|
366
168
|
retryNumber: 0,
|
|
@@ -377,15 +179,12 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
377
179
|
|
|
378
180
|
let result = await this.runRetryLoop(state, sequenceNumber, promptWithAttachments, imageAttachments, options?.sandboxed, options?.workingDir);
|
|
379
181
|
|
|
380
|
-
// If cancelled, emit a minimal movement and return early
|
|
381
182
|
if (this._cancelled) {
|
|
382
183
|
return this.handleCancelledExecution(result, userPrompt, sequenceNumber, _execStart);
|
|
383
184
|
}
|
|
384
185
|
|
|
385
186
|
if (state.contextLost) this.claudeSessionId = undefined;
|
|
386
|
-
|
|
387
|
-
// true before the loop, we returned in the block above; otherwise runner.run() assigned it).
|
|
388
|
-
result = await this.selectBestResult(state, result!, userPrompt);
|
|
187
|
+
result = await selectBestResult(state, result!, userPrompt, this.options.verbose);
|
|
389
188
|
this.captureSessionAndSurfaceErrors(result);
|
|
390
189
|
this.isFirstPrompt = false;
|
|
391
190
|
|
|
@@ -421,52 +220,34 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
421
220
|
}
|
|
422
221
|
}
|
|
423
222
|
|
|
424
|
-
// ==========
|
|
223
|
+
// ========== Retry Loop ==========
|
|
425
224
|
|
|
426
|
-
private
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// If cancel() already emitted movementComplete, just clean up state —
|
|
438
|
-
// don't double-emit or double-persist.
|
|
439
|
-
if (this._cancelCompleteEmitted) {
|
|
440
|
-
const existing = this.history.movements.find(m => m.sequenceNumber === sequenceNumber);
|
|
441
|
-
if (existing) return existing;
|
|
442
|
-
}
|
|
225
|
+
private buildRetryCallbacks(): RetryCallbacks {
|
|
226
|
+
return {
|
|
227
|
+
isCancelled: () => this._cancelled,
|
|
228
|
+
queueOutput: (text) => this.queueOutput(text),
|
|
229
|
+
flushOutputQueue: () => this.flushOutputQueue(),
|
|
230
|
+
emit: (event, ...args) => this.emit(event, ...args),
|
|
231
|
+
addEventLog: (entry) => this.executionEventLog.push(entry),
|
|
232
|
+
setRunner: (runner) => { this.currentRunner = runner; },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
443
235
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
assistantResponse: result?.assistantResponse,
|
|
453
|
-
thinkingOutput: result?.thinkingOutput,
|
|
454
|
-
toolUseHistory: result?.toolUseHistory?.map(t => ({
|
|
455
|
-
toolName: t.toolName,
|
|
456
|
-
toolId: t.toolId,
|
|
457
|
-
toolInput: t.toolInput,
|
|
458
|
-
result: t.result,
|
|
459
|
-
})),
|
|
460
|
-
errorOutput: 'Execution cancelled by user',
|
|
461
|
-
durationMs: Date.now() - execStart,
|
|
236
|
+
private buildRetrySessionState(): RetrySessionState {
|
|
237
|
+
return {
|
|
238
|
+
options: this.options,
|
|
239
|
+
claudeSessionId: this.claudeSessionId,
|
|
240
|
+
isFirstPrompt: this.isFirstPrompt,
|
|
241
|
+
isResumedSession: this.isResumedSession,
|
|
242
|
+
history: this.history,
|
|
243
|
+
executionStartTimestamp: this._executionStartTimestamp,
|
|
462
244
|
};
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return cancelledMovement;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private syncSessionStateBack(session: RetrySessionState): void {
|
|
248
|
+
if (session.claudeSessionId !== this.claudeSessionId) {
|
|
249
|
+
this.claudeSessionId = session.claudeSessionId;
|
|
250
|
+
}
|
|
470
251
|
}
|
|
471
252
|
|
|
472
253
|
private async runRetryLoop(
|
|
@@ -479,709 +260,119 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
479
260
|
): Promise<HeadlessRunResult | undefined> {
|
|
480
261
|
const maxRetries = 3;
|
|
481
262
|
let result: HeadlessRunResult | undefined;
|
|
263
|
+
const callbacks = this.buildRetryCallbacks();
|
|
482
264
|
|
|
483
265
|
// eslint-disable-next-line no-constant-condition
|
|
484
266
|
while (true) {
|
|
485
267
|
if (this._cancelled) break;
|
|
486
|
-
this.
|
|
487
|
-
|
|
488
|
-
const { useResume, resumeSessionId } = this.determineResumeStrategy(state);
|
|
489
|
-
const runner = this.createExecutionRunner(state, sequenceNumber, useResume, resumeSessionId, imageAttachments, sandboxed, workingDirOverride);
|
|
490
|
-
this.currentRunner = runner;
|
|
491
|
-
result = await runner.run();
|
|
492
|
-
this.currentRunner = null;
|
|
493
|
-
|
|
268
|
+
const iteration = await this.executeRetryIteration(state, callbacks, sequenceNumber, imageAttachments, sandboxed, workingDirOverride);
|
|
269
|
+
result = iteration.result;
|
|
494
270
|
if (this._cancelled) break;
|
|
495
|
-
|
|
496
|
-
this.updateBestResult(state, result);
|
|
497
|
-
const nativeTimeouts = result.nativeTimeoutCount ?? 0;
|
|
498
|
-
this.detectResumeContextLoss(result, state, useResume, maxRetries, nativeTimeouts);
|
|
499
|
-
await this.detectNativeTimeoutContextLoss(result, state, maxRetries, nativeTimeouts);
|
|
500
|
-
this.flushPostTimeoutOutput(result, state);
|
|
501
|
-
|
|
502
|
-
// Signal crashes checked first: they use --resume (lighter), and context loss
|
|
503
|
-
// recovery would clear the session ID, preventing future --resume attempts.
|
|
504
|
-
if (this.shouldRetrySignalCrash(result, state, maxRetries, promptWithAttachments)) continue;
|
|
505
|
-
if (this.shouldRetryContextLoss(result, state, useResume, nativeTimeouts, maxRetries, promptWithAttachments)) continue;
|
|
506
|
-
if (this.applyToolTimeoutRetry(state, maxRetries, promptWithAttachments)) continue;
|
|
507
|
-
// Premature completion: model exited normally but task appears incomplete
|
|
508
|
-
if (await this.shouldRetryPrematureCompletion(result, state, maxRetries)) continue;
|
|
271
|
+
if (await this.evaluateRetryStrategies(result, state, iteration.useResume, iteration.nativeTimeouts, maxRetries, promptWithAttachments, callbacks)) continue;
|
|
509
272
|
break;
|
|
510
273
|
}
|
|
511
274
|
return result;
|
|
512
275
|
}
|
|
513
276
|
|
|
514
|
-
/**
|
|
515
|
-
private
|
|
516
|
-
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
|
517
|
-
]);
|
|
518
|
-
|
|
519
|
-
/** Hydrate pre-uploaded images from disk and downgrade unsupported formats */
|
|
520
|
-
private hydrateAndFilterAttachments(attachments: FileAttachment[]): void {
|
|
521
|
-
for (const attachment of attachments) {
|
|
522
|
-
// Pre-uploaded images need their content read from disk
|
|
523
|
-
const preUploaded = (attachment as FileAttachment & { _preUploaded?: boolean })._preUploaded;
|
|
524
|
-
if (preUploaded && attachment.isImage && !attachment.content && existsSync(attachment.filePath)) {
|
|
525
|
-
try {
|
|
526
|
-
attachment.content = readFileSync(attachment.filePath).toString('base64');
|
|
527
|
-
} catch (err) {
|
|
528
|
-
herror(`Failed to read pre-uploaded image ${attachment.filePath}:`, err);
|
|
529
|
-
attachment.isImage = false;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Downgrade unsupported image formats (SVG, BMP, TIFF, ICO, etc.) to text attachments
|
|
534
|
-
if (attachment.isImage) {
|
|
535
|
-
const mime = (attachment.mimeType || '').toLowerCase();
|
|
536
|
-
if (mime && !ImprovisationSessionManager.SUPPORTED_IMAGE_MIMES.has(mime)) {
|
|
537
|
-
attachment.isImage = false;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/** Prepare prompt with attachments and limit image count */
|
|
544
|
-
private preparePromptAndAttachments(
|
|
545
|
-
userPrompt: string,
|
|
546
|
-
attachments: FileAttachment[] | undefined,
|
|
547
|
-
): { prompt: string; imageAttachments: FileAttachment[] | undefined } {
|
|
548
|
-
if (attachments) {
|
|
549
|
-
this.hydrateAndFilterAttachments(attachments);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const diskPaths = attachments ? this.persistAttachments(attachments) : [];
|
|
553
|
-
const prompt = this.buildPromptWithAttachments(userPrompt, attachments, diskPaths);
|
|
554
|
-
|
|
555
|
-
const MAX_IMAGE_ATTACHMENTS = 20;
|
|
556
|
-
// Only include images that have valid content
|
|
557
|
-
const allImages = attachments?.filter(a => a.isImage && a.content);
|
|
558
|
-
let imageAttachments = allImages;
|
|
559
|
-
if (allImages && allImages.length > MAX_IMAGE_ATTACHMENTS) {
|
|
560
|
-
imageAttachments = allImages.slice(-MAX_IMAGE_ATTACHMENTS);
|
|
561
|
-
this.queueOutput(
|
|
562
|
-
`\n[[MSTRO_ERROR:TOO_MANY_IMAGES]] ${allImages.length} images attached, limit is ${MAX_IMAGE_ATTACHMENTS}. Using the ${MAX_IMAGE_ATTACHMENTS} most recent.\n`
|
|
563
|
-
);
|
|
564
|
-
this.flushOutputQueue();
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return { prompt, imageAttachments };
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/** Determine whether to use --resume and which session ID */
|
|
571
|
-
private determineResumeStrategy(state: RetryLoopState): { useResume: boolean; resumeSessionId: string | undefined } {
|
|
572
|
-
if (state.freshRecoveryMode) {
|
|
573
|
-
state.freshRecoveryMode = false;
|
|
574
|
-
return { useResume: false, resumeSessionId: undefined };
|
|
575
|
-
}
|
|
576
|
-
if (state.contextRecoverySessionId) {
|
|
577
|
-
const id = state.contextRecoverySessionId;
|
|
578
|
-
state.contextRecoverySessionId = undefined;
|
|
579
|
-
return { useResume: true, resumeSessionId: id };
|
|
580
|
-
}
|
|
581
|
-
if (state.retryNumber === 0) {
|
|
582
|
-
return { useResume: !this.isFirstPrompt, resumeSessionId: this.claudeSessionId };
|
|
583
|
-
}
|
|
584
|
-
if (state.lastWatchdogCheckpoint?.inProgressTools.length === 0 && state.lastWatchdogCheckpoint.claudeSessionId) {
|
|
585
|
-
return { useResume: true, resumeSessionId: state.lastWatchdogCheckpoint.claudeSessionId };
|
|
586
|
-
}
|
|
587
|
-
return { useResume: false, resumeSessionId: undefined };
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Create HeadlessRunner for one retry iteration */
|
|
591
|
-
private createExecutionRunner(
|
|
277
|
+
/** Run a single iteration: spawn runner, execute, detect context loss */
|
|
278
|
+
private async executeRetryIteration(
|
|
592
279
|
state: RetryLoopState,
|
|
280
|
+
callbacks: RetryCallbacks,
|
|
593
281
|
sequenceNumber: number,
|
|
594
|
-
useResume: boolean,
|
|
595
|
-
resumeSessionId: string | undefined,
|
|
596
282
|
imageAttachments: FileAttachment[] | undefined,
|
|
597
283
|
sandboxed: boolean | undefined,
|
|
598
|
-
workingDirOverride
|
|
599
|
-
):
|
|
600
|
-
return new HeadlessRunner({
|
|
601
|
-
workingDir: workingDirOverride || this.options.workingDir,
|
|
602
|
-
tokenBudgetThreshold: this.options.tokenBudgetThreshold,
|
|
603
|
-
maxSessions: this.options.maxSessions,
|
|
604
|
-
verbose: this.options.verbose,
|
|
605
|
-
noColor: this.options.noColor,
|
|
606
|
-
model: this.options.model,
|
|
607
|
-
improvisationMode: true,
|
|
608
|
-
movementNumber: sequenceNumber,
|
|
609
|
-
continueSession: useResume,
|
|
610
|
-
claudeSessionId: resumeSessionId,
|
|
611
|
-
outputCallback: (text: string) => {
|
|
612
|
-
this.executionEventLog.push({ type: 'output', data: { text, timestamp: Date.now() }, timestamp: Date.now() });
|
|
613
|
-
this.queueOutput(text);
|
|
614
|
-
this.flushOutputQueue();
|
|
615
|
-
},
|
|
616
|
-
thinkingCallback: (text: string) => {
|
|
617
|
-
this.executionEventLog.push({ type: 'thinking', data: { text }, timestamp: Date.now() });
|
|
618
|
-
this.emit('onThinking', text);
|
|
619
|
-
this.flushOutputQueue();
|
|
620
|
-
},
|
|
621
|
-
toolUseCallback: (event) => {
|
|
622
|
-
this.executionEventLog.push({ type: 'toolUse', data: { ...event, timestamp: Date.now() }, timestamp: Date.now() });
|
|
623
|
-
this.emit('onToolUse', event);
|
|
624
|
-
this.flushOutputQueue();
|
|
625
|
-
},
|
|
626
|
-
tokenUsageCallback: (usage) => {
|
|
627
|
-
this.emit('onTokenUsage', usage);
|
|
628
|
-
},
|
|
629
|
-
directPrompt: state.currentPrompt,
|
|
630
|
-
imageAttachments,
|
|
631
|
-
promptContext: (state.retryNumber === 0 && this.isResumedSession && this.isFirstPrompt)
|
|
632
|
-
? { accumulatedKnowledge: this.buildHistoricalContext(), filesModified: [] }
|
|
633
|
-
: undefined,
|
|
634
|
-
onToolTimeout: (checkpoint: ExecutionCheckpoint) => {
|
|
635
|
-
state.checkpointRef.value = checkpoint;
|
|
636
|
-
},
|
|
637
|
-
sandboxed,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/** Save checkpoint and reset per-iteration state before each retry loop pass. */
|
|
642
|
-
private resetIterationState(state: RetryLoopState): void {
|
|
284
|
+
workingDirOverride: string | undefined,
|
|
285
|
+
): Promise<{ result: HeadlessRunResult; useResume: boolean; nativeTimeouts: number }> {
|
|
643
286
|
if (state.checkpointRef.value) state.lastWatchdogCheckpoint = state.checkpointRef.value;
|
|
644
287
|
state.checkpointRef.value = null;
|
|
645
288
|
state.contextLost = false;
|
|
646
|
-
}
|
|
647
289
|
|
|
648
|
-
|
|
649
|
-
|
|
290
|
+
const session = this.buildRetrySessionState();
|
|
291
|
+
const { useResume, resumeSessionId } = determineResumeStrategy(state, session);
|
|
292
|
+
const runner = createExecutionRunner(state, session, callbacks, sequenceNumber, useResume, resumeSessionId, imageAttachments, sandboxed, workingDirOverride);
|
|
293
|
+
this.currentRunner = runner;
|
|
294
|
+
const result = await runner.run();
|
|
295
|
+
this.currentRunner = null;
|
|
296
|
+
|
|
650
297
|
if (!state.bestResult || scoreRunResult(result) > scoreRunResult(state.bestResult)) {
|
|
651
298
|
state.bestResult = result;
|
|
652
299
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
private detectResumeContextLoss(
|
|
657
|
-
result: HeadlessRunResult,
|
|
658
|
-
state: RetryLoopState,
|
|
659
|
-
useResume: boolean,
|
|
660
|
-
maxRetries: number,
|
|
661
|
-
nativeTimeouts: number,
|
|
662
|
-
): void {
|
|
663
|
-
if (!useResume || state.checkpointRef.value || state.retryNumber >= maxRetries || nativeTimeouts > 0) {
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (!result.assistantResponse || result.assistantResponse.trim().length === 0) {
|
|
667
|
-
state.contextLost = true;
|
|
668
|
-
if (this.options.verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: null/empty response');
|
|
669
|
-
} else if (result.resumeBufferedOutput !== undefined) {
|
|
670
|
-
state.contextLost = true;
|
|
671
|
-
if (this.options.verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: buffer never flushed (no thinking/tools)');
|
|
672
|
-
} else if (
|
|
673
|
-
(!result.toolUseHistory || result.toolUseHistory.length === 0) &&
|
|
674
|
-
!result.thinkingOutput &&
|
|
675
|
-
result.assistantResponse.length < 500
|
|
676
|
-
) {
|
|
677
|
-
state.contextLost = true;
|
|
678
|
-
if (this.options.verbose) hlog('[CONTEXT-RECOVERY] Resume context loss: no tools, no thinking, short response');
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/** Detect native timeout context loss (Path 2): tool timeouts caused confusion */
|
|
683
|
-
private async detectNativeTimeoutContextLoss(
|
|
684
|
-
result: HeadlessRunResult,
|
|
685
|
-
state: RetryLoopState,
|
|
686
|
-
maxRetries: number,
|
|
687
|
-
nativeTimeouts: number,
|
|
688
|
-
): Promise<void> {
|
|
689
|
-
if (state.contextLost) return;
|
|
690
|
-
|
|
691
|
-
// Deduplicate by toolId: if a toolId has at least one entry with a result,
|
|
692
|
-
// its orphaned duplicates are Claude Code internal retries, not actual timeouts.
|
|
693
|
-
const succeededIds = new Set<string>();
|
|
694
|
-
const allIds = new Set<string>();
|
|
695
|
-
for (const t of result.toolUseHistory ?? []) {
|
|
696
|
-
allIds.add(t.toolId);
|
|
697
|
-
if (t.result !== undefined) succeededIds.add(t.toolId);
|
|
698
|
-
}
|
|
699
|
-
const toolsWithoutResult = [...allIds].filter(id => !succeededIds.has(id)).length;
|
|
700
|
-
const effectiveTimeouts = Math.max(nativeTimeouts, toolsWithoutResult);
|
|
701
|
-
|
|
702
|
-
if (effectiveTimeouts === 0 || !result.assistantResponse || state.checkpointRef.value || state.retryNumber >= maxRetries) {
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const writeToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
707
|
-
const contextLossCtx: ContextLossContext = {
|
|
708
|
-
assistantResponse: result.assistantResponse,
|
|
709
|
-
effectiveTimeouts,
|
|
710
|
-
nativeTimeoutCount: nativeTimeouts,
|
|
711
|
-
successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
|
|
712
|
-
thinkingOutputLength: result.thinkingOutput?.length ?? 0,
|
|
713
|
-
hasSuccessfulWrite: result.toolUseHistory?.some(
|
|
714
|
-
t => writeToolNames.has(t.toolName) && t.result !== undefined && !t.isError
|
|
715
|
-
) ?? false,
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
719
|
-
const verdict = await assessContextLoss(contextLossCtx, claudeCmd, this.options.verbose);
|
|
720
|
-
state.contextLost = verdict.contextLost;
|
|
721
|
-
if (this.options.verbose) {
|
|
722
|
-
hlog(`[CONTEXT-RECOVERY] Haiku verdict: ${state.contextLost ? 'LOST' : 'OK'} — ${verdict.reason}`);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/** Flush post-timeout output if context wasn't lost */
|
|
727
|
-
private flushPostTimeoutOutput(result: HeadlessRunResult, state: RetryLoopState): void {
|
|
300
|
+
const nativeTimeouts = result.nativeTimeoutCount ?? 0;
|
|
301
|
+
detectResumeContextLoss(result, state, useResume, 3, nativeTimeouts, this.options.verbose);
|
|
302
|
+
await detectNativeTimeoutContextLoss(result, state, 3, nativeTimeouts, this.options.verbose);
|
|
728
303
|
if (!state.contextLost && result.postTimeoutOutput) {
|
|
729
304
|
this.queueOutput(result.postTimeoutOutput);
|
|
730
305
|
this.flushOutputQueue();
|
|
731
306
|
}
|
|
307
|
+
return { result, useResume, nativeTimeouts };
|
|
732
308
|
}
|
|
733
309
|
|
|
734
|
-
/**
|
|
735
|
-
private
|
|
310
|
+
/** Evaluate all retry strategies. Returns true if the loop should continue. */
|
|
311
|
+
private async evaluateRetryStrategies(
|
|
736
312
|
result: HeadlessRunResult,
|
|
737
313
|
state: RetryLoopState,
|
|
738
314
|
useResume: boolean,
|
|
739
315
|
nativeTimeouts: number,
|
|
740
316
|
maxRetries: number,
|
|
741
317
|
promptWithAttachments: string,
|
|
742
|
-
|
|
743
|
-
if (state.checkpointRef.value || state.retryNumber >= maxRetries || !state.contextLost) {
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
this.accumulateToolResults(result, state);
|
|
747
|
-
state.retryNumber++;
|
|
748
|
-
const path = (useResume && nativeTimeouts === 0) ? 'InterMovementRecovery' : 'NativeTimeoutRecovery';
|
|
749
|
-
state.retryLog.push({
|
|
750
|
-
retryNumber: state.retryNumber,
|
|
751
|
-
path,
|
|
752
|
-
reason: `Context lost (${nativeTimeouts} timeouts, ${state.accumulatedToolResults.length} tools preserved)`,
|
|
753
|
-
timestamp: Date.now(),
|
|
754
|
-
});
|
|
755
|
-
if (useResume && nativeTimeouts === 0) {
|
|
756
|
-
this.applyInterMovementRecovery(state, promptWithAttachments);
|
|
757
|
-
} else {
|
|
758
|
-
this.applyNativeTimeoutRecovery(result, state, promptWithAttachments);
|
|
759
|
-
}
|
|
760
|
-
return true;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/** Accumulate completed tool results from a run into the retry state.
|
|
764
|
-
* Caps at MAX_ACCUMULATED_RESULTS to prevent recovery prompts from exceeding context limits.
|
|
765
|
-
* When the cap is reached, older results are evicted (FIFO) to make room for newer ones. */
|
|
766
|
-
private static readonly MAX_ACCUMULATED_RESULTS = 50;
|
|
767
|
-
|
|
768
|
-
private accumulateToolResults(result: HeadlessRunResult, state: RetryLoopState): void {
|
|
769
|
-
if (!result.toolUseHistory) return;
|
|
770
|
-
for (const t of result.toolUseHistory) {
|
|
771
|
-
if (t.result !== undefined) {
|
|
772
|
-
state.accumulatedToolResults.push({
|
|
773
|
-
toolName: t.toolName,
|
|
774
|
-
toolId: t.toolId,
|
|
775
|
-
toolInput: t.toolInput,
|
|
776
|
-
result: t.result,
|
|
777
|
-
isError: t.isError,
|
|
778
|
-
duration: t.duration,
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
// Evict oldest results if over the cap
|
|
783
|
-
const cap = ImprovisationSessionManager.MAX_ACCUMULATED_RESULTS;
|
|
784
|
-
if (state.accumulatedToolResults.length > cap) {
|
|
785
|
-
state.accumulatedToolResults = state.accumulatedToolResults.slice(-cap);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/** Handle inter-movement context loss recovery (resume session expired) */
|
|
790
|
-
private applyInterMovementRecovery(state: RetryLoopState, promptWithAttachments: string): void {
|
|
791
|
-
// Preserve session ID so --resume remains available on subsequent retries.
|
|
792
|
-
// The fresh recovery prompt will be used, but if this attempt also fails,
|
|
793
|
-
// the next retry can still try --resume via shouldRetrySignalCrash.
|
|
794
|
-
const historicalResults = this.extractHistoricalToolResults();
|
|
795
|
-
const allResults = [...historicalResults, ...state.accumulatedToolResults];
|
|
796
|
-
|
|
797
|
-
this.emit('onAutoRetry', {
|
|
798
|
-
retryNumber: state.retryNumber,
|
|
799
|
-
maxRetries: 3,
|
|
800
|
-
toolName: 'InterMovementRecovery',
|
|
801
|
-
completedCount: allResults.length,
|
|
802
|
-
});
|
|
803
|
-
this.queueOutput(
|
|
804
|
-
`\n[[MSTRO_CONTEXT_RECOVERY]] Session context expired — continuing with ${allResults.length} preserved results from prior work (retry ${state.retryNumber}/3).\n`
|
|
805
|
-
);
|
|
806
|
-
this.flushOutputQueue();
|
|
807
|
-
|
|
808
|
-
state.freshRecoveryMode = true;
|
|
809
|
-
state.currentPrompt = this.buildInterMovementRecoveryPrompt(promptWithAttachments, allResults);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/** Handle native-timeout context loss recovery (tool timeouts caused confusion) */
|
|
813
|
-
private applyNativeTimeoutRecovery(
|
|
814
|
-
result: HeadlessRunResult,
|
|
815
|
-
state: RetryLoopState,
|
|
816
|
-
promptWithAttachments: string,
|
|
817
|
-
): void {
|
|
818
|
-
const completedCount = state.accumulatedToolResults.length;
|
|
819
|
-
|
|
820
|
-
this.emit('onAutoRetry', {
|
|
821
|
-
retryNumber: state.retryNumber,
|
|
822
|
-
maxRetries: 3,
|
|
823
|
-
toolName: 'ContextRecovery',
|
|
824
|
-
completedCount,
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
if (result.claudeSessionId && state.retryNumber === 1) {
|
|
828
|
-
this.queueOutput(
|
|
829
|
-
`\n[[MSTRO_CONTEXT_RECOVERY]] Context loss detected — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/3).\n`
|
|
830
|
-
);
|
|
831
|
-
this.flushOutputQueue();
|
|
832
|
-
state.contextRecoverySessionId = result.claudeSessionId;
|
|
833
|
-
this.claudeSessionId = result.claudeSessionId;
|
|
834
|
-
state.currentPrompt = this.buildContextRecoveryPrompt(promptWithAttachments);
|
|
835
|
-
} else {
|
|
836
|
-
this.queueOutput(
|
|
837
|
-
`\n[[MSTRO_CONTEXT_RECOVERY]] Continuing with fresh context — ${completedCount} preserved results injected (retry ${state.retryNumber}/3).\n`
|
|
838
|
-
);
|
|
839
|
-
this.flushOutputQueue();
|
|
840
|
-
state.freshRecoveryMode = true;
|
|
841
|
-
state.currentPrompt = this.buildFreshRecoveryPrompt(promptWithAttachments, state.accumulatedToolResults, state.timedOutTools);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/** Handle tool timeout checkpoint. Returns true if loop should continue. */
|
|
846
|
-
private applyToolTimeoutRetry(
|
|
847
|
-
state: RetryLoopState,
|
|
848
|
-
maxRetries: number,
|
|
849
|
-
promptWithAttachments: string,
|
|
850
|
-
): boolean {
|
|
851
|
-
if (!state.checkpointRef.value || state.retryNumber >= maxRetries) {
|
|
852
|
-
return false;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
const cp: ExecutionCheckpoint = state.checkpointRef.value;
|
|
856
|
-
state.retryNumber++;
|
|
857
|
-
|
|
858
|
-
state.timedOutTools.push({
|
|
859
|
-
toolName: cp.hungTool.toolName,
|
|
860
|
-
input: cp.hungTool.input ?? {},
|
|
861
|
-
timeoutMs: cp.hungTool.timeoutMs,
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
const canResumeSession = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
|
|
865
|
-
state.retryLog.push({
|
|
866
|
-
retryNumber: state.retryNumber,
|
|
867
|
-
path: 'ToolTimeout',
|
|
868
|
-
reason: `${cp.hungTool.toolName} timed out after ${cp.hungTool.timeoutMs}ms, ${cp.completedTools.length} tools completed, ${canResumeSession ? 'resuming' : 'fresh start'}`,
|
|
869
|
-
timestamp: Date.now(),
|
|
870
|
-
});
|
|
871
|
-
this.emit('onAutoRetry', {
|
|
872
|
-
retryNumber: state.retryNumber,
|
|
873
|
-
maxRetries,
|
|
874
|
-
toolName: cp.hungTool.toolName,
|
|
875
|
-
url: cp.hungTool.url,
|
|
876
|
-
completedCount: cp.completedTools.length,
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
|
|
880
|
-
retry_number: state.retryNumber,
|
|
881
|
-
hung_tool: cp.hungTool.toolName,
|
|
882
|
-
hung_url: cp.hungTool.url?.slice(0, 200),
|
|
883
|
-
completed_tools: cp.completedTools.length,
|
|
884
|
-
elapsed_ms: cp.elapsedMs,
|
|
885
|
-
resume_attempted: canResumeSession,
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
state.currentPrompt = canResumeSession
|
|
889
|
-
? this.buildResumeRetryPrompt(cp, state.timedOutTools)
|
|
890
|
-
: this.buildRetryPrompt(cp, promptWithAttachments, state.timedOutTools);
|
|
891
|
-
|
|
892
|
-
this.queueOutput(
|
|
893
|
-
`\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`
|
|
894
|
-
);
|
|
895
|
-
this.flushOutputQueue();
|
|
896
|
-
|
|
897
|
-
return true;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/**
|
|
901
|
-
* Detect and retry after a signal crash (e.g., SIGTERM exit code 143).
|
|
902
|
-
* When the Claude process is killed externally (OOM, system signal, internal timeout
|
|
903
|
-
* that bypasses our watchdog), no existing recovery path catches it because contextLost
|
|
904
|
-
* is never set and no checkpoint is created. This adds a dedicated recovery path.
|
|
905
|
-
*/
|
|
906
|
-
private shouldRetrySignalCrash(
|
|
907
|
-
result: HeadlessRunResult,
|
|
908
|
-
state: RetryLoopState,
|
|
909
|
-
maxRetries: number,
|
|
910
|
-
promptWithAttachments: string,
|
|
911
|
-
): boolean {
|
|
912
|
-
// Only trigger for signal-killed processes (exit code 128+) that weren't already
|
|
913
|
-
// handled by context-loss or tool-timeout recovery paths.
|
|
914
|
-
// Must have an actual signal name — regular errors (e.g., auth failures, exit code 1)
|
|
915
|
-
// should NOT be retried as signal crashes.
|
|
916
|
-
const isSignalCrash = !!result.signalName;
|
|
917
|
-
const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
|
|
918
|
-
if ((!isSignalCrash && !exitCodeSignal) || state.retryNumber >= maxRetries) {
|
|
919
|
-
return false;
|
|
920
|
-
}
|
|
921
|
-
// Don't re-trigger if tool timeout watchdog already handled this iteration
|
|
922
|
-
// (contextLost is NOT checked here — signal crash takes priority over context loss
|
|
923
|
-
// because it uses --resume which is lighter and avoids re-sending accumulated results)
|
|
924
|
-
if (state.checkpointRef.value) {
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
this.accumulateToolResults(result, state);
|
|
929
|
-
state.retryNumber++;
|
|
930
|
-
|
|
931
|
-
const completedCount = state.accumulatedToolResults.length;
|
|
932
|
-
const signalInfo = result.signalName || 'unknown signal';
|
|
933
|
-
const useResume = !!result.claudeSessionId && state.retryNumber === 1;
|
|
934
|
-
|
|
935
|
-
state.retryLog.push({
|
|
936
|
-
retryNumber: state.retryNumber,
|
|
937
|
-
path: 'SignalCrash',
|
|
938
|
-
reason: `Process killed (${signalInfo}), ${completedCount} tools preserved, ${useResume ? 'resuming' : 'fresh start'}`,
|
|
939
|
-
timestamp: Date.now(),
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
this.emit('onAutoRetry', {
|
|
943
|
-
retryNumber: state.retryNumber,
|
|
944
|
-
maxRetries,
|
|
945
|
-
toolName: `SignalCrash(${signalInfo})`,
|
|
946
|
-
completedCount,
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
|
|
950
|
-
retry_number: state.retryNumber,
|
|
951
|
-
hung_tool: `signal_crash:${signalInfo}`,
|
|
952
|
-
completed_tools: completedCount,
|
|
953
|
-
resume_attempted: useResume,
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
// If we have a session ID, try resuming first (preserves full context)
|
|
957
|
-
if (useResume) {
|
|
958
|
-
this.queueOutput(
|
|
959
|
-
`\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — resuming session with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
|
|
960
|
-
);
|
|
961
|
-
this.flushOutputQueue();
|
|
962
|
-
state.contextRecoverySessionId = result.claudeSessionId;
|
|
963
|
-
this.claudeSessionId = result.claudeSessionId;
|
|
964
|
-
state.currentPrompt = this.buildSignalCrashRecoveryPrompt(promptWithAttachments, true);
|
|
965
|
-
} else {
|
|
966
|
-
// Fresh start with accumulated results injected
|
|
967
|
-
this.queueOutput(
|
|
968
|
-
`\n[[MSTRO_SIGNAL_RECOVERY]] Process killed (${signalInfo}) — restarting with ${completedCount} preserved results (retry ${state.retryNumber}/${maxRetries}).\n`
|
|
969
|
-
);
|
|
970
|
-
this.flushOutputQueue();
|
|
971
|
-
state.freshRecoveryMode = true;
|
|
972
|
-
const allResults = [...this.extractHistoricalToolResults(), ...state.accumulatedToolResults];
|
|
973
|
-
state.currentPrompt = this.buildSignalCrashRecoveryPrompt(promptWithAttachments, false, allResults);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
return true;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/** Build a recovery prompt after signal crash */
|
|
980
|
-
private buildSignalCrashRecoveryPrompt(
|
|
981
|
-
originalPrompt: string,
|
|
982
|
-
isResume: boolean,
|
|
983
|
-
toolResults?: ToolUseRecord[],
|
|
984
|
-
): string {
|
|
985
|
-
const parts: string[] = [];
|
|
986
|
-
|
|
987
|
-
if (isResume) {
|
|
988
|
-
parts.push('Your previous execution was interrupted by a system signal (the process was killed externally).');
|
|
989
|
-
parts.push('Your full conversation history is preserved — including all successful tool results.');
|
|
990
|
-
parts.push('');
|
|
991
|
-
parts.push('Review your conversation history above and continue from where you left off.');
|
|
992
|
-
} else {
|
|
993
|
-
parts.push('## AUTOMATIC RETRY — Previous Execution Interrupted');
|
|
994
|
-
parts.push('');
|
|
995
|
-
parts.push('The previous execution was interrupted by a system signal (process killed).');
|
|
996
|
-
if (toolResults && toolResults.length > 0) {
|
|
997
|
-
parts.push(`${toolResults.length} tool results were preserved from prior work.`);
|
|
998
|
-
parts.push('');
|
|
999
|
-
parts.push('### Preserved results:');
|
|
1000
|
-
for (const t of toolResults.slice(-20)) {
|
|
1001
|
-
const inputSummary = JSON.stringify(t.toolInput).slice(0, 120);
|
|
1002
|
-
const resultPreview = (t.result ?? '').slice(0, 200);
|
|
1003
|
-
parts.push(`- **${t.toolName}**(${inputSummary}): ${resultPreview}`);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
parts.push('');
|
|
1009
|
-
parts.push('### Original task:');
|
|
1010
|
-
parts.push(originalPrompt);
|
|
1011
|
-
parts.push('');
|
|
1012
|
-
parts.push('INSTRUCTIONS:');
|
|
1013
|
-
parts.push('1. Use the results above -- do not re-fetch content you already have');
|
|
1014
|
-
parts.push('2. Continue from where you left off');
|
|
1015
|
-
parts.push('3. Prefer multiple small, focused tool calls over single large ones');
|
|
1016
|
-
parts.push('4. Do NOT spawn Task subagents — do work inline to avoid further interruptions');
|
|
1017
|
-
|
|
1018
|
-
return parts.join('\n');
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/**
|
|
1022
|
-
* Detect premature completion: Claude exited normally (exit code 0, end_turn) but the
|
|
1023
|
-
* response indicates more work was planned. This happens when the model "context-fatigues"
|
|
1024
|
-
* during long multi-step tasks and produces end_turn after completing a subset of the work.
|
|
1025
|
-
*
|
|
1026
|
-
* Two paths:
|
|
1027
|
-
* - max_tokens: always retry (model was forcibly stopped mid-generation)
|
|
1028
|
-
* - end_turn: Haiku assessment determines if the response looks incomplete
|
|
1029
|
-
*/
|
|
1030
|
-
private async shouldRetryPrematureCompletion(
|
|
1031
|
-
result: HeadlessRunResult,
|
|
1032
|
-
state: RetryLoopState,
|
|
1033
|
-
maxRetries: number,
|
|
318
|
+
callbacks: RetryCallbacks,
|
|
1034
319
|
): Promise<boolean> {
|
|
1035
|
-
|
|
1036
|
-
return false;
|
|
1037
|
-
}
|
|
320
|
+
const session = this.buildRetrySessionState();
|
|
1038
321
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
this.applyPrematureCompletionRetry(result, state, maxRetries, stopReason, isMaxTokens);
|
|
1046
|
-
return true;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/** Guard checks for premature completion — must pass all to proceed with assessment */
|
|
1050
|
-
private isPrematureCompletionCandidate(
|
|
1051
|
-
result: HeadlessRunResult,
|
|
1052
|
-
state: RetryLoopState,
|
|
1053
|
-
maxRetries: number,
|
|
1054
|
-
): boolean {
|
|
1055
|
-
// Only trigger for clean exits with a known stop reason
|
|
1056
|
-
if (!result.completed || result.signalName || state.retryNumber >= maxRetries) return false;
|
|
1057
|
-
// Don't re-trigger if other recovery paths already handled this iteration
|
|
1058
|
-
if (state.checkpointRef.value || state.contextLost) return false;
|
|
1059
|
-
// Must have a session ID to resume, and a stop reason to classify
|
|
1060
|
-
if (!result.claudeSessionId || !result.stopReason) return false;
|
|
1061
|
-
// Only act on max_tokens or end_turn
|
|
1062
|
-
return result.stopReason === 'max_tokens' || result.stopReason === 'end_turn';
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/** Use Haiku to assess whether an end_turn response is genuinely complete */
|
|
1066
|
-
private async assessEndTurnCompletion(result: HeadlessRunResult): Promise<boolean> {
|
|
1067
|
-
if (!result.assistantResponse) return false;
|
|
1068
|
-
|
|
1069
|
-
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
1070
|
-
const verdict = await assessPrematureCompletion({
|
|
1071
|
-
responseTail: result.assistantResponse.slice(-800),
|
|
1072
|
-
successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
|
|
1073
|
-
hasThinking: !!result.thinkingOutput,
|
|
1074
|
-
responseLength: result.assistantResponse.length,
|
|
1075
|
-
}, claudeCmd, this.options.verbose);
|
|
1076
|
-
|
|
1077
|
-
if (this.options.verbose) {
|
|
1078
|
-
hlog(`[PREMATURE-COMPLETION] Haiku verdict: ${verdict.isIncomplete ? 'INCOMPLETE' : 'COMPLETE'} — ${verdict.reason}`);
|
|
1079
|
-
}
|
|
1080
|
-
return verdict.isIncomplete;
|
|
322
|
+
if (shouldRetrySignalCrash(result, state, session, maxRetries, promptWithAttachments, callbacks)) { this.syncSessionStateBack(session); return true; }
|
|
323
|
+
if (shouldRetryContextLoss(result, state, session, useResume, nativeTimeouts, maxRetries, promptWithAttachments, callbacks)) { this.syncSessionStateBack(session); return true; }
|
|
324
|
+
if (applyToolTimeoutRetry(state, maxRetries, promptWithAttachments, callbacks, this.options.model)) return true;
|
|
325
|
+
if (await shouldRetryPrematureCompletion(result, state, session, maxRetries, callbacks)) { this.syncSessionStateBack(session); return true; }
|
|
326
|
+
this.syncSessionStateBack(session);
|
|
327
|
+
return false;
|
|
1081
328
|
}
|
|
1082
329
|
|
|
1083
|
-
|
|
1084
|
-
private applyPrematureCompletionRetry(
|
|
1085
|
-
result: HeadlessRunResult,
|
|
1086
|
-
state: RetryLoopState,
|
|
1087
|
-
maxRetries: number,
|
|
1088
|
-
stopReason: string,
|
|
1089
|
-
isMaxTokens: boolean,
|
|
1090
|
-
): void {
|
|
1091
|
-
state.retryNumber++;
|
|
1092
|
-
const reason = isMaxTokens ? 'Output limit reached' : 'Task appears unfinished (AI assessment)';
|
|
1093
|
-
|
|
1094
|
-
state.retryLog.push({
|
|
1095
|
-
retryNumber: state.retryNumber,
|
|
1096
|
-
path: 'PrematureCompletion',
|
|
1097
|
-
reason,
|
|
1098
|
-
timestamp: Date.now(),
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
this.emit('onAutoRetry', {
|
|
1102
|
-
retryNumber: state.retryNumber,
|
|
1103
|
-
maxRetries,
|
|
1104
|
-
toolName: `PrematureCompletion(${stopReason})`,
|
|
1105
|
-
completedCount: result.toolUseHistory?.length ?? 0,
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
trackEvent(AnalyticsEvents.IMPROVISE_AUTO_RETRY, {
|
|
1109
|
-
retry_number: state.retryNumber,
|
|
1110
|
-
hung_tool: `premature_completion:${stopReason}`,
|
|
1111
|
-
completed_tools: result.toolUseHistory?.length ?? 0,
|
|
1112
|
-
resume_attempted: true,
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
this.queueOutput(
|
|
1116
|
-
`\n${reason} — resuming session (retry ${state.retryNumber}/${maxRetries}).\n`
|
|
1117
|
-
);
|
|
1118
|
-
this.flushOutputQueue();
|
|
1119
|
-
|
|
1120
|
-
state.contextRecoverySessionId = result.claudeSessionId;
|
|
1121
|
-
this.claudeSessionId = result.claudeSessionId;
|
|
1122
|
-
state.currentPrompt = 'continue';
|
|
1123
|
-
}
|
|
330
|
+
// ========== Cancel Handling ==========
|
|
1124
331
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
state: RetryLoopState,
|
|
1128
|
-
result: HeadlessRunResult,
|
|
332
|
+
private handleCancelledExecution(
|
|
333
|
+
result: HeadlessRunResult | undefined,
|
|
1129
334
|
userPrompt: string,
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
const currentToolCount = result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
335
|
+
sequenceNumber: number,
|
|
336
|
+
execStart: number,
|
|
337
|
+
): MovementRecord {
|
|
338
|
+
this._isExecuting = false;
|
|
339
|
+
this._executionStartTimestamp = undefined;
|
|
340
|
+
this.executionEventLog = [];
|
|
341
|
+
this.currentRunner = null;
|
|
1138
342
|
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
resultA: {
|
|
1143
|
-
successfulToolCalls: bestToolCount,
|
|
1144
|
-
responseLength: state.bestResult.assistantResponse?.length ?? 0,
|
|
1145
|
-
hasThinking: !!state.bestResult.thinkingOutput,
|
|
1146
|
-
responseTail: (state.bestResult.assistantResponse ?? '').slice(-500),
|
|
1147
|
-
},
|
|
1148
|
-
resultB: {
|
|
1149
|
-
successfulToolCalls: currentToolCount,
|
|
1150
|
-
responseLength: result.assistantResponse?.length ?? 0,
|
|
1151
|
-
hasThinking: !!result.thinkingOutput,
|
|
1152
|
-
responseTail: (result.assistantResponse ?? '').slice(-500),
|
|
1153
|
-
},
|
|
1154
|
-
}, claudeCmd, this.options.verbose);
|
|
1155
|
-
|
|
1156
|
-
if (verdict.winner === 'A') {
|
|
1157
|
-
if (this.options.verbose) hlog(`[BEST-RESULT] Haiku picked earlier attempt: ${verdict.reason}`);
|
|
1158
|
-
return this.mergeResultSessionId(state.bestResult, result.claudeSessionId);
|
|
1159
|
-
}
|
|
1160
|
-
if (this.options.verbose) hlog(`[BEST-RESULT] Haiku picked final attempt: ${verdict.reason}`);
|
|
1161
|
-
return result;
|
|
1162
|
-
} catch {
|
|
1163
|
-
return this.fallbackBestResult(state.bestResult, result);
|
|
343
|
+
if (this._cancelCompleteEmitted) {
|
|
344
|
+
const existing = this.history.movements.find(m => m.sequenceNumber === sequenceNumber);
|
|
345
|
+
if (existing) return existing;
|
|
1164
346
|
}
|
|
1165
|
-
}
|
|
1166
347
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
348
|
+
const cancelledMovement: MovementRecord = {
|
|
349
|
+
id: `prompt-${sequenceNumber}`,
|
|
350
|
+
sequenceNumber,
|
|
351
|
+
userPrompt,
|
|
352
|
+
timestamp: new Date().toISOString(),
|
|
353
|
+
tokensUsed: result ? result.totalTokens : 0,
|
|
354
|
+
summary: '',
|
|
355
|
+
filesModified: [],
|
|
356
|
+
assistantResponse: result?.assistantResponse,
|
|
357
|
+
thinkingOutput: result?.thinkingOutput,
|
|
358
|
+
toolUseHistory: result?.toolUseHistory?.map(t => ({
|
|
359
|
+
toolName: t.toolName, toolId: t.toolId, toolInput: t.toolInput,
|
|
360
|
+
result: t.result,
|
|
361
|
+
})),
|
|
362
|
+
errorOutput: 'Execution cancelled by user',
|
|
363
|
+
durationMs: Date.now() - execStart,
|
|
364
|
+
};
|
|
365
|
+
this.persistMovement(cancelledMovement);
|
|
366
|
+
const fallbackResult = {
|
|
367
|
+
completed: false, needsHandoff: false, totalTokens: 0, sessionId: '',
|
|
368
|
+
output: '', exitCode: 1, signalName: 'SIGTERM',
|
|
369
|
+
} as HeadlessRunResult;
|
|
370
|
+
this.emitMovementComplete(cancelledMovement, result ?? fallbackResult, execStart, sequenceNumber);
|
|
371
|
+
return cancelledMovement;
|
|
1176
372
|
}
|
|
1177
373
|
|
|
1178
|
-
|
|
1179
|
-
private mergeResultSessionId(result: HeadlessRunResult, sessionId: string | undefined): HeadlessRunResult {
|
|
1180
|
-
if (sessionId) return { ...result, claudeSessionId: sessionId };
|
|
1181
|
-
return result;
|
|
1182
|
-
}
|
|
374
|
+
// ========== Post-Execution Helpers ==========
|
|
1183
375
|
|
|
1184
|
-
/** Capture Claude session ID and surface execution failures */
|
|
1185
376
|
private captureSessionAndSurfaceErrors(result: HeadlessRunResult): void {
|
|
1186
377
|
if (result.claudeSessionId) {
|
|
1187
378
|
this.claudeSessionId = result.claudeSessionId;
|
|
@@ -1193,13 +384,12 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1193
384
|
}
|
|
1194
385
|
}
|
|
1195
386
|
|
|
1196
|
-
/** Build a MovementRecord from execution result */
|
|
1197
387
|
private buildMovementRecord(
|
|
1198
388
|
result: HeadlessRunResult,
|
|
1199
389
|
userPrompt: string,
|
|
1200
390
|
sequenceNumber: number,
|
|
1201
391
|
execStart: number,
|
|
1202
|
-
retryLog?: RetryLogEntry[],
|
|
392
|
+
retryLog?: import('./improvisation-types.js').RetryLogEntry[],
|
|
1203
393
|
): MovementRecord {
|
|
1204
394
|
return {
|
|
1205
395
|
id: `prompt-${sequenceNumber}`,
|
|
@@ -1212,12 +402,8 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1212
402
|
assistantResponse: result.assistantResponse,
|
|
1213
403
|
thinkingOutput: result.thinkingOutput,
|
|
1214
404
|
toolUseHistory: result.toolUseHistory?.map(t => ({
|
|
1215
|
-
toolName: t.toolName,
|
|
1216
|
-
|
|
1217
|
-
toolInput: t.toolInput,
|
|
1218
|
-
result: t.result,
|
|
1219
|
-
isError: t.isError,
|
|
1220
|
-
duration: t.duration
|
|
405
|
+
toolName: t.toolName, toolId: t.toolId, toolInput: t.toolInput,
|
|
406
|
+
result: t.result, isError: t.isError, duration: t.duration,
|
|
1221
407
|
})),
|
|
1222
408
|
errorOutput: result.error,
|
|
1223
409
|
durationMs: Date.now() - execStart,
|
|
@@ -1225,33 +411,23 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1225
411
|
};
|
|
1226
412
|
}
|
|
1227
413
|
|
|
1228
|
-
/** Handle file conflicts from execution result */
|
|
1229
414
|
private handleConflicts(result: HeadlessRunResult): void {
|
|
1230
415
|
if (!result.conflicts || result.conflicts.length === 0) return;
|
|
1231
416
|
this.queueOutput(`\n⚠ File conflicts detected: ${result.conflicts.length}`);
|
|
1232
417
|
result.conflicts.forEach(c => {
|
|
1233
418
|
this.queueOutput(` - ${c.filePath} (modified by: ${c.modifiedBy.join(', ')})`);
|
|
1234
|
-
if (c.backupPath) {
|
|
1235
|
-
this.queueOutput(` Backup created: ${c.backupPath}`);
|
|
1236
|
-
}
|
|
419
|
+
if (c.backupPath) this.queueOutput(` Backup created: ${c.backupPath}`);
|
|
1237
420
|
});
|
|
1238
421
|
this.flushOutputQueue();
|
|
1239
422
|
}
|
|
1240
423
|
|
|
1241
|
-
/** Persist movement to history */
|
|
1242
424
|
private persistMovement(movement: MovementRecord): void {
|
|
1243
425
|
this.history.movements.push(movement);
|
|
1244
426
|
this.history.totalTokens += movement.tokensUsed;
|
|
1245
427
|
this.saveHistory();
|
|
1246
428
|
}
|
|
1247
429
|
|
|
1248
|
-
|
|
1249
|
-
private emitMovementComplete(
|
|
1250
|
-
movement: MovementRecord,
|
|
1251
|
-
result: HeadlessRunResult,
|
|
1252
|
-
execStart: number,
|
|
1253
|
-
sequenceNumber: number,
|
|
1254
|
-
): void {
|
|
430
|
+
private emitMovementComplete(movement: MovementRecord, result: HeadlessRunResult, execStart: number, sequenceNumber: number): void {
|
|
1255
431
|
this.emit('onMovementComplete', movement);
|
|
1256
432
|
trackEvent(AnalyticsEvents.IMPROVISE_MOVEMENT_COMPLETED, {
|
|
1257
433
|
tokens_used: movement.tokensUsed,
|
|
@@ -1263,341 +439,8 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1263
439
|
this.emit('onSessionUpdate', this.getHistory());
|
|
1264
440
|
}
|
|
1265
441
|
|
|
1266
|
-
|
|
1267
|
-
* Build historical context for resuming a session.
|
|
1268
|
-
* This creates a summary of the previous conversation that will be injected
|
|
1269
|
-
* into the first prompt of a resumed session.
|
|
1270
|
-
*/
|
|
1271
|
-
private buildHistoricalContext(): string {
|
|
1272
|
-
if (this.history.movements.length === 0) {
|
|
1273
|
-
return '';
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
const contextParts: string[] = [
|
|
1277
|
-
'--- CONVERSATION HISTORY (for context, do not repeat these responses) ---',
|
|
1278
|
-
''
|
|
1279
|
-
];
|
|
1280
|
-
|
|
1281
|
-
// Include each movement as context
|
|
1282
|
-
for (const movement of this.history.movements) {
|
|
1283
|
-
contextParts.push(`[User Prompt ${movement.sequenceNumber}]:`);
|
|
1284
|
-
contextParts.push(movement.userPrompt);
|
|
1285
|
-
contextParts.push('');
|
|
1286
|
-
|
|
1287
|
-
if (movement.assistantResponse) {
|
|
1288
|
-
contextParts.push(`[Your Response ${movement.sequenceNumber}]:`);
|
|
1289
|
-
// Truncate very long responses to save tokens
|
|
1290
|
-
const response = movement.assistantResponse.length > 2000
|
|
1291
|
-
? `${movement.assistantResponse.slice(0, 2000)}\n... (response truncated for context)`
|
|
1292
|
-
: movement.assistantResponse;
|
|
1293
|
-
contextParts.push(response);
|
|
1294
|
-
contextParts.push('');
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
if (movement.toolUseHistory && movement.toolUseHistory.length > 0) {
|
|
1298
|
-
contextParts.push(`[Tools Used in Prompt ${movement.sequenceNumber}]:`);
|
|
1299
|
-
for (const tool of movement.toolUseHistory) {
|
|
1300
|
-
contextParts.push(`- ${tool.toolName}`);
|
|
1301
|
-
}
|
|
1302
|
-
contextParts.push('');
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
contextParts.push('--- END OF CONVERSATION HISTORY ---');
|
|
1307
|
-
contextParts.push('');
|
|
1308
|
-
contextParts.push('Continue the conversation from where we left off. The user is now asking:');
|
|
1309
|
-
contextParts.push('');
|
|
1310
|
-
|
|
1311
|
-
return contextParts.join('\n');
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
/**
|
|
1315
|
-
* Build a retry prompt from a tool timeout checkpoint.
|
|
1316
|
-
* Injects completed tool results and instructs Claude to skip the failed resource.
|
|
1317
|
-
*/
|
|
1318
|
-
private buildRetryPrompt(
|
|
1319
|
-
checkpoint: ExecutionCheckpoint,
|
|
1320
|
-
originalPrompt: string,
|
|
1321
|
-
allTimedOut?: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>,
|
|
1322
|
-
): string {
|
|
1323
|
-
const urlSuffix = checkpoint.hungTool.url ? ` while fetching: ${checkpoint.hungTool.url}` : '';
|
|
1324
|
-
const parts: string[] = [
|
|
1325
|
-
'## AUTOMATIC RETRY -- Previous Execution Interrupted',
|
|
1326
|
-
'',
|
|
1327
|
-
`The previous execution was interrupted because ${checkpoint.hungTool.toolName} timed out after ${Math.round(checkpoint.hungTool.timeoutMs / 1000)}s${urlSuffix}.`,
|
|
1328
|
-
'',
|
|
1329
|
-
];
|
|
1330
|
-
|
|
1331
|
-
if (allTimedOut && allTimedOut.length > 0) {
|
|
1332
|
-
parts.push(...this.formatTimedOutTools(allTimedOut), '');
|
|
1333
|
-
} else {
|
|
1334
|
-
parts.push('This URL/resource is unreachable. DO NOT retry the same URL or query.', '');
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
if (checkpoint.completedTools.length > 0) {
|
|
1338
|
-
parts.push(...this.formatCompletedTools(checkpoint.completedTools), '');
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
if (checkpoint.inProgressTools && checkpoint.inProgressTools.length > 0) {
|
|
1342
|
-
parts.push(...this.formatInProgressTools(checkpoint.inProgressTools), '');
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
if (checkpoint.assistantText) {
|
|
1346
|
-
const preview = checkpoint.assistantText.length > 8000
|
|
1347
|
-
? `${checkpoint.assistantText.slice(0, 8000)}...\n(truncated — full response was ${checkpoint.assistantText.length} chars)`
|
|
1348
|
-
: checkpoint.assistantText;
|
|
1349
|
-
parts.push('### Your response before interruption:', preview, '');
|
|
1350
|
-
}
|
|
442
|
+
// ========== History I/O ==========
|
|
1351
443
|
|
|
1352
|
-
parts.push('### Original task (continue from where you left off):');
|
|
1353
|
-
parts.push(originalPrompt);
|
|
1354
|
-
parts.push('');
|
|
1355
|
-
parts.push('INSTRUCTIONS:');
|
|
1356
|
-
parts.push('1. Use the results above -- do not re-fetch content you already have');
|
|
1357
|
-
parts.push('2. Find ALTERNATIVE sources for the content that timed out (different URL, different approach)');
|
|
1358
|
-
parts.push('3. Re-run any in-progress tools that were lost (listed above) if their results are needed');
|
|
1359
|
-
parts.push('4. If no alternative exists, proceed with the results you have and note what was unavailable');
|
|
1360
|
-
|
|
1361
|
-
return parts.join('\n');
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
/**
|
|
1365
|
-
* Build a short retry prompt for --resume sessions.
|
|
1366
|
-
* The session already has full conversation context, so we only need to
|
|
1367
|
-
* explain what timed out and instruct Claude to continue.
|
|
1368
|
-
*/
|
|
1369
|
-
private buildResumeRetryPrompt(
|
|
1370
|
-
checkpoint: ExecutionCheckpoint,
|
|
1371
|
-
allTimedOut?: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>,
|
|
1372
|
-
): string {
|
|
1373
|
-
const parts: string[] = [];
|
|
1374
|
-
|
|
1375
|
-
parts.push(
|
|
1376
|
-
`Your previous ${checkpoint.hungTool.toolName} call timed out after ${Math.round(checkpoint.hungTool.timeoutMs / 1000)}s${checkpoint.hungTool.url ? ` fetching: ${checkpoint.hungTool.url}` : ''}.`
|
|
1377
|
-
);
|
|
1378
|
-
|
|
1379
|
-
// List all timed-out tools across retries so Claude avoids repeating them
|
|
1380
|
-
if (allTimedOut && allTimedOut.length > 1) {
|
|
1381
|
-
parts.push('');
|
|
1382
|
-
parts.push('All timed-out tools/resources (DO NOT retry any of these):');
|
|
1383
|
-
for (const t of allTimedOut) {
|
|
1384
|
-
const inputSummary = this.summarizeToolInput(t.input);
|
|
1385
|
-
parts.push(`- ${t.toolName}(${inputSummary})`);
|
|
1386
|
-
}
|
|
1387
|
-
} else {
|
|
1388
|
-
parts.push('This URL/resource is unreachable. DO NOT retry the same URL or query.');
|
|
1389
|
-
}
|
|
1390
|
-
parts.push('Continue your task — find an alternative source or proceed with the results you already have.');
|
|
1391
|
-
|
|
1392
|
-
return parts.join('\n');
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Context loss detection is now handled by assessContextLoss() in stall-assessor.ts
|
|
1396
|
-
// using Haiku assessment instead of brittle regex patterns.
|
|
1397
|
-
|
|
1398
|
-
/**
|
|
1399
|
-
* Build a recovery prompt for --resume after context loss.
|
|
1400
|
-
* Since we're resuming the same session, Claude has full conversation history
|
|
1401
|
-
* (including all preserved tool results). We just need to redirect it back to the task.
|
|
1402
|
-
*/
|
|
1403
|
-
private buildContextRecoveryPrompt(originalPrompt: string): string {
|
|
1404
|
-
const parts: string[] = [];
|
|
1405
|
-
|
|
1406
|
-
parts.push('Your previous response indicated you lost context due to tool timeouts, but your full conversation history is preserved — including all successful tool results.');
|
|
1407
|
-
parts.push('');
|
|
1408
|
-
parts.push('Review your conversation history above. You already have results from many successful tool calls. Use those results to continue the task.');
|
|
1409
|
-
parts.push('');
|
|
1410
|
-
parts.push('Original task:');
|
|
1411
|
-
parts.push(originalPrompt);
|
|
1412
|
-
parts.push('');
|
|
1413
|
-
parts.push('INSTRUCTIONS:');
|
|
1414
|
-
parts.push('1. Review your conversation history — all your previous tool results are still available');
|
|
1415
|
-
parts.push('2. Continue from where you left off using the results you already gathered');
|
|
1416
|
-
parts.push('3. If specific tool calls timed out, skip those and work with what you have');
|
|
1417
|
-
parts.push('4. Do NOT start over — build on the work already done');
|
|
1418
|
-
parts.push('5. Do NOT spawn Task subagents for work that previously timed out — do it inline instead');
|
|
1419
|
-
parts.push('6. Prefer multiple small, focused tool calls over single large ones to avoid further timeouts');
|
|
1420
|
-
|
|
1421
|
-
return parts.join('\n');
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
/**
|
|
1425
|
-
* Build a recovery prompt for a fresh session (no --resume) after repeated context loss.
|
|
1426
|
-
* Injects all accumulated tool results from previous attempts so Claude can continue
|
|
1427
|
-
* the task without re-fetching data it already gathered.
|
|
1428
|
-
*/
|
|
1429
|
-
private buildFreshRecoveryPrompt(
|
|
1430
|
-
originalPrompt: string,
|
|
1431
|
-
toolResults: ToolUseRecord[],
|
|
1432
|
-
timedOutTools?: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>,
|
|
1433
|
-
): string {
|
|
1434
|
-
const parts: string[] = [
|
|
1435
|
-
'## CONTINUING LONG-RUNNING TASK',
|
|
1436
|
-
'',
|
|
1437
|
-
'The previous execution encountered tool timeouts and lost context.',
|
|
1438
|
-
'Below are all results gathered before the interruption. Continue the task using these results.',
|
|
1439
|
-
'',
|
|
1440
|
-
];
|
|
1441
|
-
|
|
1442
|
-
if (timedOutTools && timedOutTools.length > 0) {
|
|
1443
|
-
parts.push(...this.formatTimedOutTools(timedOutTools), '');
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
parts.push(...this.formatToolResults(toolResults));
|
|
1447
|
-
|
|
1448
|
-
parts.push('### Original task:');
|
|
1449
|
-
parts.push(originalPrompt);
|
|
1450
|
-
parts.push('');
|
|
1451
|
-
parts.push('INSTRUCTIONS:');
|
|
1452
|
-
parts.push('1. Use the preserved results above \u2014 do NOT re-fetch data you already have');
|
|
1453
|
-
parts.push('2. Continue the task from where it was interrupted');
|
|
1454
|
-
parts.push('3. If you need additional data, fetch it (but try alternative sources if the original timed out)');
|
|
1455
|
-
parts.push('4. Complete the original task fully');
|
|
1456
|
-
parts.push('5. Do NOT spawn Task subagents for work that previously timed out \u2014 do it inline instead');
|
|
1457
|
-
parts.push('6. Prefer multiple small, focused tool calls over single large ones to avoid further timeouts');
|
|
1458
|
-
|
|
1459
|
-
return parts.join('\n');
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
/**
|
|
1463
|
-
* Extract tool results from the last N movements in history.
|
|
1464
|
-
* Used for inter-movement recovery to provide context from prior work
|
|
1465
|
-
* when a resume session is corrupted/expired.
|
|
1466
|
-
*/
|
|
1467
|
-
private extractHistoricalToolResults(maxMovements = 3): ToolUseRecord[] {
|
|
1468
|
-
const results: ToolUseRecord[] = [];
|
|
1469
|
-
const recentMovements = this.history.movements.slice(-maxMovements);
|
|
1470
|
-
|
|
1471
|
-
for (const movement of recentMovements) {
|
|
1472
|
-
if (!movement.toolUseHistory) continue;
|
|
1473
|
-
for (const tool of movement.toolUseHistory) {
|
|
1474
|
-
if (tool.result !== undefined && !tool.isError) {
|
|
1475
|
-
results.push({
|
|
1476
|
-
toolName: tool.toolName,
|
|
1477
|
-
toolId: tool.toolId,
|
|
1478
|
-
toolInput: tool.toolInput,
|
|
1479
|
-
result: tool.result,
|
|
1480
|
-
isError: tool.isError,
|
|
1481
|
-
duration: tool.duration,
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
return results;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
/**
|
|
1491
|
-
* Build a recovery prompt for inter-movement context loss.
|
|
1492
|
-
* The Claude session expired between movements (not due to native timeouts).
|
|
1493
|
-
* Includes prior conversation summary + preserved tool results + anti-timeout guidance.
|
|
1494
|
-
*/
|
|
1495
|
-
private buildInterMovementRecoveryPrompt(originalPrompt: string, toolResults: ToolUseRecord[]): string {
|
|
1496
|
-
const parts: string[] = [
|
|
1497
|
-
'## SESSION RECOVERY — Prior Session Expired',
|
|
1498
|
-
'',
|
|
1499
|
-
'Your previous session expired between prompts. Below is a summary of the conversation so far and all preserved tool results.',
|
|
1500
|
-
'',
|
|
1501
|
-
];
|
|
1502
|
-
|
|
1503
|
-
parts.push(...this.formatConversationHistory(this.history.movements));
|
|
1504
|
-
parts.push(...this.formatToolResults(toolResults));
|
|
1505
|
-
|
|
1506
|
-
parts.push('### Current user prompt:');
|
|
1507
|
-
parts.push(originalPrompt);
|
|
1508
|
-
parts.push('');
|
|
1509
|
-
parts.push('INSTRUCTIONS:');
|
|
1510
|
-
parts.push('1. Use the preserved results above — do NOT re-fetch data you already have');
|
|
1511
|
-
parts.push('2. Continue the conversation naturally based on the history above');
|
|
1512
|
-
parts.push('3. If you need additional data, fetch it with small focused tool calls');
|
|
1513
|
-
parts.push('4. Do NOT spawn Task subagents — do work inline to avoid further timeouts');
|
|
1514
|
-
parts.push('5. Prefer multiple small, focused tool calls over single large ones');
|
|
1515
|
-
|
|
1516
|
-
return parts.join('\n');
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
/** Summarize a tool input for display in retry prompts */
|
|
1520
|
-
private summarizeToolInput(input: Record<string, unknown>): string {
|
|
1521
|
-
if (input.url) return String(input.url).slice(0, 100);
|
|
1522
|
-
if (input.query) return String(input.query).slice(0, 100);
|
|
1523
|
-
if (input.command) return String(input.command).slice(0, 100);
|
|
1524
|
-
if (input.prompt) return String(input.prompt).slice(0, 100);
|
|
1525
|
-
return JSON.stringify(input).slice(0, 100);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
/** Format a list of timed-out tools for retry prompts */
|
|
1529
|
-
private formatTimedOutTools(tools: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>): string[] {
|
|
1530
|
-
const lines: string[] = [];
|
|
1531
|
-
lines.push('### Tools/resources that have timed out (DO NOT retry these):');
|
|
1532
|
-
for (const t of tools) {
|
|
1533
|
-
const inputSummary = this.summarizeToolInput(t.input);
|
|
1534
|
-
lines.push(`- **${t.toolName}**(${inputSummary}) — timed out after ${Math.round(t.timeoutMs / 1000)}s`);
|
|
1535
|
-
}
|
|
1536
|
-
return lines;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
/** Format completed checkpoint tools for retry prompts */
|
|
1540
|
-
private formatCompletedTools(tools: Array<{ toolName: string; input: Record<string, unknown>; result: string }>, maxLen = 2000): string[] {
|
|
1541
|
-
const lines: string[] = [];
|
|
1542
|
-
lines.push('### Results already obtained:');
|
|
1543
|
-
for (const tool of tools) {
|
|
1544
|
-
const inputSummary = this.summarizeToolInput(tool.input);
|
|
1545
|
-
const preview = tool.result.length > maxLen ? `${tool.result.slice(0, maxLen)}...` : tool.result;
|
|
1546
|
-
lines.push(`- **${tool.toolName}**(${inputSummary}): ${preview}`);
|
|
1547
|
-
}
|
|
1548
|
-
return lines;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
/** Format in-progress tools for retry prompts */
|
|
1552
|
-
private formatInProgressTools(tools: Array<{ toolName: string; input: Record<string, unknown> }>): string[] {
|
|
1553
|
-
const lines: string[] = [];
|
|
1554
|
-
lines.push('### Tools that were still running (lost when process was killed):');
|
|
1555
|
-
for (const tool of tools) {
|
|
1556
|
-
const inputSummary = this.summarizeToolInput(tool.input);
|
|
1557
|
-
lines.push(`- **${tool.toolName}**(${inputSummary}) — was in progress, may need re-running`);
|
|
1558
|
-
}
|
|
1559
|
-
return lines;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
/** Format tool results from ToolUseRecord[] for recovery prompts */
|
|
1563
|
-
private formatToolResults(toolResults: ToolUseRecord[], maxLen = 3000): string[] {
|
|
1564
|
-
const completed = toolResults.filter(t => t.result !== undefined && !t.isError);
|
|
1565
|
-
if (completed.length === 0) return [];
|
|
1566
|
-
const lines: string[] = [`### ${completed.length} preserved results from prior work:`, ''];
|
|
1567
|
-
for (const tool of completed) {
|
|
1568
|
-
const inputSummary = this.summarizeToolInput(tool.toolInput);
|
|
1569
|
-
const preview = tool.result && tool.result.length > maxLen
|
|
1570
|
-
? `${tool.result.slice(0, maxLen)}...\n(truncated, ${tool.result.length} chars total)`
|
|
1571
|
-
: tool.result || '';
|
|
1572
|
-
lines.push(`**${tool.toolName}**(${inputSummary}):`);
|
|
1573
|
-
lines.push(preview);
|
|
1574
|
-
lines.push('');
|
|
1575
|
-
}
|
|
1576
|
-
return lines;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
/** Format conversation history for recovery prompts */
|
|
1580
|
-
private formatConversationHistory(movements: MovementRecord[], maxMovements = 5): string[] {
|
|
1581
|
-
const recent = movements.slice(-maxMovements);
|
|
1582
|
-
if (recent.length === 0) return [];
|
|
1583
|
-
const lines: string[] = ['### Conversation so far:'];
|
|
1584
|
-
for (const movement of recent) {
|
|
1585
|
-
const promptText = movement.userPrompt.length > 300 ? `${movement.userPrompt.slice(0, 300)}...` : movement.userPrompt;
|
|
1586
|
-
lines.push(`**User (prompt ${movement.sequenceNumber}):** ${promptText}`);
|
|
1587
|
-
if (movement.assistantResponse) {
|
|
1588
|
-
const response = movement.assistantResponse.length > 1000
|
|
1589
|
-
? `${movement.assistantResponse.slice(0, 1000)}...\n(truncated, ${movement.assistantResponse.length} chars)`
|
|
1590
|
-
: movement.assistantResponse;
|
|
1591
|
-
lines.push(`**Your response:** ${response}`);
|
|
1592
|
-
}
|
|
1593
|
-
lines.push('');
|
|
1594
|
-
}
|
|
1595
|
-
return lines;
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
/**
|
|
1599
|
-
* Load history from disk
|
|
1600
|
-
*/
|
|
1601
444
|
private loadHistory(): SessionHistory {
|
|
1602
445
|
if (existsSync(this.historyPath)) {
|
|
1603
446
|
try {
|
|
@@ -1607,35 +450,26 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1607
450
|
herror('Failed to load history:', error);
|
|
1608
451
|
}
|
|
1609
452
|
}
|
|
1610
|
-
|
|
1611
453
|
return {
|
|
1612
454
|
sessionId: this.sessionId,
|
|
1613
455
|
startedAt: new Date().toISOString(),
|
|
1614
456
|
lastActivityAt: new Date().toISOString(),
|
|
1615
457
|
totalTokens: 0,
|
|
1616
|
-
movements: []
|
|
458
|
+
movements: [],
|
|
1617
459
|
};
|
|
1618
460
|
}
|
|
1619
461
|
|
|
1620
|
-
/**
|
|
1621
|
-
* Save history to disk
|
|
1622
|
-
*/
|
|
1623
462
|
private saveHistory(): void {
|
|
1624
463
|
this.history.lastActivityAt = new Date().toISOString();
|
|
1625
464
|
writeFileSync(this.historyPath, JSON.stringify(this.history, null, 2));
|
|
1626
465
|
}
|
|
1627
466
|
|
|
1628
|
-
/**
|
|
1629
|
-
* Get session history
|
|
1630
|
-
*/
|
|
1631
467
|
getHistory(): SessionHistory {
|
|
1632
468
|
return this.history;
|
|
1633
469
|
}
|
|
1634
470
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
* gets instant feedback, then cleans up the process tree in the background.
|
|
1638
|
-
*/
|
|
471
|
+
// ========== Lifecycle ==========
|
|
472
|
+
|
|
1639
473
|
cancel(): void {
|
|
1640
474
|
this._cancelled = true;
|
|
1641
475
|
|
|
@@ -1644,8 +478,6 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1644
478
|
this.currentRunner = null;
|
|
1645
479
|
}
|
|
1646
480
|
|
|
1647
|
-
// Emit movementComplete immediately so the web UI updates without waiting
|
|
1648
|
-
// for the process tree to fully die (SIGTERM → SIGKILL can take up to 5s).
|
|
1649
481
|
if (this._isExecuting && !this._cancelCompleteEmitted) {
|
|
1650
482
|
this._cancelCompleteEmitted = true;
|
|
1651
483
|
const execStart = this._executionStartTimestamp || Date.now();
|
|
@@ -1672,40 +504,28 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1672
504
|
this.emitMovementComplete(cancelledMovement, fallbackResult, execStart, this._currentSequenceNumber);
|
|
1673
505
|
}
|
|
1674
506
|
|
|
1675
|
-
this.queueOutput('\n⚠ Execution cancelled\n');
|
|
1676
507
|
this.flushOutputQueue();
|
|
1677
508
|
}
|
|
1678
509
|
|
|
1679
|
-
/**
|
|
1680
|
-
* Cleanup queue processor on shutdown
|
|
1681
|
-
*/
|
|
1682
510
|
destroy(): void {
|
|
1683
511
|
if (this.queueTimer) {
|
|
1684
512
|
clearInterval(this.queueTimer);
|
|
1685
513
|
this.queueTimer = null;
|
|
1686
514
|
}
|
|
1687
|
-
this.flushOutputQueue();
|
|
515
|
+
this.flushOutputQueue();
|
|
1688
516
|
}
|
|
1689
517
|
|
|
1690
|
-
/**
|
|
1691
|
-
* Clear session history and reset to fresh Claude session
|
|
1692
|
-
* This resets the isFirstPrompt flag and claudeSessionId so the next prompt starts a new session
|
|
1693
|
-
*/
|
|
1694
518
|
clearHistory(): void {
|
|
1695
519
|
this.history.movements = [];
|
|
1696
520
|
this.history.totalTokens = 0;
|
|
1697
521
|
this.accumulatedKnowledge = '';
|
|
1698
|
-
this.isFirstPrompt = true;
|
|
1699
|
-
this.claudeSessionId = undefined;
|
|
1700
|
-
this.
|
|
522
|
+
this.isFirstPrompt = true;
|
|
523
|
+
this.claudeSessionId = undefined;
|
|
524
|
+
cleanupAttachments(this.options.workingDir, this.sessionId);
|
|
1701
525
|
this.saveHistory();
|
|
1702
526
|
this.emit('onSessionUpdate', this.getHistory());
|
|
1703
527
|
}
|
|
1704
528
|
|
|
1705
|
-
/**
|
|
1706
|
-
* Request user approval for a plan
|
|
1707
|
-
* Returns a promise that resolves when the user approves/rejects
|
|
1708
|
-
*/
|
|
1709
529
|
async requestApproval(plan: unknown): Promise<boolean> {
|
|
1710
530
|
return new Promise((resolve) => {
|
|
1711
531
|
this.pendingApproval = { plan, resolve };
|
|
@@ -1713,9 +533,6 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1713
533
|
});
|
|
1714
534
|
}
|
|
1715
535
|
|
|
1716
|
-
/**
|
|
1717
|
-
* Respond to approval request
|
|
1718
|
-
*/
|
|
1719
536
|
respondToApproval(approved: boolean): void {
|
|
1720
537
|
if (this.pendingApproval) {
|
|
1721
538
|
this.pendingApproval.resolve(approved);
|
|
@@ -1723,9 +540,6 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1723
540
|
}
|
|
1724
541
|
}
|
|
1725
542
|
|
|
1726
|
-
/**
|
|
1727
|
-
* Get session metadata
|
|
1728
|
-
*/
|
|
1729
543
|
getSessionInfo() {
|
|
1730
544
|
return {
|
|
1731
545
|
sessionId: this.sessionId,
|
|
@@ -1733,51 +547,28 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1733
547
|
workingDir: this.options.workingDir,
|
|
1734
548
|
totalTokens: this.history.totalTokens,
|
|
1735
549
|
tokenBudgetThreshold: this.options.tokenBudgetThreshold,
|
|
1736
|
-
movementCount: this.history.movements.length
|
|
550
|
+
movementCount: this.history.movements.length,
|
|
1737
551
|
};
|
|
1738
552
|
}
|
|
1739
553
|
|
|
1740
|
-
/**
|
|
1741
|
-
* Whether a prompt is currently executing
|
|
1742
|
-
*/
|
|
1743
554
|
get isExecuting(): boolean {
|
|
1744
555
|
return this._isExecuting;
|
|
1745
556
|
}
|
|
1746
557
|
|
|
1747
|
-
/**
|
|
1748
|
-
* Timestamp when current execution started (undefined when not executing)
|
|
1749
|
-
*/
|
|
1750
558
|
get executionStartTimestamp(): number | undefined {
|
|
1751
559
|
return this._executionStartTimestamp;
|
|
1752
560
|
}
|
|
1753
561
|
|
|
1754
|
-
/**
|
|
1755
|
-
* Get buffered execution events for replay on reconnect.
|
|
1756
|
-
* Only meaningful while isExecuting is true.
|
|
1757
|
-
*/
|
|
1758
562
|
getExecutionEventLog(): Array<{ type: string; data: unknown; timestamp: number }> {
|
|
1759
563
|
return this.executionEventLog;
|
|
1760
564
|
}
|
|
1761
565
|
|
|
1762
|
-
/**
|
|
1763
|
-
* Start a new session with fresh context
|
|
1764
|
-
* Creates a completely new session manager with isFirstPrompt=true and no claudeSessionId,
|
|
1765
|
-
* ensuring the next prompt starts a fresh Claude conversation (proper tab isolation)
|
|
1766
|
-
*/
|
|
1767
566
|
startNewSession(overrides?: Partial<ImprovisationOptions>): ImprovisationSessionManager {
|
|
1768
|
-
// Save current session
|
|
1769
567
|
this.saveHistory();
|
|
1770
|
-
|
|
1771
|
-
// Create new session manager - the new instance has:
|
|
1772
|
-
// - isFirstPrompt=true by default
|
|
1773
|
-
// - claudeSessionId=undefined by default
|
|
1774
|
-
// This means the first prompt will start a completely fresh Claude conversation
|
|
1775
|
-
const newSession = new ImprovisationSessionManager({
|
|
568
|
+
return new ImprovisationSessionManager({
|
|
1776
569
|
...this.options,
|
|
1777
570
|
sessionId: `improv-${Date.now()}`,
|
|
1778
571
|
...overrides,
|
|
1779
572
|
});
|
|
1780
|
-
|
|
1781
|
-
return newSession;
|
|
1782
573
|
}
|
|
1783
574
|
}
|