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