mstro-app 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-process.js +140 -0
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
- package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +10 -807
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
- package/dist/server/cli/headless/haiku-assessments.js +281 -0
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
- package/dist/server/cli/headless/headless-logger.d.ts +3 -2
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +28 -5
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
- package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
- package/dist/server/cli/headless/native-timeout-detector.js +99 -0
- package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +65 -457
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/improvisation-attachments.d.ts +21 -0
- package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
- package/dist/server/cli/improvisation-attachments.js +116 -0
- package/dist/server/cli/improvisation-attachments.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +52 -0
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
- package/dist/server/cli/improvisation-retry.js +434 -0
- package/dist/server/cli/improvisation-retry.js.map +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +117 -1079
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +86 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -0
- package/dist/server/cli/improvisation-types.js +10 -0
- package/dist/server/cli/improvisation-types.js.map +1 -0
- package/dist/server/cli/prompt-builders.d.ts +68 -0
- package/dist/server/cli/prompt-builders.d.ts.map +1 -0
- package/dist/server/cli/prompt-builders.js +312 -0
- package/dist/server/cli/prompt-builders.js.map +1 -0
- package/dist/server/index.js +33 -212
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-haiku.js +152 -0
- package/dist/server/mcp/bouncer-haiku.js.map +1 -0
- package/dist/server/mcp/bouncer-integration.d.ts +3 -4
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +50 -196
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-analysis.d.ts +38 -0
- package/dist/server/mcp/security-analysis.d.ts.map +1 -0
- package/dist/server/mcp/security-analysis.js +183 -0
- package/dist/server/mcp/security-analysis.js.map +1 -0
- package/dist/server/mcp/security-audit.d.ts +1 -1
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts +1 -25
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +55 -260
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/server-setup.d.ts +22 -0
- package/dist/server/server-setup.d.ts.map +1 -0
- package/dist/server/server-setup.js +101 -0
- package/dist/server/server-setup.js.map +1 -0
- package/dist/server/services/file-explorer-ops.d.ts +24 -0
- package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
- package/dist/server/services/file-explorer-ops.js +211 -0
- package/dist/server/services/file-explorer-ops.js.map +1 -0
- package/dist/server/services/files.d.ts +2 -85
- package/dist/server/services/files.d.ts.map +1 -1
- package/dist/server/services/files.js +7 -427
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +2 -1
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts +20 -0
- package/dist/server/services/plan/parser-core.d.ts.map +1 -0
- package/dist/server/services/plan/parser-core.js +350 -0
- package/dist/server/services/plan/parser-core.js.map +1 -0
- package/dist/server/services/plan/parser-migration.d.ts +5 -0
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
- package/dist/server/services/plan/parser-migration.js +124 -0
- package/dist/server/services/plan/parser-migration.js.map +1 -0
- package/dist/server/services/plan/parser.d.ts +0 -8
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +50 -569
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts +2 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +2 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +2 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts +24 -0
- package/dist/server/services/platform-credentials.d.ts.map +1 -0
- package/dist/server/services/platform-credentials.js +68 -0
- package/dist/server/services/platform-credentials.js.map +1 -0
- package/dist/server/services/platform.d.ts +1 -31
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +10 -119
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +7 -97
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +53 -266
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/terminal/pty-utils.d.ts +57 -0
- package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
- package/dist/server/services/terminal/pty-utils.js +141 -0
- package/dist/server/services/terminal/pty-utils.js.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.js +153 -0
- package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-search-handlers.js +238 -0
- package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-utils.js +3 -3
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-branch-handlers.js +110 -0
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.js +123 -0
- package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-handlers.d.ts +2 -31
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +35 -541
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-log-handlers.js +128 -0
- package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +13 -53
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-tag-handlers.js +76 -0
- package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-utils.d.ts +43 -0
- package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
- package/dist/server/services/websocket/git-utils.js +201 -0
- package/dist/server/services/websocket/git-utils.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +37 -126
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-board-handlers.js +218 -0
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +6 -925
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-helpers.js +199 -0
- package/dist/server/services/websocket/plan-helpers.js.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
- package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-complexity.js +262 -0
- package/dist/server/services/websocket/quality-complexity.js.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.js +140 -0
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +34 -346
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-linting.d.ts +9 -0
- package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-linting.js +178 -0
- package/dist/server/services/websocket/quality-linting.js.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.js +206 -0
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-service.d.ts +3 -51
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +9 -651
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +23 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-tools.js +208 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -0
- package/dist/server/services/websocket/quality-types.d.ts +59 -0
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-types.js +101 -0
- package/dist/server/services/websocket/quality-types.js.map +1 -0
- package/dist/server/services/websocket/session-handlers.d.ts +3 -4
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +3 -378
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.d.ts +4 -0
- package/dist/server/services/websocket/session-history.d.ts.map +1 -0
- package/dist/server/services/websocket/session-history.js +208 -0
- package/dist/server/services/websocket/session-history.js.map +1 -0
- package/dist/server/services/websocket/session-initialization.d.ts +5 -0
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
- package/dist/server/services/websocket/session-initialization.js +163 -0
- package/dist/server/services/websocket/session-initialization.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +12 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-process.ts +204 -0
- package/server/cli/headless/claude-invoker-stall.ts +164 -0
- package/server/cli/headless/claude-invoker-stream.ts +353 -0
- package/server/cli/headless/claude-invoker-tools.ts +187 -0
- package/server/cli/headless/claude-invoker.ts +15 -1096
- package/server/cli/headless/haiku-assessments.ts +365 -0
- package/server/cli/headless/headless-logger.ts +26 -5
- package/server/cli/headless/native-timeout-detector.ts +117 -0
- package/server/cli/headless/stall-assessor.ts +65 -618
- package/server/cli/improvisation-attachments.ts +148 -0
- package/server/cli/improvisation-retry.ts +602 -0
- package/server/cli/improvisation-session-manager.ts +140 -1349
- package/server/cli/improvisation-types.ts +98 -0
- package/server/cli/prompt-builders.ts +370 -0
- package/server/index.ts +35 -246
- package/server/mcp/bouncer-haiku.ts +182 -0
- package/server/mcp/bouncer-integration.ts +87 -248
- package/server/mcp/security-analysis.ts +217 -0
- package/server/mcp/security-audit.ts +1 -1
- package/server/mcp/security-patterns.ts +60 -283
- package/server/server-setup.ts +114 -0
- package/server/services/file-explorer-ops.ts +293 -0
- package/server/services/files.ts +20 -532
- package/server/services/plan/composer.ts +2 -1
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/parser-core.ts +406 -0
- package/server/services/plan/parser-migration.ts +128 -0
- package/server/services/plan/parser.ts +52 -620
- package/server/services/plan/review-gate.ts +4 -2
- package/server/services/plan/types.ts +2 -0
- package/server/services/platform-credentials.ts +83 -0
- package/server/services/platform.ts +15 -141
- package/server/services/terminal/pty-manager.ts +66 -313
- package/server/services/terminal/pty-utils.ts +176 -0
- package/server/services/websocket/file-definition-handlers.ts +165 -0
- package/server/services/websocket/file-explorer-handlers.ts +37 -452
- package/server/services/websocket/file-search-handlers.ts +291 -0
- package/server/services/websocket/file-utils.ts +3 -3
- package/server/services/websocket/git-branch-handlers.ts +130 -0
- package/server/services/websocket/git-diff-handlers.ts +140 -0
- package/server/services/websocket/git-handlers.ts +40 -625
- package/server/services/websocket/git-log-handlers.ts +149 -0
- package/server/services/websocket/git-pr-handlers.ts +17 -62
- package/server/services/websocket/git-tag-handlers.ts +91 -0
- package/server/services/websocket/git-utils.ts +230 -0
- package/server/services/websocket/handler.ts +39 -126
- package/server/services/websocket/plan-board-handlers.ts +277 -0
- package/server/services/websocket/plan-execution-handlers.ts +184 -0
- package/server/services/websocket/plan-handlers.ts +8 -1114
- package/server/services/websocket/plan-helpers.ts +215 -0
- package/server/services/websocket/plan-issue-handlers.ts +204 -0
- package/server/services/websocket/plan-sprint-handlers.ts +252 -0
- package/server/services/websocket/quality-complexity.ts +294 -0
- package/server/services/websocket/quality-fix-agent.ts +181 -0
- package/server/services/websocket/quality-handlers.ts +36 -404
- package/server/services/websocket/quality-linting.ts +187 -0
- package/server/services/websocket/quality-review-agent.ts +246 -0
- package/server/services/websocket/quality-service.ts +11 -762
- package/server/services/websocket/quality-tools.ts +209 -0
- package/server/services/websocket/quality-types.ts +169 -0
- package/server/services/websocket/session-handlers.ts +5 -437
- package/server/services/websocket/session-history.ts +222 -0
- package/server/services/websocket/session-initialization.ts +209 -0
- package/server/services/websocket/types.ts +17 -0
|
@@ -4,1073 +4,28 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Claude Invoker
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Orchestrates spawning and managing Claude CLI processes.
|
|
8
|
+
* Stream handling, stall detection, tool tracking, and process management
|
|
9
|
+
* are delegated to focused sub-modules.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { ToolWatchdog } from './tool-watchdog.js';
|
|
19
|
-
import type {
|
|
20
|
-
ExecutionResult,
|
|
21
|
-
ResolvedHeadlessConfig,
|
|
22
|
-
ToolUseAccumulator,
|
|
23
|
-
ToolUseEvent,
|
|
24
|
-
} from './types.js';
|
|
25
|
-
|
|
26
|
-
/** Parsed JSON from Claude CLI stream — structure varies by event type */
|
|
27
|
-
// biome-ignore lint/suspicious/noExplicitAny: external CLI stream JSON with heterogeneous shapes
|
|
28
|
-
type StreamJson = any;
|
|
12
|
+
import type { ChildProcess } from 'node:child_process';
|
|
13
|
+
import { buildCloseResult, handleSpawnError, spawnAndRegister } from './claude-invoker-process.js';
|
|
14
|
+
import { runStallCheckTick, type StallState } from './claude-invoker-stall.js';
|
|
15
|
+
import { classifyUnmatchedStderr, processStreamLines, type StreamHandlerContext, verboseLog } from './claude-invoker-stream.js';
|
|
16
|
+
import { setupToolTracking } from './claude-invoker-tools.js';
|
|
17
|
+
import { NativeTimeoutDetector } from './native-timeout-detector.js';
|
|
18
|
+
import { detectErrorInStderr } from './output-utils.js';
|
|
19
|
+
import type { ExecutionResult, ResolvedHeadlessConfig } from './types.js';
|
|
29
20
|
|
|
30
21
|
export interface ClaudeInvokerOptions {
|
|
31
22
|
config: ResolvedHeadlessConfig;
|
|
32
23
|
runningProcesses: Map<number, ChildProcess>;
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
// ========== Signal Helpers ==========
|
|
36
|
-
|
|
37
|
-
/** Map a Node.js signal name to its numeric value for exit code computation */
|
|
38
|
-
function signalToNumber(signal: string): number | undefined {
|
|
39
|
-
const map: Record<string, number> = {
|
|
40
|
-
SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6,
|
|
41
|
-
SIGKILL: 9, SIGTERM: 15, SIGUSR1: 10, SIGUSR2: 12,
|
|
42
|
-
};
|
|
43
|
-
return map[signal];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ========== Stall Detection Helpers ==========
|
|
47
|
-
|
|
48
|
-
/** Summarize a tool's input for stall assessment context */
|
|
49
|
-
function summarizeToolInput(input: Record<string, unknown>): string | undefined {
|
|
50
|
-
try {
|
|
51
|
-
if (input.description) {
|
|
52
|
-
return String(input.description).slice(0, 200);
|
|
53
|
-
}
|
|
54
|
-
if (input.prompt) {
|
|
55
|
-
return String(input.prompt).slice(0, 200);
|
|
56
|
-
}
|
|
57
|
-
if (input.command) {
|
|
58
|
-
return String(input.command).slice(0, 200);
|
|
59
|
-
}
|
|
60
|
-
if (input.pattern) {
|
|
61
|
-
return `pattern: ${String(input.pattern).slice(0, 100)}`;
|
|
62
|
-
}
|
|
63
|
-
return JSON.stringify(input).slice(0, 200);
|
|
64
|
-
} catch {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Terminate a stalled process: SIGTERM then SIGKILL after 5s */
|
|
70
|
-
function terminateStallProcess(
|
|
71
|
-
claudeProcess: ChildProcess,
|
|
72
|
-
interval: ReturnType<typeof setInterval>,
|
|
73
|
-
config: ResolvedHeadlessConfig,
|
|
74
|
-
message: string,
|
|
75
|
-
): void {
|
|
76
|
-
clearInterval(interval);
|
|
77
|
-
config.outputCallback?.(message);
|
|
78
|
-
if (claudeProcess.pid) killProcessGroup(claudeProcess.pid, 'SIGTERM');
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
if (!claudeProcess.killed && claudeProcess.pid) {
|
|
81
|
-
killProcessGroup(claudeProcess.pid, 'SIGKILL');
|
|
82
|
-
}
|
|
83
|
-
}, 5000);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface StallAssessmentParams {
|
|
87
|
-
stallCtx: StallContext;
|
|
88
|
-
config: ResolvedHeadlessConfig;
|
|
89
|
-
now: number;
|
|
90
|
-
extensionsGranted: number;
|
|
91
|
-
maxExtensions: number;
|
|
92
|
-
toolWatchdogActive?: boolean;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Run stall assessment and return updated state if extended, null otherwise */
|
|
96
|
-
async function runStallAssessment(
|
|
97
|
-
params: StallAssessmentParams,
|
|
98
|
-
): Promise<{ extensionsGranted: number; currentKillDeadline: number } | null> {
|
|
99
|
-
const { stallCtx, config, now, extensionsGranted, maxExtensions, toolWatchdogActive } = params;
|
|
100
|
-
try {
|
|
101
|
-
const verdict = await assessStall(stallCtx, config.claudeCommand, config.verbose, toolWatchdogActive);
|
|
102
|
-
if (verdict.action === 'extend') {
|
|
103
|
-
const newExtensions = extensionsGranted + 1;
|
|
104
|
-
const elapsedMin = Math.round(stallCtx.elapsedTotalMs / 60_000);
|
|
105
|
-
const pendingNames = stallCtx.pendingToolNames ?? new Set<string>();
|
|
106
|
-
|
|
107
|
-
// Emit a progress message instead of a scary stall warning.
|
|
108
|
-
// Task subagents and Agent Teams leads get friendlier messages since long silence is expected.
|
|
109
|
-
const isAgentTeamsLead = verdict.reason.includes('Agent Teams lead');
|
|
110
|
-
if (pendingNames.has('Task') || isAgentTeamsLead) {
|
|
111
|
-
config.outputCallback?.(
|
|
112
|
-
`\n[[MSTRO_STALL_EXTENDED]] ${isAgentTeamsLead ? 'Teammates still working' : 'Task subagent still running'} (${elapsedMin} min elapsed). ${verdict.reason}.\n`
|
|
113
|
-
);
|
|
114
|
-
} else {
|
|
115
|
-
config.outputCallback?.(
|
|
116
|
-
`\n[[MSTRO_STALL_EXTENDED]] Process still working (${elapsedMin} min elapsed). ${verdict.reason}. Extension ${newExtensions}/${maxExtensions}.\n`
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
if (config.verbose) {
|
|
120
|
-
hlog(`[STALL] Extended by ${Math.round(verdict.extensionMs / 60_000)} min: ${verdict.reason}`);
|
|
121
|
-
}
|
|
122
|
-
return { extensionsGranted: newExtensions, currentKillDeadline: now + verdict.extensionMs };
|
|
123
|
-
}
|
|
124
|
-
config.outputCallback?.(
|
|
125
|
-
`\n[[MSTRO_STALL_CONFIRMED]] Assessment: process likely stalled. ${verdict.reason}.\n`
|
|
126
|
-
);
|
|
127
|
-
if (config.verbose) {
|
|
128
|
-
hlog(`[STALL] Assessment says stalled: ${verdict.reason}`);
|
|
129
|
-
}
|
|
130
|
-
} catch (err) {
|
|
131
|
-
if (config.verbose) {
|
|
132
|
-
hlog(`[STALL] Assessment error: ${err}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ========== Native Timeout Detection ==========
|
|
139
|
-
|
|
140
|
-
/** Regex matching Claude Code's internal tool timeout messages */
|
|
141
|
-
const NATIVE_TIMEOUT_PATTERN = /^(\w+) timed out — (continuing|retrying) with (\d+) results? preserved$/;
|
|
142
|
-
|
|
143
|
-
/** Quick prefix check: does incomplete text look like it might be a timeout?
|
|
144
|
-
* Matches any capitalized tool name followed by " timed" — no hardcoded set
|
|
145
|
-
* needed because the full NATIVE_TIMEOUT_PATTERN validates on the next chunk. */
|
|
146
|
-
const TIMEOUT_PREFIX_PATTERN = /^[A-Z]\w* timed/;
|
|
147
|
-
|
|
148
|
-
interface NativeTimeoutEvent {
|
|
149
|
-
toolName: string;
|
|
150
|
-
action: 'continuing' | 'retrying';
|
|
151
|
-
preservedCount: number;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Detects Claude Code's internal tool timeout messages in the text stream.
|
|
156
|
-
*
|
|
157
|
-
* Buffers text at newline boundaries to detect complete timeout lines.
|
|
158
|
-
* Non-matching text is forwarded immediately to minimize streaming latency.
|
|
159
|
-
*/
|
|
160
|
-
class NativeTimeoutDetector {
|
|
161
|
-
private lineBuffer = '';
|
|
162
|
-
private detectedTimeouts: NativeTimeoutEvent[] = [];
|
|
163
|
-
/** Text buffered after native timeouts — held back from streaming until context is assessed */
|
|
164
|
-
private postTimeoutBuffer = '';
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Process a text_delta chunk.
|
|
168
|
-
* Returns passthrough text (for outputCallback) and any detected timeouts.
|
|
169
|
-
*
|
|
170
|
-
* After the first native timeout is detected, subsequent passthrough text
|
|
171
|
-
* is held in postTimeoutBuffer instead of returned as passthrough. This
|
|
172
|
-
* prevents confused "What were you working on?" responses from streaming
|
|
173
|
-
* to the user before context loss can be assessed.
|
|
174
|
-
*/
|
|
175
|
-
processChunk(text: string): { passthrough: string; timeouts: NativeTimeoutEvent[] } {
|
|
176
|
-
const timeouts: NativeTimeoutEvent[] = [];
|
|
177
|
-
let passthrough = '';
|
|
178
|
-
|
|
179
|
-
this.lineBuffer += text;
|
|
180
|
-
|
|
181
|
-
const lines = this.lineBuffer.split('\n');
|
|
182
|
-
const incomplete = lines.pop() ?? '';
|
|
183
|
-
|
|
184
|
-
for (const line of lines) {
|
|
185
|
-
const trimmed = line.trim();
|
|
186
|
-
const match = trimmed.match(NATIVE_TIMEOUT_PATTERN);
|
|
187
|
-
|
|
188
|
-
if (match) {
|
|
189
|
-
const event: NativeTimeoutEvent = {
|
|
190
|
-
toolName: match[1],
|
|
191
|
-
action: match[2] as 'continuing' | 'retrying',
|
|
192
|
-
preservedCount: parseInt(match[3], 10),
|
|
193
|
-
};
|
|
194
|
-
timeouts.push(event);
|
|
195
|
-
this.detectedTimeouts.push(event);
|
|
196
|
-
// Suppress this line from passthrough — replaced by structured marker
|
|
197
|
-
} else {
|
|
198
|
-
passthrough += `${line}\n`;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Handle incomplete trailing text
|
|
203
|
-
if (incomplete) {
|
|
204
|
-
if (TIMEOUT_PREFIX_PATTERN.test(incomplete)) {
|
|
205
|
-
// Looks like the start of a timeout message — hold it
|
|
206
|
-
this.lineBuffer = incomplete;
|
|
207
|
-
} else {
|
|
208
|
-
passthrough += incomplete;
|
|
209
|
-
this.lineBuffer = '';
|
|
210
|
-
}
|
|
211
|
-
} else {
|
|
212
|
-
this.lineBuffer = '';
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// After native timeouts, buffer passthrough text instead of returning it.
|
|
216
|
-
// The session manager will assess context loss and either flush or discard.
|
|
217
|
-
if (this.detectedTimeouts.length > 0 && passthrough) {
|
|
218
|
-
this.postTimeoutBuffer += passthrough;
|
|
219
|
-
passthrough = '';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return { passthrough, timeouts };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Flush any held buffer (call on stream end).
|
|
226
|
-
* Also checks remaining buffer for timeout patterns so the last
|
|
227
|
-
* timeout message (without trailing newline) is always counted.
|
|
228
|
-
*/
|
|
229
|
-
flush(): string {
|
|
230
|
-
const remaining = this.lineBuffer;
|
|
231
|
-
this.lineBuffer = '';
|
|
232
|
-
|
|
233
|
-
// Check if the unflushed buffer IS a timeout message
|
|
234
|
-
if (remaining) {
|
|
235
|
-
const trimmed = remaining.trim();
|
|
236
|
-
const match = trimmed.match(NATIVE_TIMEOUT_PATTERN);
|
|
237
|
-
if (match) {
|
|
238
|
-
this.detectedTimeouts.push({
|
|
239
|
-
toolName: match[1],
|
|
240
|
-
action: match[2] as 'continuing' | 'retrying',
|
|
241
|
-
preservedCount: parseInt(match[3], 10),
|
|
242
|
-
});
|
|
243
|
-
// Return empty — this was a timeout message, not user-visible text
|
|
244
|
-
return '';
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return remaining;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Get count of detected timeouts */
|
|
252
|
-
get timeoutCount(): number {
|
|
253
|
-
return this.detectedTimeouts.length;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Get buffered post-timeout text (for session manager to flush or discard) */
|
|
257
|
-
get bufferedPostTimeoutOutput(): string {
|
|
258
|
-
return this.postTimeoutBuffer;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ========== Stream Event Handlers ==========
|
|
263
|
-
|
|
264
|
-
interface StreamHandlerContext {
|
|
265
|
-
config: ResolvedHeadlessConfig;
|
|
266
|
-
accumulatedAssistantResponse: string;
|
|
267
|
-
accumulatedThinking: string;
|
|
268
|
-
accumulatedToolUse: ToolUseAccumulator[];
|
|
269
|
-
toolInputBuffers: Map<number, { name: string; id: string; inputJson: string; startTime: number }>;
|
|
270
|
-
nativeTimeoutDetector: NativeTimeoutDetector;
|
|
271
|
-
/** When true, assistant text is buffered instead of forwarded to outputCallback.
|
|
272
|
-
* Active during resume mode until thinking/tool activity confirms Claude has context. */
|
|
273
|
-
resumeAssessmentActive: boolean;
|
|
274
|
-
/** Buffered assistant text during resume assessment */
|
|
275
|
-
resumeAssessmentBuffer: string;
|
|
276
|
-
/** Cumulative API token usage from message_start/message_delta events */
|
|
277
|
-
apiTokenUsage: { inputTokens: number; outputTokens: number };
|
|
278
|
-
/** Tracks cumulative output_tokens within the current step (message_delta is cumulative per-step) */
|
|
279
|
-
currentStepOutputTokens: number;
|
|
280
|
-
/** Timestamp of the last token usage change (tokens still flowing = process alive) */
|
|
281
|
-
lastTokenActivityTime: number;
|
|
282
|
-
/** Claude Code result event stop_reason (e.g., 'end_turn', 'max_tokens') */
|
|
283
|
-
stopReason?: string;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function handleSessionCapture(
|
|
287
|
-
parsed: StreamJson,
|
|
288
|
-
captured: { claudeSessionId?: string }
|
|
289
|
-
): void {
|
|
290
|
-
if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.session_id) {
|
|
291
|
-
captured.claudeSessionId = parsed.session_id;
|
|
292
|
-
}
|
|
293
|
-
if (parsed.type === 'result' && parsed.session_id && !captured.claudeSessionId) {
|
|
294
|
-
captured.claudeSessionId = parsed.session_id;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function handleThinkingDelta(event: StreamJson, ctx: StreamHandlerContext): string {
|
|
299
|
-
if (
|
|
300
|
-
event.type !== 'content_block_delta' ||
|
|
301
|
-
event.delta?.type !== 'thinking_delta' ||
|
|
302
|
-
!event.delta?.thinking
|
|
303
|
-
) {
|
|
304
|
-
return ctx.accumulatedThinking;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Thinking activity confirms Claude has context — flush resume buffer
|
|
308
|
-
if (ctx.resumeAssessmentActive) {
|
|
309
|
-
ctx.resumeAssessmentActive = false;
|
|
310
|
-
if (ctx.resumeAssessmentBuffer) {
|
|
311
|
-
ctx.config.outputCallback?.(ctx.resumeAssessmentBuffer);
|
|
312
|
-
ctx.resumeAssessmentBuffer = '';
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const thinking = event.delta.thinking;
|
|
317
|
-
const updated = ctx.accumulatedThinking + thinking;
|
|
318
|
-
|
|
319
|
-
if (ctx.config.thinkingCallback) {
|
|
320
|
-
ctx.config.thinkingCallback(thinking);
|
|
321
|
-
} else if (ctx.config.outputCallback) {
|
|
322
|
-
ctx.config.outputCallback(thinking);
|
|
323
|
-
} else {
|
|
324
|
-
process.stdout.write(thinking);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return updated;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function handleTextDelta(event: StreamJson, ctx: StreamHandlerContext): string {
|
|
331
|
-
if (
|
|
332
|
-
event.type !== 'content_block_delta' ||
|
|
333
|
-
event.delta?.type !== 'text_delta' ||
|
|
334
|
-
!event.delta?.text
|
|
335
|
-
) {
|
|
336
|
-
return ctx.accumulatedAssistantResponse;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const text = event.delta.text;
|
|
340
|
-
|
|
341
|
-
// Always accumulate raw text for checkpoint context
|
|
342
|
-
const updated = ctx.accumulatedAssistantResponse + text;
|
|
343
|
-
|
|
344
|
-
// Route through native timeout detector to intercept Claude Code's internal timeout messages
|
|
345
|
-
const { passthrough, timeouts } = ctx.nativeTimeoutDetector.processChunk(text);
|
|
346
|
-
|
|
347
|
-
// Emit structured markers for detected native timeouts
|
|
348
|
-
for (const timeout of timeouts) {
|
|
349
|
-
ctx.config.outputCallback?.(
|
|
350
|
-
`\n[[MSTRO_NATIVE_TIMEOUT]] ${timeout.toolName} timed out \u2014 ${timeout.action} with ${timeout.preservedCount} results preserved\n`
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// When resume assessment is active, buffer text instead of forwarding.
|
|
355
|
-
// This prevents confused "What were you working on?" responses from streaming
|
|
356
|
-
// to the user before we can assess whether Claude retained context.
|
|
357
|
-
if (ctx.resumeAssessmentActive) {
|
|
358
|
-
if (passthrough) {
|
|
359
|
-
ctx.resumeAssessmentBuffer += passthrough;
|
|
360
|
-
}
|
|
361
|
-
return updated;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Forward non-timeout text to output
|
|
365
|
-
if (passthrough && ctx.config.outputCallback) {
|
|
366
|
-
ctx.config.outputCallback(passthrough);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return updated;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function handleToolStart(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
373
|
-
if (
|
|
374
|
-
event.type !== 'content_block_start' ||
|
|
375
|
-
event.content_block?.type !== 'tool_use'
|
|
376
|
-
) {
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Tool activity confirms Claude has context — flush resume buffer
|
|
381
|
-
if (ctx.resumeAssessmentActive) {
|
|
382
|
-
ctx.resumeAssessmentActive = false;
|
|
383
|
-
if (ctx.resumeAssessmentBuffer) {
|
|
384
|
-
ctx.config.outputCallback?.(ctx.resumeAssessmentBuffer);
|
|
385
|
-
ctx.resumeAssessmentBuffer = '';
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const toolName = event.content_block.name;
|
|
390
|
-
const toolId = event.content_block.id;
|
|
391
|
-
const index = event.index;
|
|
392
|
-
|
|
393
|
-
ctx.toolInputBuffers.set(index, {
|
|
394
|
-
name: toolName,
|
|
395
|
-
id: toolId,
|
|
396
|
-
inputJson: '',
|
|
397
|
-
startTime: Date.now()
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
if (ctx.config.toolUseCallback) {
|
|
401
|
-
ctx.config.toolUseCallback({ type: 'tool_start', toolName, toolId, index });
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function handleToolInputDelta(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
406
|
-
if (
|
|
407
|
-
event.type !== 'content_block_delta' ||
|
|
408
|
-
event.delta?.type !== 'input_json_delta'
|
|
409
|
-
) {
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const index = event.index;
|
|
414
|
-
const partialJson = event.delta.partial_json;
|
|
415
|
-
|
|
416
|
-
const toolBuffer = ctx.toolInputBuffers.get(index);
|
|
417
|
-
if (toolBuffer) {
|
|
418
|
-
toolBuffer.inputJson += partialJson;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (ctx.config.toolUseCallback) {
|
|
422
|
-
ctx.config.toolUseCallback({ type: 'tool_input_delta', partialJson, index });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function handleToolComplete(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
427
|
-
if (event.type !== 'content_block_stop') {
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const index = event.index;
|
|
432
|
-
const toolBuffer = ctx.toolInputBuffers.get(index);
|
|
433
|
-
if (!toolBuffer) {
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
let completeInput: Record<string, unknown> = {};
|
|
438
|
-
try {
|
|
439
|
-
completeInput = JSON.parse(toolBuffer.inputJson);
|
|
440
|
-
} catch (_e) {
|
|
441
|
-
// Input might not be valid JSON yet
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
ctx.accumulatedToolUse.push({
|
|
445
|
-
toolName: toolBuffer.name,
|
|
446
|
-
toolId: toolBuffer.id,
|
|
447
|
-
toolInput: completeInput,
|
|
448
|
-
startTime: toolBuffer.startTime
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// Clean up the input buffer — it's no longer needed after accumulation
|
|
452
|
-
ctx.toolInputBuffers.delete(index);
|
|
453
|
-
|
|
454
|
-
if (ctx.config.toolUseCallback) {
|
|
455
|
-
ctx.config.toolUseCallback({
|
|
456
|
-
type: 'tool_complete',
|
|
457
|
-
toolName: toolBuffer.name,
|
|
458
|
-
toolId: toolBuffer.id,
|
|
459
|
-
index,
|
|
460
|
-
completeInput
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** Accumulate input tokens from a message_start event. Returns true if any tokens were added. */
|
|
466
|
-
function handleMessageStartTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
|
|
467
|
-
if (event.type !== 'message_start' || !event.message?.usage) return false;
|
|
468
|
-
const usage = event.message.usage;
|
|
469
|
-
ctx.currentStepOutputTokens = 0;
|
|
470
|
-
let changed = false;
|
|
471
|
-
if (typeof usage.input_tokens === 'number') {
|
|
472
|
-
ctx.apiTokenUsage.inputTokens += usage.input_tokens;
|
|
473
|
-
changed = true;
|
|
474
|
-
}
|
|
475
|
-
if (typeof usage.cache_creation_input_tokens === 'number') {
|
|
476
|
-
ctx.apiTokenUsage.inputTokens += usage.cache_creation_input_tokens;
|
|
477
|
-
changed = true;
|
|
478
|
-
}
|
|
479
|
-
if (typeof usage.cache_read_input_tokens === 'number') {
|
|
480
|
-
ctx.apiTokenUsage.inputTokens += usage.cache_read_input_tokens;
|
|
481
|
-
changed = true;
|
|
482
|
-
}
|
|
483
|
-
verboseLog(ctx.config.verbose,
|
|
484
|
-
`[TOKENS] message_start: input=${usage.input_tokens ?? 0} cache_create=${usage.cache_creation_input_tokens ?? 0} cache_read=${usage.cache_read_input_tokens ?? 0} → total_input=${ctx.apiTokenUsage.inputTokens}`);
|
|
485
|
-
return changed;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Accumulate output tokens from a message_delta event. Returns true if any tokens were added.
|
|
489
|
-
* message_delta carries CUMULATIVE output token count for the current step.
|
|
490
|
-
* Per Anthropic docs: "The token counts shown in the usage field of the
|
|
491
|
-
* message_delta event are cumulative" and there can be "one or more message_delta
|
|
492
|
-
* events" per message. We track the delta from the previous value to avoid
|
|
493
|
-
* double-counting when multiple message_delta events fire per step. */
|
|
494
|
-
function handleMessageDeltaTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
|
|
495
|
-
if (event.type !== 'message_delta' || !event.usage) return false;
|
|
496
|
-
if (typeof event.usage.output_tokens !== 'number') return false;
|
|
497
|
-
const increment = event.usage.output_tokens - ctx.currentStepOutputTokens;
|
|
498
|
-
verboseLog(ctx.config.verbose,
|
|
499
|
-
`[TOKENS] message_delta: output=${event.usage.output_tokens} (step_prev=${ctx.currentStepOutputTokens} increment=${increment}) → total_output=${ctx.apiTokenUsage.outputTokens + Math.max(increment, 0)}`);
|
|
500
|
-
if (increment <= 0) return false;
|
|
501
|
-
ctx.apiTokenUsage.outputTokens += increment;
|
|
502
|
-
ctx.currentStepOutputTokens = event.usage.output_tokens;
|
|
503
|
-
return true;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function handleTokenUsage(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
507
|
-
const changed = handleMessageStartTokens(event, ctx) || handleMessageDeltaTokens(event, ctx);
|
|
508
|
-
if (changed) {
|
|
509
|
-
ctx.lastTokenActivityTime = Date.now();
|
|
510
|
-
ctx.config.tokenUsageCallback?.({ ...ctx.apiTokenUsage });
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Extract definitive token usage from the result event emitted at the end of a Claude session.
|
|
516
|
-
* The result event's `usage` field contains the authoritative total — it overrides any
|
|
517
|
-
* accumulated stream-based counts which may be incomplete (e.g., when extended thinking
|
|
518
|
-
* suppresses stream_event emissions).
|
|
519
|
-
*/
|
|
520
|
-
function handleResultTokenUsage(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
521
|
-
if (!parsed.usage) return;
|
|
522
|
-
const u = parsed.usage;
|
|
523
|
-
const input = (typeof u.input_tokens === 'number' ? u.input_tokens : 0)
|
|
524
|
-
+ (typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0)
|
|
525
|
-
+ (typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0);
|
|
526
|
-
const output = typeof u.output_tokens === 'number' ? u.output_tokens : 0;
|
|
527
|
-
|
|
528
|
-
if (input > 0 || output > 0) {
|
|
529
|
-
verboseLog(ctx.config.verbose,
|
|
530
|
-
`[TOKENS] Result event usage: input=${input} output=${output} ` +
|
|
531
|
-
`(stream accumulated: input=${ctx.apiTokenUsage.inputTokens} output=${ctx.apiTokenUsage.outputTokens})`);
|
|
532
|
-
// Replace with authoritative counts from the result event
|
|
533
|
-
ctx.apiTokenUsage = { inputTokens: input, outputTokens: output };
|
|
534
|
-
ctx.lastTokenActivityTime = Date.now();
|
|
535
|
-
ctx.config.tokenUsageCallback?.({ ...ctx.apiTokenUsage });
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function handleToolResult(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
540
|
-
if (parsed.type !== 'user' || !parsed.message?.content) {
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
for (const content of parsed.message.content) {
|
|
545
|
-
if (content.type !== 'tool_result') {
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const toolId = content.tool_use_id;
|
|
550
|
-
const result = content.content;
|
|
551
|
-
const isError = content.is_error || false;
|
|
552
|
-
const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
|
|
553
|
-
|
|
554
|
-
const toolEntry = ctx.accumulatedToolUse.find(t => t.toolId === toolId);
|
|
555
|
-
if (toolEntry) {
|
|
556
|
-
toolEntry.result = resultStr;
|
|
557
|
-
toolEntry.isError = isError;
|
|
558
|
-
toolEntry.duration = Date.now() - toolEntry.startTime;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (ctx.config.toolUseCallback) {
|
|
562
|
-
ctx.config.toolUseCallback({ type: 'tool_result', toolId, result: resultStr, isError });
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function processStreamLines(
|
|
568
|
-
buffer: string,
|
|
569
|
-
sessionCapture: { claudeSessionId?: string },
|
|
570
|
-
ctx: StreamHandlerContext
|
|
571
|
-
): string {
|
|
572
|
-
const lines = buffer.split('\n');
|
|
573
|
-
const remainder = lines.pop() || '';
|
|
574
|
-
|
|
575
|
-
for (const line of lines) {
|
|
576
|
-
if (!line.trim()) continue;
|
|
577
|
-
try {
|
|
578
|
-
const parsed = JSON.parse(line);
|
|
579
|
-
handleSessionCapture(parsed, sessionCapture);
|
|
580
|
-
processStreamEvent(parsed, ctx);
|
|
581
|
-
} catch (_e) {
|
|
582
|
-
// Ignore parse errors
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
return remainder;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function processStreamEvent(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
590
|
-
// Handle error events from Claude CLI (API errors, model errors, etc.)
|
|
591
|
-
if (parsed.type === 'error') {
|
|
592
|
-
const errorMessage = parsed.error?.message || parsed.message || JSON.stringify(parsed);
|
|
593
|
-
ctx.config.outputCallback?.(`\n[[MSTRO_ERROR:CLAUDE_ERROR]] ${errorMessage}\n`);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Handle result events — extract definitive token usage, stop_reason, and surface errors
|
|
598
|
-
if (parsed.type === 'result') {
|
|
599
|
-
handleResultTokenUsage(parsed, ctx);
|
|
600
|
-
if (parsed.stop_reason) {
|
|
601
|
-
ctx.stopReason = parsed.stop_reason;
|
|
602
|
-
}
|
|
603
|
-
if (parsed.is_error) {
|
|
604
|
-
const errorMessage = parsed.error || parsed.result || 'Unknown error in result';
|
|
605
|
-
ctx.config.outputCallback?.(`\n[[MSTRO_ERROR:CLAUDE_RESULT_ERROR]] ${errorMessage}\n`);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (parsed.type === 'stream_event' && parsed.event) {
|
|
611
|
-
const event = parsed.event;
|
|
612
|
-
ctx.accumulatedThinking = handleThinkingDelta(event, ctx);
|
|
613
|
-
ctx.accumulatedAssistantResponse = handleTextDelta(event, ctx);
|
|
614
|
-
handleToolStart(event, ctx);
|
|
615
|
-
handleToolInputDelta(event, ctx);
|
|
616
|
-
handleToolComplete(event, ctx);
|
|
617
|
-
handleTokenUsage(event, ctx);
|
|
618
|
-
}
|
|
619
|
-
handleToolResult(parsed, ctx);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// ========== Close Handler Helpers ==========
|
|
623
|
-
|
|
624
|
-
/** Flush native timeout detector buffers and return post-timeout output if any */
|
|
625
|
-
function flushNativeTimeoutBuffers(ctx: StreamHandlerContext): string | undefined {
|
|
626
|
-
const remaining = ctx.nativeTimeoutDetector.flush();
|
|
627
|
-
const buffered = ctx.nativeTimeoutDetector.bufferedPostTimeoutOutput;
|
|
628
|
-
const postTimeout = (buffered + remaining) || undefined;
|
|
629
|
-
|
|
630
|
-
// Only flush remaining text if there were no native timeouts
|
|
631
|
-
// (when there are timeouts, the session manager decides what to show)
|
|
632
|
-
if (!postTimeout && remaining) {
|
|
633
|
-
ctx.config.outputCallback?.(remaining);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return postTimeout;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/** Classify unmatched stderr via Haiku when process exits with error */
|
|
640
|
-
async function classifyUnmatchedStderr(
|
|
641
|
-
stderr: string,
|
|
642
|
-
errorAlreadySurfaced: boolean,
|
|
643
|
-
code: number | null,
|
|
644
|
-
config: ResolvedHeadlessConfig,
|
|
645
|
-
): Promise<void> {
|
|
646
|
-
if (!stderr || errorAlreadySurfaced || code === 0) return;
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
const classified = await classifyError(stderr, config.claudeCommand, config.verbose);
|
|
650
|
-
if (classified) {
|
|
651
|
-
config.outputCallback?.(`\n[[MSTRO_ERROR:${classified.errorCode}]] ${classified.message}\n`);
|
|
652
|
-
}
|
|
653
|
-
} catch {
|
|
654
|
-
// Haiku classification failed — proceed without it
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// ========== Error Handling ==========
|
|
659
|
-
|
|
660
|
-
const SPAWN_ERROR_MAP: Record<string, { code: string; message: string }> = {
|
|
661
|
-
ENOENT: {
|
|
662
|
-
code: 'CLAUDE_NOT_INSTALLED',
|
|
663
|
-
message: 'Claude Code is not installed or not in PATH. Please install Claude Code: npm install -g @anthropic-ai/claude-code'
|
|
664
|
-
},
|
|
665
|
-
EACCES: {
|
|
666
|
-
code: 'PERMISSION_DENIED',
|
|
667
|
-
message: 'Permission denied when running Claude Code. Please check file permissions.'
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
function handleSpawnError(
|
|
672
|
-
error: NodeJS.ErrnoException,
|
|
673
|
-
config: ResolvedHeadlessConfig,
|
|
674
|
-
reject: (reason: Error) => void
|
|
675
|
-
): void {
|
|
676
|
-
const mapped = error.code ? SPAWN_ERROR_MAP[error.code] : undefined;
|
|
677
|
-
if (!mapped) {
|
|
678
|
-
reject(error);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const formatted = `[[MSTRO_ERROR:${mapped.code}]] ${mapped.message}`;
|
|
683
|
-
if (config.outputCallback) {
|
|
684
|
-
config.outputCallback(`\n${formatted}\n`);
|
|
685
|
-
}
|
|
686
|
-
reject(new Error(formatted));
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// ========== Argument Building ==========
|
|
690
|
-
|
|
691
|
-
function buildClaudeArgs(
|
|
692
|
-
config: ResolvedHeadlessConfig,
|
|
693
|
-
prompt: string,
|
|
694
|
-
hasImageAttachments: boolean,
|
|
695
|
-
useStreamJson: boolean,
|
|
696
|
-
mcpConfigPath: string | null
|
|
697
|
-
): string[] {
|
|
698
|
-
const args = ['--print'];
|
|
699
|
-
|
|
700
|
-
if (config.model && config.model !== 'default') {
|
|
701
|
-
args.push('--model', config.model);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (useStreamJson) {
|
|
705
|
-
args.push('--output-format', 'stream-json', '--include-partial-messages', '--verbose');
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
if (hasImageAttachments) {
|
|
709
|
-
args.push('--input-format', 'stream-json');
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (config.claudeSessionId) {
|
|
713
|
-
args.push('--resume', config.claudeSessionId);
|
|
714
|
-
} else if (config.continueSession) {
|
|
715
|
-
args.push('--continue');
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (config.disallowedTools && config.disallowedTools.length > 0) {
|
|
719
|
-
args.push('--disallowedTools', config.disallowedTools.join(','));
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (mcpConfigPath) {
|
|
723
|
-
args.push('--mcp-config', mcpConfigPath);
|
|
724
|
-
args.push('--permission-prompt-tool', 'mcp__mstro-bouncer__approval_prompt');
|
|
725
|
-
} else {
|
|
726
|
-
// Bouncer unavailable: use acceptEdits so file operations work without stdin prompts.
|
|
727
|
-
// Bash still requires approval — Claude Code will skip tools it can't get permission for,
|
|
728
|
-
// which is better than hanging on a stdin prompt that can never be answered.
|
|
729
|
-
args.push('--permission-mode', 'acceptEdits');
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (!hasImageAttachments) {
|
|
733
|
-
args.push(prompt);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
return args;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/** Write image attachments to the Claude process stdin as stream-json */
|
|
740
|
-
function writeImageAttachmentsToStdin(
|
|
741
|
-
claudeProcess: ChildProcess,
|
|
742
|
-
prompt: string,
|
|
743
|
-
config: ResolvedHeadlessConfig,
|
|
744
|
-
): void {
|
|
745
|
-
claudeProcess.stdin!.on('error', (err) => {
|
|
746
|
-
if (config.verbose) {
|
|
747
|
-
herror('[STDIN] Write error:', err.message);
|
|
748
|
-
}
|
|
749
|
-
config.outputCallback?.(`\n[[MSTRO_ERROR:STDIN_WRITE_FAILED]] Failed to send image data to Claude: ${err.message}\n`);
|
|
750
|
-
});
|
|
751
|
-
const multimodalMessage = buildMultimodalMessage(prompt, config.imageAttachments!);
|
|
752
|
-
claudeProcess.stdin!.write(multimodalMessage);
|
|
753
|
-
claudeProcess.stdin!.end();
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/** Mutable state for stall detection, shared between the interval callback and the outer function */
|
|
757
|
-
interface StallState {
|
|
758
|
-
lastActivityTime: number;
|
|
759
|
-
stallWarningEmitted: boolean;
|
|
760
|
-
assessmentInProgress: boolean;
|
|
761
|
-
extensionsGranted: number;
|
|
762
|
-
currentKillDeadline: number;
|
|
763
|
-
nextWarningAfter: number;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/** Run a single stall-check tick. Extracted to reduce cognitive complexity of executeClaudeCommand. */
|
|
767
|
-
async function runStallCheckTick(
|
|
768
|
-
state: StallState,
|
|
769
|
-
opts: {
|
|
770
|
-
perfStart: number;
|
|
771
|
-
stallWarningMs: number;
|
|
772
|
-
stallHardCapMs: number;
|
|
773
|
-
maxExtensions: number;
|
|
774
|
-
stallAssessEnabled: boolean;
|
|
775
|
-
toolWatchdogActive: boolean;
|
|
776
|
-
prompt: string;
|
|
777
|
-
pendingTools: Map<string, string>;
|
|
778
|
-
lastToolInputSummary: string | undefined;
|
|
779
|
-
totalToolCalls: number;
|
|
780
|
-
claudeProcess: ChildProcess;
|
|
781
|
-
stallCheckInterval: ReturnType<typeof setInterval>;
|
|
782
|
-
config: ResolvedHeadlessConfig;
|
|
783
|
-
lastTokenActivityTime: number;
|
|
784
|
-
},
|
|
785
|
-
): Promise<void> {
|
|
786
|
-
const now = Date.now();
|
|
787
|
-
const silenceMs = now - state.lastActivityTime;
|
|
788
|
-
const totalElapsed = now - opts.perfStart;
|
|
789
|
-
const tokenSilenceMs = now - opts.lastTokenActivityTime;
|
|
790
|
-
|
|
791
|
-
if (totalElapsed >= opts.stallHardCapMs) {
|
|
792
|
-
terminateStallProcess(opts.claudeProcess, opts.stallCheckInterval, opts.config,
|
|
793
|
-
`\n[[MSTRO_ERROR:EXECUTION_STALLED]] Hard time limit reached (${Math.round(opts.stallHardCapMs / 60000)} min total). Terminating process.\n`
|
|
794
|
-
);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Token activity pushes the kill deadline forward — tokens flowing means
|
|
799
|
-
// the process is alive even if stdout is silent (e.g. silent thinking).
|
|
800
|
-
if (tokenSilenceMs < 60_000 && now < state.currentKillDeadline) {
|
|
801
|
-
const killMs = opts.config.stallKillMs ?? 1_800_000;
|
|
802
|
-
state.currentKillDeadline = Math.max(state.currentKillDeadline, now + killMs);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
if (now >= state.currentKillDeadline) {
|
|
806
|
-
terminateStallProcess(opts.claudeProcess, opts.stallCheckInterval, opts.config,
|
|
807
|
-
`\n[[MSTRO_ERROR:EXECUTION_STALLED]] No output for ${Math.round(silenceMs / 60_000)} minutes. Terminating process.\n`
|
|
808
|
-
);
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (silenceMs < opts.stallWarningMs || state.stallWarningEmitted || now < state.nextWarningAfter || state.assessmentInProgress) return;
|
|
813
|
-
|
|
814
|
-
const stallCtx: StallContext = {
|
|
815
|
-
originalPrompt: opts.prompt,
|
|
816
|
-
silenceMs,
|
|
817
|
-
lastToolName: opts.pendingTools.size > 0 ? Array.from(opts.pendingTools.values()).pop() : undefined,
|
|
818
|
-
lastToolInputSummary: opts.lastToolInputSummary,
|
|
819
|
-
pendingToolCount: opts.pendingTools.size,
|
|
820
|
-
pendingToolNames: new Set(opts.pendingTools.values()),
|
|
821
|
-
totalToolCalls: opts.totalToolCalls,
|
|
822
|
-
elapsedTotalMs: totalElapsed,
|
|
823
|
-
tokenSilenceMs,
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
if (opts.stallAssessEnabled && state.extensionsGranted < opts.maxExtensions) {
|
|
827
|
-
state.assessmentInProgress = true;
|
|
828
|
-
const result = await runStallAssessment({ stallCtx, config: opts.config, now, extensionsGranted: state.extensionsGranted, maxExtensions: opts.maxExtensions, toolWatchdogActive: opts.toolWatchdogActive });
|
|
829
|
-
state.assessmentInProgress = false;
|
|
830
|
-
|
|
831
|
-
if (result) {
|
|
832
|
-
state.extensionsGranted = result.extensionsGranted;
|
|
833
|
-
state.currentKillDeadline = result.currentKillDeadline;
|
|
834
|
-
state.nextWarningAfter = now + opts.stallWarningMs;
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
state.stallWarningEmitted = true;
|
|
840
|
-
const killIn = Math.round((state.currentKillDeadline - now) / 60_000);
|
|
841
|
-
opts.config.outputCallback?.(
|
|
842
|
-
`\n[[MSTRO_ERROR:EXECUTION_STALLED]] No output for ${Math.round(silenceMs / 60_000)} minutes. Will terminate in ${killIn} minutes if no activity.\n`
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// ========== Tool Tracking Setup ==========
|
|
847
|
-
|
|
848
|
-
/** Shared mutable state for tool event handlers */
|
|
849
|
-
interface ToolTrackingState {
|
|
850
|
-
pendingTools: Map<string, string>;
|
|
851
|
-
counters: { lastToolInputSummary: string | undefined; totalToolCalls: number };
|
|
852
|
-
toolIdToName: Map<string, string>;
|
|
853
|
-
toolIdToInput: Map<string, Record<string, unknown>>;
|
|
854
|
-
watchdog: ToolWatchdog | null;
|
|
855
|
-
stallState: StallState;
|
|
856
|
-
ctx: StreamHandlerContext;
|
|
857
|
-
onTimeout: (hungToolId: string) => void;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
interface ToolTrackingResult {
|
|
861
|
-
pendingTools: Map<string, string>;
|
|
862
|
-
watchdog: ToolWatchdog | null;
|
|
863
|
-
toolWatchdogActive: boolean;
|
|
864
|
-
counters: { lastToolInputSummary: string | undefined; totalToolCalls: number };
|
|
865
|
-
/** Must be called after stallCheckInterval is created, to wire up the kill handler */
|
|
866
|
-
setKillContext: (claudeProcess: ChildProcess, stallCheckInterval: ReturnType<typeof setInterval>) => void;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
/** Handle tool_start events. Extracted to reduce cognitive complexity. */
|
|
870
|
-
function onToolStart(event: ToolUseEvent, s: ToolTrackingState): void {
|
|
871
|
-
const id = event.toolId!;
|
|
872
|
-
s.pendingTools.set(id, event.toolName!);
|
|
873
|
-
s.counters.totalToolCalls++;
|
|
874
|
-
s.toolIdToName.set(id, event.toolName!);
|
|
875
|
-
if (s.watchdog) {
|
|
876
|
-
s.watchdog.startWatch(id, event.toolName!, {}, () => { s.onTimeout(id); });
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/** Handle tool_complete events. Extracted to reduce cognitive complexity. */
|
|
881
|
-
function onToolComplete(event: ToolUseEvent, s: ToolTrackingState): void {
|
|
882
|
-
const id = event.toolId!;
|
|
883
|
-
const input = event.completeInput ?? {};
|
|
884
|
-
s.counters.lastToolInputSummary = summarizeToolInput(input);
|
|
885
|
-
s.toolIdToInput.set(id, input);
|
|
886
|
-
if (!s.watchdog) return;
|
|
887
|
-
const toolName = s.toolIdToName.get(id);
|
|
888
|
-
if (toolName) {
|
|
889
|
-
s.watchdog.startWatch(id, toolName, input, () => { s.onTimeout(id); });
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
/** Handle tool_result events. Extracted to reduce cognitive complexity. */
|
|
894
|
-
function onToolResult(event: ToolUseEvent, s: ToolTrackingState): void {
|
|
895
|
-
const id = event.toolId!;
|
|
896
|
-
s.pendingTools.delete(id);
|
|
897
|
-
s.stallState.stallWarningEmitted = false;
|
|
898
|
-
s.stallState.lastActivityTime = Date.now();
|
|
899
|
-
const toolEntry = s.ctx.accumulatedToolUse.find(t => t.toolId === id);
|
|
900
|
-
if (!s.watchdog || !toolEntry) return;
|
|
901
|
-
const toolName = s.toolIdToName.get(id);
|
|
902
|
-
if (toolName && toolEntry.duration) {
|
|
903
|
-
s.watchdog.recordCompletion(toolName, toolEntry.duration);
|
|
904
|
-
}
|
|
905
|
-
s.watchdog.clearWatch(id);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
/** Resolve a display URL from tool input for timeout messages */
|
|
909
|
-
function resolveToolUrl(toolInput: Record<string, unknown>): string | undefined {
|
|
910
|
-
if (toolInput.url) return String(toolInput.url);
|
|
911
|
-
if (toolInput.query) return String(toolInput.query);
|
|
912
|
-
return undefined;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/** Handle a tool timeout by building a checkpoint and killing the process. */
|
|
916
|
-
function executeToolTimeout(
|
|
917
|
-
hungToolId: string,
|
|
918
|
-
watchdog: ToolWatchdog,
|
|
919
|
-
killCtx: { claudeProcess: ChildProcess; stallCheckInterval: ReturnType<typeof setInterval> },
|
|
920
|
-
s: ToolTrackingState,
|
|
921
|
-
config: ResolvedHeadlessConfig,
|
|
922
|
-
prompt: string,
|
|
923
|
-
sessionCapture: { claudeSessionId?: string },
|
|
924
|
-
perfStart: number,
|
|
925
|
-
): void {
|
|
926
|
-
const checkpoint = watchdog.buildCheckpoint(
|
|
927
|
-
prompt, s.ctx.accumulatedAssistantResponse, s.ctx.accumulatedThinking,
|
|
928
|
-
s.ctx.accumulatedToolUse, hungToolId, sessionCapture.claudeSessionId, perfStart,
|
|
929
|
-
);
|
|
930
|
-
|
|
931
|
-
const toolName = s.toolIdToName.get(hungToolId) || 'unknown';
|
|
932
|
-
const toolInput = s.toolIdToInput.get(hungToolId) || {};
|
|
933
|
-
const timeoutMs = watchdog.getTimeout(toolName);
|
|
934
|
-
const url = resolveToolUrl(toolInput);
|
|
935
|
-
|
|
936
|
-
config.outputCallback?.(
|
|
937
|
-
`\n[[MSTRO_TOOL_TIMEOUT]] ${toolName} timed out after ${Math.round(timeoutMs / 1000)}s${url ? ` fetching: ${url.slice(0, 100)}` : ''}. ${s.ctx.accumulatedToolUse.filter(t => t.result !== undefined).length} completed results preserved.\n`
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
if (checkpoint) {
|
|
941
|
-
config.onToolTimeout?.(checkpoint);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
verboseLog(config.verbose, `[WATCHDOG] Killing process due to ${toolName} timeout`);
|
|
945
|
-
watchdog.clearAll();
|
|
946
|
-
clearInterval(killCtx.stallCheckInterval);
|
|
947
|
-
if (killCtx.claudeProcess.pid) killProcessGroup(killCtx.claudeProcess.pid, 'SIGTERM');
|
|
948
|
-
const proc = killCtx.claudeProcess;
|
|
949
|
-
setTimeout(() => { if (!proc.killed && proc.pid) killProcessGroup(proc.pid, 'SIGKILL'); }, 5000);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
/** Set up tool activity tracking and watchdog. Extracted to reduce cognitive complexity. */
|
|
953
|
-
function setupToolTracking(
|
|
954
|
-
config: ResolvedHeadlessConfig,
|
|
955
|
-
stallState: StallState,
|
|
956
|
-
ctx: StreamHandlerContext,
|
|
957
|
-
sessionCapture: { claudeSessionId?: string },
|
|
958
|
-
prompt: string,
|
|
959
|
-
perfStart: number,
|
|
960
|
-
): ToolTrackingResult {
|
|
961
|
-
const pendingTools = new Map<string, string>();
|
|
962
|
-
const counters = { lastToolInputSummary: undefined as string | undefined, totalToolCalls: 0 };
|
|
963
|
-
|
|
964
|
-
const toolWatchdogActive = config.enableToolWatchdog !== false;
|
|
965
|
-
const watchdog = toolWatchdogActive
|
|
966
|
-
? new ToolWatchdog({
|
|
967
|
-
profiles: config.toolTimeoutProfiles,
|
|
968
|
-
verbose: config.verbose,
|
|
969
|
-
onTiebreaker: async (toolName, toolInput, elapsedMs, tokenSilenceMs) => {
|
|
970
|
-
return assessToolTimeout(toolName, toolInput, elapsedMs, config.claudeCommand, config.verbose, tokenSilenceMs);
|
|
971
|
-
},
|
|
972
|
-
getTokenSilenceMs: () => {
|
|
973
|
-
const last = ctx.lastTokenActivityTime;
|
|
974
|
-
return last > 0 ? Date.now() - last : undefined;
|
|
975
|
-
},
|
|
976
|
-
})
|
|
977
|
-
: null;
|
|
978
|
-
|
|
979
|
-
// Deferred kill context — set after stallCheckInterval is created
|
|
980
|
-
let killCtx: { claudeProcess: ChildProcess; stallCheckInterval: ReturnType<typeof setInterval> } | null = null;
|
|
981
|
-
|
|
982
|
-
const trackingState: ToolTrackingState = {
|
|
983
|
-
pendingTools, counters,
|
|
984
|
-
toolIdToName: new Map(), toolIdToInput: new Map(),
|
|
985
|
-
watchdog, stallState, ctx,
|
|
986
|
-
onTimeout: (hungToolId) => {
|
|
987
|
-
if (!watchdog || !killCtx) return;
|
|
988
|
-
executeToolTimeout(hungToolId, watchdog, killCtx, trackingState, config, prompt, sessionCapture, perfStart);
|
|
989
|
-
},
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
const origToolUseCallback = config.toolUseCallback;
|
|
993
|
-
|
|
994
|
-
config.toolUseCallback = (event) => {
|
|
995
|
-
if (event.type === 'tool_start' && event.toolName && event.toolId) {
|
|
996
|
-
onToolStart(event, trackingState);
|
|
997
|
-
} else if (event.type === 'tool_complete' && event.completeInput && event.toolId) {
|
|
998
|
-
onToolComplete(event, trackingState);
|
|
999
|
-
} else if (event.type === 'tool_result' && event.toolId) {
|
|
1000
|
-
onToolResult(event, trackingState);
|
|
1001
|
-
}
|
|
1002
|
-
origToolUseCallback?.(event);
|
|
1003
|
-
};
|
|
1004
|
-
|
|
1005
|
-
return {
|
|
1006
|
-
pendingTools, watchdog, toolWatchdogActive, counters,
|
|
1007
|
-
setKillContext: (claudeProcess, stallCheckInterval) => {
|
|
1008
|
-
killCtx = { claudeProcess, stallCheckInterval };
|
|
1009
|
-
},
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
/** Log messages when verbose mode is enabled. Extracted to reduce cognitive complexity. */
|
|
1014
|
-
function verboseLog(verbose: boolean | undefined, ...msgs: string[]): void {
|
|
1015
|
-
if (verbose) {
|
|
1016
|
-
for (const msg of msgs) hlog(msg);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
/** Spawn the Claude CLI process and register it. Extracted to reduce cognitive complexity. */
|
|
1021
|
-
function spawnAndRegister(
|
|
1022
|
-
config: ResolvedHeadlessConfig,
|
|
1023
|
-
prompt: string,
|
|
1024
|
-
hasImageAttachments: boolean,
|
|
1025
|
-
useStreamJson: boolean,
|
|
1026
|
-
runningProcesses: Map<number, ChildProcess>,
|
|
1027
|
-
perfStart: number,
|
|
1028
|
-
): ChildProcess {
|
|
1029
|
-
const mcpConfigPath = generateMcpConfig(config.workingDir, config.verbose);
|
|
1030
|
-
|
|
1031
|
-
if (!mcpConfigPath && config.outputCallback) {
|
|
1032
|
-
config.outputCallback(
|
|
1033
|
-
'\n[[MSTRO_ERROR:BOUNCER_UNAVAILABLE]] Security bouncer not available. Running with limited permissions — file edits allowed, but shell commands may be restricted.\n'
|
|
1034
|
-
);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const args = buildClaudeArgs(config, prompt, hasImageAttachments, useStreamJson, mcpConfigPath);
|
|
1038
|
-
|
|
1039
|
-
verboseLog(config.verbose,
|
|
1040
|
-
`[PERF] About to spawn: ${Date.now() - perfStart}ms`,
|
|
1041
|
-
`[PERF] Command: ${config.claudeCommand} ${args.join(' ')}`,
|
|
1042
|
-
);
|
|
1043
|
-
|
|
1044
|
-
const baseEnv = config.sandboxed
|
|
1045
|
-
? sanitizeEnvForSandbox(process.env, config.workingDir)
|
|
1046
|
-
: { ...process.env };
|
|
1047
|
-
const spawnEnv = config.extraEnv
|
|
1048
|
-
? { ...baseEnv, ...config.extraEnv }
|
|
1049
|
-
: baseEnv;
|
|
1050
|
-
|
|
1051
|
-
const claudeProcess = spawn(config.claudeCommand, args, {
|
|
1052
|
-
cwd: config.workingDir,
|
|
1053
|
-
detached: true,
|
|
1054
|
-
env: spawnEnv,
|
|
1055
|
-
stdio: [hasImageAttachments ? 'pipe' : 'ignore', 'pipe', 'pipe']
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
if (hasImageAttachments && claudeProcess.stdin) {
|
|
1059
|
-
writeImageAttachmentsToStdin(claudeProcess, prompt, config);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (claudeProcess.pid) {
|
|
1063
|
-
runningProcesses.set(claudeProcess.pid, claudeProcess);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
verboseLog(config.verbose, `[PERF] Spawned: ${Date.now() - perfStart}ms`);
|
|
1067
|
-
|
|
1068
|
-
return claudeProcess;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
26
|
/**
|
|
1072
|
-
* Execute a Claude CLI command for a single movement
|
|
1073
|
-
* Supports multimodal prompts via --input-format stream-json when image attachments are present
|
|
27
|
+
* Execute a Claude CLI command for a single movement.
|
|
28
|
+
* Supports multimodal prompts via --input-format stream-json when image attachments are present.
|
|
1074
29
|
*/
|
|
1075
30
|
export async function executeClaudeCommand(
|
|
1076
31
|
prompt: string,
|
|
@@ -1080,7 +35,7 @@ export async function executeClaudeCommand(
|
|
|
1080
35
|
): Promise<ExecutionResult> {
|
|
1081
36
|
const { config, runningProcesses } = options;
|
|
1082
37
|
const perfStart = Date.now();
|
|
1083
|
-
verboseLog(config.verbose,
|
|
38
|
+
verboseLog(config.verbose, '[PERF] executeMovement started');
|
|
1084
39
|
|
|
1085
40
|
const hasImageAttachments = config.imageAttachments && config.imageAttachments.length > 0;
|
|
1086
41
|
const useStreamJson = hasImageAttachments || config.thinkingCallback || config.outputCallback || config.toolUseCallback;
|
|
@@ -1094,8 +49,6 @@ export async function executeClaudeCommand(
|
|
|
1094
49
|
let errorAlreadySurfaced = false;
|
|
1095
50
|
|
|
1096
51
|
const sessionCapture: { claudeSessionId?: string } = {};
|
|
1097
|
-
// Activate resume assessment buffering when resuming a session.
|
|
1098
|
-
// Text is held until thinking/tool activity confirms Claude has context.
|
|
1099
52
|
const isResumeMode = !!(config.continueSession && config.claudeSessionId);
|
|
1100
53
|
|
|
1101
54
|
const ctx: StreamHandlerContext = {
|
|
@@ -1112,7 +65,6 @@ export async function executeClaudeCommand(
|
|
|
1112
65
|
lastTokenActivityTime: Date.now(),
|
|
1113
66
|
};
|
|
1114
67
|
|
|
1115
|
-
// Stall detection state (mutable object shared with runStallCheckTick)
|
|
1116
68
|
const stallState: StallState = {
|
|
1117
69
|
lastActivityTime: Date.now(),
|
|
1118
70
|
stallWarningEmitted: false,
|
|
@@ -1122,17 +74,14 @@ export async function executeClaudeCommand(
|
|
|
1122
74
|
nextWarningAfter: 0,
|
|
1123
75
|
};
|
|
1124
76
|
|
|
1125
|
-
// Tool activity tracking for stall assessment context
|
|
1126
77
|
const toolTracking = setupToolTracking(config, stallState, ctx, sessionCapture, prompt, perfStart);
|
|
1127
78
|
const { pendingTools, watchdog, toolWatchdogActive } = toolTracking;
|
|
1128
|
-
// Mutable counters accessed by stall check tick
|
|
1129
79
|
const toolCounters = toolTracking.counters;
|
|
1130
80
|
|
|
1131
81
|
claudeProcess.stdout!.on('data', (data) => {
|
|
1132
82
|
stallState.lastActivityTime = Date.now();
|
|
1133
83
|
stallState.stallWarningEmitted = false;
|
|
1134
|
-
stallState.nextWarningAfter = 0;
|
|
1135
|
-
// Push kill deadline forward on any activity
|
|
84
|
+
stallState.nextWarningAfter = 0;
|
|
1136
85
|
const killMs = config.stallKillMs ?? 1_800_000;
|
|
1137
86
|
stallState.currentKillDeadline = Date.now() + killMs;
|
|
1138
87
|
|
|
@@ -1180,7 +129,6 @@ export async function executeClaudeCommand(
|
|
|
1180
129
|
});
|
|
1181
130
|
}, 10_000);
|
|
1182
131
|
|
|
1183
|
-
// Wire up the kill context now that stallCheckInterval exists
|
|
1184
132
|
toolTracking.setKillContext(claudeProcess, stallCheckInterval);
|
|
1185
133
|
|
|
1186
134
|
return new Promise((resolve, reject) => {
|
|
@@ -1200,32 +148,3 @@ export async function executeClaudeCommand(
|
|
|
1200
148
|
});
|
|
1201
149
|
});
|
|
1202
150
|
}
|
|
1203
|
-
|
|
1204
|
-
function buildCloseResult(
|
|
1205
|
-
ctx: StreamHandlerContext,
|
|
1206
|
-
stdout: string,
|
|
1207
|
-
stderr: string,
|
|
1208
|
-
code: number | null,
|
|
1209
|
-
signal: NodeJS.Signals | null,
|
|
1210
|
-
sessionCapture: { claudeSessionId?: string },
|
|
1211
|
-
): ExecutionResult {
|
|
1212
|
-
const postTimeout = flushNativeTimeoutBuffers(ctx);
|
|
1213
|
-
const resumeBuffered = ctx.resumeAssessmentActive ? (ctx.resumeAssessmentBuffer || undefined) : undefined;
|
|
1214
|
-
const exitCode = code ?? (signal ? 128 + (signalToNumber(signal) ?? 0) : 0);
|
|
1215
|
-
const hasTokenUsage = ctx.apiTokenUsage.inputTokens > 0 || ctx.apiTokenUsage.outputTokens > 0;
|
|
1216
|
-
return {
|
|
1217
|
-
output: stdout,
|
|
1218
|
-
error: stderr || undefined,
|
|
1219
|
-
exitCode,
|
|
1220
|
-
signalName: signal || undefined,
|
|
1221
|
-
assistantResponse: ctx.accumulatedAssistantResponse || undefined,
|
|
1222
|
-
thinkingOutput: ctx.accumulatedThinking || undefined,
|
|
1223
|
-
toolUseHistory: ctx.accumulatedToolUse.length > 0 ? ctx.accumulatedToolUse : undefined,
|
|
1224
|
-
claudeSessionId: sessionCapture.claudeSessionId,
|
|
1225
|
-
nativeTimeoutCount: ctx.nativeTimeoutDetector.timeoutCount || undefined,
|
|
1226
|
-
postTimeoutOutput: postTimeout,
|
|
1227
|
-
resumeBufferedOutput: resumeBuffered,
|
|
1228
|
-
apiTokenUsage: hasTokenUsage ? { ...ctx.apiTokenUsage } : undefined,
|
|
1229
|
-
stopReason: ctx.stopReason,
|
|
1230
|
-
};
|
|
1231
|
-
}
|