mstro-app 0.4.39 → 0.4.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +1 -3
- package/bin/commands/login.js +17 -7
- package/bin/commands/logout.js +14 -6
- package/bin/commands/status.js +9 -3
- package/bin/commands/whoami.js +10 -4
- package/bin/mstro.js +11 -1
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts +1 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +2 -0
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/resilient-runner.d.ts +47 -0
- package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -0
- package/dist/server/cli/headless/resilient-runner.js +234 -0
- package/dist/server/cli/headless/resilient-runner.js.map +1 -0
- package/dist/server/cli/headless/retry-strategies.d.ts +44 -0
- package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -0
- package/dist/server/cli/headless/retry-strategies.js +262 -0
- package/dist/server/cli/headless/retry-strategies.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +5 -0
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +2 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +31 -4
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +1 -30
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +16 -3
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/prompt-builders.d.ts.map +1 -1
- package/dist/server/cli/prompt-builders.js +31 -13
- package/dist/server/cli/prompt-builders.js.map +1 -1
- package/dist/server/index.js +1 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +5 -4
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +1 -1
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +14 -8
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-patterns.js +1 -1
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +19 -9
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +6 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +163 -77
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +1 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +6 -0
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-classification.d.ts +11 -0
- package/dist/server/services/plan/issue-classification.d.ts.map +1 -0
- package/dist/server/services/plan/issue-classification.js +20 -0
- package/dist/server/services/plan/issue-classification.js.map +1 -0
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +7 -4
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +0 -5
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +12 -241
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +9 -6
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.js +11 -4
- package/dist/server/services/platform-credentials.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +7 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.js +4 -0
- package/dist/server/services/websocket/file-search-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +2 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- 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 +18 -7
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +6 -6
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.js +90 -42
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +48 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +22 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +48 -1
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +74 -32
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +18 -18
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/skill-handlers.d.ts +3 -1
- package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-handlers.js +52 -41
- package/dist/server/services/websocket/skill-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-watcher.d.ts +17 -0
- package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -0
- package/dist/server/services/websocket/skill-watcher.js +85 -0
- package/dist/server/services/websocket/skill-watcher.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +2 -268
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +0 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-stream.ts +1 -0
- package/server/cli/headless/index.ts +2 -0
- package/server/cli/headless/resilient-runner.ts +354 -0
- package/server/cli/headless/retry-strategies.ts +330 -0
- package/server/cli/headless/stall-assessor.ts +5 -0
- package/server/cli/headless/tool-watchdog.ts +40 -4
- package/server/cli/improvisation-retry.ts +1 -32
- package/server/cli/improvisation-session-manager.ts +17 -3
- package/server/cli/prompt-builders.ts +33 -12
- package/server/index.ts +1 -9
- package/server/mcp/bouncer-cli.ts +5 -4
- package/server/mcp/bouncer-haiku.ts +1 -1
- package/server/mcp/bouncer-integration.ts +15 -8
- package/server/mcp/security-patterns.ts +1 -1
- package/server/services/plan/agents/code-review.md +109 -0
- package/server/services/plan/agents/commit-message.md +26 -0
- package/server/services/plan/agents/fix-quality.md +24 -0
- package/server/services/plan/agents/pr-description.md +28 -0
- package/server/services/plan/composer.ts +20 -9
- package/server/services/plan/executor.ts +165 -77
- package/server/services/plan/front-matter.ts +7 -0
- package/server/services/plan/issue-classification.ts +21 -0
- package/server/services/plan/issue-prompt-builder.ts +8 -4
- package/server/services/plan/issue-retry.ts +15 -330
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/review-gate.ts +9 -6
- package/server/services/plan/types.ts +3 -0
- package/server/services/platform-credentials.ts +10 -4
- package/server/services/terminal/pty-manager.ts +7 -1
- package/server/services/websocket/file-search-handlers.ts +2 -0
- package/server/services/websocket/handler-context.ts +2 -0
- package/server/services/websocket/handler.ts +18 -8
- package/server/services/websocket/plan-execution-handlers.ts +7 -7
- package/server/services/websocket/quality-fix-agent.ts +86 -44
- package/server/services/websocket/quality-handlers.ts +48 -7
- package/server/services/websocket/quality-persistence.ts +75 -1
- package/server/services/websocket/quality-review-agent.ts +70 -31
- package/server/services/websocket/quality-tools.ts +16 -14
- package/server/services/websocket/skill-handlers.ts +50 -40
- package/server/services/websocket/skill-watcher.ts +79 -0
- package/server/services/websocket/types.ts +0 -311
- package/dist/server/services/deploy/ai-broker.d.ts +0 -63
- package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
- package/dist/server/services/deploy/ai-broker.js +0 -360
- package/dist/server/services/deploy/ai-broker.js.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
- package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.js +0 -621
- package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
- package/dist/server/services/deploy/credentials.d.ts +0 -35
- package/dist/server/services/deploy/credentials.d.ts.map +0 -1
- package/dist/server/services/deploy/credentials.js +0 -177
- package/dist/server/services/deploy/credentials.js.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
- package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.js +0 -294
- package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
- package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.js +0 -266
- package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
- package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.js +0 -409
- package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
- package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
- package/server/cli/headless/RESEARCH.md +0 -627
- package/server/services/deploy/ai-broker.ts +0 -512
- package/server/services/deploy/board-execution-handler.ts +0 -847
- package/server/services/deploy/credentials.ts +0 -200
- package/server/services/deploy/deploy-ai-service.ts +0 -401
- package/server/services/deploy/headless-session-handler.ts +0 -414
- package/server/services/websocket/deploy-handlers.ts +0 -544
- package/server/services/websocket/handlers/deploy-handlers.ts +0 -228
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared retry strategy functions for headless execution.
|
|
6
|
+
*
|
|
7
|
+
* Pure decision functions that evaluate a SessionResult and RetryState,
|
|
8
|
+
* returning a RetryDecision when a retry should happen or null to skip.
|
|
9
|
+
* Used by both PM board execution (issue-retry) and Chat view (improvisation-retry).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
buildFreshRecoveryPrompt,
|
|
14
|
+
buildResumeRetryPrompt,
|
|
15
|
+
buildRetryPrompt,
|
|
16
|
+
buildSignalCrashRecoveryPrompt,
|
|
17
|
+
} from '../prompt-builders.js';
|
|
18
|
+
import { hlog } from './headless-logger.js';
|
|
19
|
+
import { assessContextLoss, assessPrematureCompletion } from './stall-assessor.js';
|
|
20
|
+
import type { ExecutionCheckpoint, SessionResult } from './types.js';
|
|
21
|
+
|
|
22
|
+
export interface ToolRecord {
|
|
23
|
+
toolName: string;
|
|
24
|
+
toolId: string;
|
|
25
|
+
toolInput: Record<string, unknown>;
|
|
26
|
+
result?: string;
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
duration?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RetryState {
|
|
32
|
+
retryNumber: number;
|
|
33
|
+
maxRetries: number;
|
|
34
|
+
originalPrompt: string;
|
|
35
|
+
accumulatedToolResults: ToolRecord[];
|
|
36
|
+
timedOutTools: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>;
|
|
37
|
+
checkpoint: ExecutionCheckpoint | null;
|
|
38
|
+
bestResult: SessionResult | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RetryDecision {
|
|
42
|
+
nextPrompt: string;
|
|
43
|
+
useResume: boolean;
|
|
44
|
+
resumeSessionId?: string;
|
|
45
|
+
path: string;
|
|
46
|
+
reason: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RetryConfig {
|
|
50
|
+
enableContextLossDetection: boolean;
|
|
51
|
+
enableBestResultSelection: boolean;
|
|
52
|
+
verbose: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MAX_ACCUMULATED_RESULTS = 50;
|
|
56
|
+
|
|
57
|
+
export function createRetryState(originalPrompt: string, maxRetries: number): RetryState {
|
|
58
|
+
return {
|
|
59
|
+
retryNumber: 0,
|
|
60
|
+
maxRetries,
|
|
61
|
+
originalPrompt,
|
|
62
|
+
accumulatedToolResults: [],
|
|
63
|
+
timedOutTools: [],
|
|
64
|
+
checkpoint: null,
|
|
65
|
+
bestResult: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function accumulateToolResults(result: SessionResult, state: RetryState): void {
|
|
70
|
+
if (!result.toolUseHistory) return;
|
|
71
|
+
for (const t of result.toolUseHistory) {
|
|
72
|
+
if (t.result !== undefined) {
|
|
73
|
+
state.accumulatedToolResults.push({
|
|
74
|
+
toolName: t.toolName,
|
|
75
|
+
toolId: t.toolId,
|
|
76
|
+
toolInput: t.toolInput,
|
|
77
|
+
result: t.result,
|
|
78
|
+
isError: t.isError,
|
|
79
|
+
duration: t.duration,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (state.accumulatedToolResults.length > MAX_ACCUMULATED_RESULTS) {
|
|
84
|
+
state.accumulatedToolResults = state.accumulatedToolResults.slice(-MAX_ACCUMULATED_RESULTS);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function scoreResult(r: SessionResult): number {
|
|
89
|
+
const toolCount = r.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0;
|
|
90
|
+
const responseLen = Math.min((r.assistantResponse?.length ?? 0) / 50, 100);
|
|
91
|
+
const hasThinking = r.thinkingOutput ? 20 : 0;
|
|
92
|
+
return toolCount * 10 + responseLen + hasThinking;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isResponseAbandoned(result: SessionResult): boolean {
|
|
96
|
+
const thinkingLen = result.thinkingOutput?.length ?? 0;
|
|
97
|
+
const responseLen = result.assistantResponse?.length ?? 0;
|
|
98
|
+
const toolCallsInResponse = result.toolUseHistory?.filter(t => t.result !== undefined).length ?? 0;
|
|
99
|
+
|
|
100
|
+
if (thinkingLen < 500 || responseLen > 1000) return false;
|
|
101
|
+
if (toolCallsInResponse > 0 && responseLen > 200) return false;
|
|
102
|
+
|
|
103
|
+
return thinkingLen >= responseLen * 3;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function extractFinalTextBlock(response: string, maxLen: number): string {
|
|
107
|
+
const lastBreak = response.lastIndexOf('\n\n');
|
|
108
|
+
if (lastBreak !== -1 && response.length - lastBreak > 20) {
|
|
109
|
+
return response.slice(lastBreak + 2).slice(-maxLen);
|
|
110
|
+
}
|
|
111
|
+
return response.slice(-maxLen);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function tryToolTimeout(_result: SessionResult, state: RetryState): RetryDecision | null {
|
|
115
|
+
if (!state.checkpoint || state.retryNumber >= state.maxRetries) return null;
|
|
116
|
+
|
|
117
|
+
const cp = state.checkpoint;
|
|
118
|
+
state.retryNumber++;
|
|
119
|
+
|
|
120
|
+
state.timedOutTools.push({
|
|
121
|
+
toolName: cp.hungTool.toolName,
|
|
122
|
+
input: cp.hungTool.input ?? {},
|
|
123
|
+
timeoutMs: cp.hungTool.timeoutMs,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const canResume = cp.inProgressTools.length === 0 && !!cp.claudeSessionId;
|
|
127
|
+
const reason = `${cp.hungTool.toolName} timed out after ${Math.round(cp.hungTool.timeoutMs / 1000)}s, ${cp.completedTools.length} tools completed, ${canResume ? 'resuming' : 'fresh start'}`;
|
|
128
|
+
|
|
129
|
+
hlog(`[RETRY] Tool timeout: ${reason} (retry ${state.retryNumber}/${state.maxRetries})`);
|
|
130
|
+
|
|
131
|
+
if (canResume) {
|
|
132
|
+
return {
|
|
133
|
+
nextPrompt: buildResumeRetryPrompt(cp, state.timedOutTools),
|
|
134
|
+
useResume: true,
|
|
135
|
+
resumeSessionId: cp.claudeSessionId,
|
|
136
|
+
path: 'ToolTimeout',
|
|
137
|
+
reason,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
nextPrompt: buildRetryPrompt(cp, state.originalPrompt, state.timedOutTools),
|
|
143
|
+
useResume: false,
|
|
144
|
+
path: 'ToolTimeout',
|
|
145
|
+
reason,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function trySignalCrash(result: SessionResult, state: RetryState): RetryDecision | null {
|
|
150
|
+
const isSignalCrash = !!result.signalName;
|
|
151
|
+
const exitCodeSignal = !result.completed && !result.signalName && result.error?.match(/exited with code (1[2-9]\d|[2-9]\d{2})/);
|
|
152
|
+
if (!isSignalCrash && !exitCodeSignal) return null;
|
|
153
|
+
if (state.retryNumber >= state.maxRetries) return null;
|
|
154
|
+
if (state.checkpoint) return null;
|
|
155
|
+
|
|
156
|
+
accumulateToolResults(result, state);
|
|
157
|
+
state.retryNumber++;
|
|
158
|
+
|
|
159
|
+
const signalInfo = result.signalName || 'unknown signal';
|
|
160
|
+
const useResume = !!result.claudeSessionId && state.retryNumber === 1;
|
|
161
|
+
const reason = `Process killed (${signalInfo}), ${state.accumulatedToolResults.length} tools preserved, ${useResume ? 'resuming' : 'fresh start'}`;
|
|
162
|
+
|
|
163
|
+
hlog(`[RETRY] Signal crash: ${reason} (retry ${state.retryNumber}/${state.maxRetries})`);
|
|
164
|
+
|
|
165
|
+
if (useResume) {
|
|
166
|
+
return {
|
|
167
|
+
nextPrompt: buildSignalCrashRecoveryPrompt(state.originalPrompt, true),
|
|
168
|
+
useResume: true,
|
|
169
|
+
resumeSessionId: result.claudeSessionId,
|
|
170
|
+
path: 'SignalCrash',
|
|
171
|
+
reason,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
nextPrompt: buildSignalCrashRecoveryPrompt(state.originalPrompt, false, state.accumulatedToolResults),
|
|
177
|
+
useResume: false,
|
|
178
|
+
path: 'SignalCrash',
|
|
179
|
+
reason,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function detectContextLossHeuristic(result: SessionResult, verbose: boolean): boolean {
|
|
184
|
+
if (!result.assistantResponse || result.assistantResponse.trim().length === 0) {
|
|
185
|
+
if (verbose) hlog('[RETRY] Context loss heuristic: null/empty response');
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (result.resumeBufferedOutput !== undefined) {
|
|
189
|
+
if (verbose) hlog('[RETRY] Context loss heuristic: buffer never flushed (no thinking/tools)');
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
(!result.toolUseHistory || result.toolUseHistory.length === 0) &&
|
|
194
|
+
!result.thinkingOutput &&
|
|
195
|
+
result.assistantResponse.length < 500
|
|
196
|
+
) {
|
|
197
|
+
if (verbose) hlog('[RETRY] Context loss heuristic: no tools, no thinking, short response');
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function computeEffectiveTimeouts(result: SessionResult): number {
|
|
204
|
+
const nativeTimeouts = result.nativeTimeoutCount ?? 0;
|
|
205
|
+
if (nativeTimeouts === 0) return 0;
|
|
206
|
+
|
|
207
|
+
const succeededIds = new Set<string>();
|
|
208
|
+
const allIds = new Set<string>();
|
|
209
|
+
for (const t of result.toolUseHistory ?? []) {
|
|
210
|
+
allIds.add(t.toolId);
|
|
211
|
+
if (t.result !== undefined) succeededIds.add(t.toolId);
|
|
212
|
+
}
|
|
213
|
+
const toolsWithoutResult = Array.from(allIds).filter(id => !succeededIds.has(id)).length;
|
|
214
|
+
return Math.max(nativeTimeouts, toolsWithoutResult);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function detectContextLossViaHaiku(
|
|
218
|
+
result: SessionResult,
|
|
219
|
+
effectiveTimeouts: number,
|
|
220
|
+
verbose: boolean,
|
|
221
|
+
): Promise<boolean> {
|
|
222
|
+
if (effectiveTimeouts === 0 || !result.assistantResponse) return false;
|
|
223
|
+
|
|
224
|
+
const writeToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
225
|
+
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
226
|
+
try {
|
|
227
|
+
const verdict = await assessContextLoss({
|
|
228
|
+
assistantResponse: result.assistantResponse,
|
|
229
|
+
effectiveTimeouts,
|
|
230
|
+
nativeTimeoutCount: result.nativeTimeoutCount ?? 0,
|
|
231
|
+
successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
|
|
232
|
+
thinkingOutputLength: result.thinkingOutput?.length ?? 0,
|
|
233
|
+
hasSuccessfulWrite: result.toolUseHistory?.some(
|
|
234
|
+
t => writeToolNames.has(t.toolName) && t.result !== undefined && !t.isError
|
|
235
|
+
) ?? false,
|
|
236
|
+
}, claudeCmd, verbose);
|
|
237
|
+
if (verbose) hlog(`[RETRY] Haiku context loss verdict: ${verdict.contextLost ? 'LOST' : 'OK'} — ${verdict.reason}`);
|
|
238
|
+
return verdict.contextLost;
|
|
239
|
+
} catch {
|
|
240
|
+
if (verbose) hlog('[RETRY] Haiku context loss assessment failed, assuming OK');
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function tryContextLoss(
|
|
246
|
+
result: SessionResult,
|
|
247
|
+
state: RetryState,
|
|
248
|
+
config: RetryConfig,
|
|
249
|
+
): Promise<RetryDecision | null> {
|
|
250
|
+
if (!config.enableContextLossDetection) return null;
|
|
251
|
+
if (state.checkpoint || state.retryNumber >= state.maxRetries) return null;
|
|
252
|
+
|
|
253
|
+
const heuristicLost = detectContextLossHeuristic(result, config.verbose);
|
|
254
|
+
const haikuLost = heuristicLost
|
|
255
|
+
? false
|
|
256
|
+
: await detectContextLossViaHaiku(result, computeEffectiveTimeouts(result), config.verbose);
|
|
257
|
+
|
|
258
|
+
if (!heuristicLost && !haikuLost) return null;
|
|
259
|
+
|
|
260
|
+
accumulateToolResults(result, state);
|
|
261
|
+
state.retryNumber++;
|
|
262
|
+
|
|
263
|
+
const reason = `Context lost, ${state.accumulatedToolResults.length} tools preserved`;
|
|
264
|
+
hlog(`[RETRY] Context loss: ${reason} (retry ${state.retryNumber}/${state.maxRetries})`);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
nextPrompt: buildFreshRecoveryPrompt(state.originalPrompt, state.accumulatedToolResults, state.timedOutTools),
|
|
268
|
+
useResume: false,
|
|
269
|
+
path: 'ContextLoss',
|
|
270
|
+
reason,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function isEndTurnIncomplete(result: SessionResult, verbose: boolean): Promise<boolean> {
|
|
275
|
+
if (isResponseAbandoned(result)) {
|
|
276
|
+
if (verbose) hlog('[RETRY] Response abandoned heuristic triggered');
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
if (!result.assistantResponse) return false;
|
|
280
|
+
|
|
281
|
+
const claudeCmd = process.env.CLAUDE_COMMAND || 'claude';
|
|
282
|
+
try {
|
|
283
|
+
const verdict = await assessPrematureCompletion({
|
|
284
|
+
responseTail: extractFinalTextBlock(result.assistantResponse, 800),
|
|
285
|
+
successfulToolCalls: result.toolUseHistory?.filter(t => t.result !== undefined && !t.isError).length ?? 0,
|
|
286
|
+
hasThinking: !!result.thinkingOutput,
|
|
287
|
+
responseLength: result.assistantResponse.length,
|
|
288
|
+
}, claudeCmd, verbose);
|
|
289
|
+
if (verbose) {
|
|
290
|
+
hlog(`[RETRY] Premature completion verdict: ${verdict.isIncomplete ? 'INCOMPLETE' : 'COMPLETE'} — ${verdict.reason}`);
|
|
291
|
+
}
|
|
292
|
+
return verdict.isIncomplete;
|
|
293
|
+
} catch {
|
|
294
|
+
if (verbose) hlog('[RETRY] Premature completion assessment failed, assuming complete');
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isPrematureCompletionCandidate(result: SessionResult, state: RetryState): boolean {
|
|
300
|
+
if (!result.completed || result.signalName || !result.stopReason) return false;
|
|
301
|
+
if (state.retryNumber >= state.maxRetries) return false;
|
|
302
|
+
if (state.checkpoint) return false;
|
|
303
|
+
if (!result.claudeSessionId) return false;
|
|
304
|
+
return result.stopReason === 'max_tokens' || result.stopReason === 'end_turn';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function tryPrematureCompletion(
|
|
308
|
+
result: SessionResult,
|
|
309
|
+
state: RetryState,
|
|
310
|
+
config: RetryConfig,
|
|
311
|
+
): Promise<RetryDecision | null> {
|
|
312
|
+
if (!isPrematureCompletionCandidate(result, state)) return null;
|
|
313
|
+
|
|
314
|
+
const isMaxTokens = result.stopReason === 'max_tokens';
|
|
315
|
+
|
|
316
|
+
if (!isMaxTokens && !(await isEndTurnIncomplete(result, config.verbose))) return null;
|
|
317
|
+
|
|
318
|
+
state.retryNumber++;
|
|
319
|
+
const reason = isMaxTokens ? 'Output limit reached' : 'Task appears unfinished (AI assessment)';
|
|
320
|
+
|
|
321
|
+
hlog(`[RETRY] Premature completion: ${reason}, resuming session (retry ${state.retryNumber}/${state.maxRetries})`);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
nextPrompt: 'continue',
|
|
325
|
+
useResume: true,
|
|
326
|
+
resumeSessionId: result.claudeSessionId,
|
|
327
|
+
path: 'PrematureCompletion',
|
|
328
|
+
reason,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -239,6 +239,11 @@ export async function assessToolTimeout(
|
|
|
239
239
|
Task: 'spawns a subagent that runs autonomously with its own tools',
|
|
240
240
|
Agent: 'spawns a subagent that runs autonomously with its own tools',
|
|
241
241
|
Bash: 'executes a shell command',
|
|
242
|
+
Write: 'writes file content that may be very large; content streams token-by-token through stdio before disk write',
|
|
243
|
+
Edit: 'applies string replacements to a file; streams through stdio protocol',
|
|
244
|
+
Read: 'reads file contents that may be very large; result streams through stdio protocol',
|
|
245
|
+
Grep: 'searches file contents with regex; large codebases produce large result streams',
|
|
246
|
+
Glob: 'finds files by pattern; large directory trees take time to scan',
|
|
242
247
|
};
|
|
243
248
|
const toolDesc = toolDescriptions[toolName] || `executes the ${toolName} tool`;
|
|
244
249
|
|
|
@@ -98,10 +98,10 @@ export const DEFAULT_TOOL_TIMEOUT_PROFILES: Record<string, ToolTimeoutProfile> =
|
|
|
98
98
|
useHaikuTiebreaker: true,
|
|
99
99
|
},
|
|
100
100
|
Write: {
|
|
101
|
-
coldStartMs:
|
|
102
|
-
floorMs:
|
|
103
|
-
ceilingMs:
|
|
104
|
-
useAdaptive:
|
|
101
|
+
coldStartMs: 300_000, // 5 min — large docs stream slowly through stdio; model generates content inline
|
|
102
|
+
floorMs: 120_000, // 2 min minimum — prevents premature kills on big writes
|
|
103
|
+
ceilingMs: 600_000, // 10 min hard cap
|
|
104
|
+
useAdaptive: false, // bimodal: 1-line config vs 50KB research doc defeats EMA
|
|
105
105
|
useHaikuTiebreaker: true,
|
|
106
106
|
},
|
|
107
107
|
};
|
|
@@ -244,6 +244,22 @@ export class ToolWatchdog {
|
|
|
244
244
|
|
|
245
245
|
const elapsedMs = Date.now() - watch.startTime;
|
|
246
246
|
|
|
247
|
+
// Activity-gated auto-extension: if data is actively streaming, extend without
|
|
248
|
+
// consuming the one-shot Haiku tiebreaker. Respects ceiling to prevent runaway.
|
|
249
|
+
const tokenSilenceMs = this.getTokenSilenceMs?.();
|
|
250
|
+
if (tokenSilenceMs !== undefined && tokenSilenceMs < 60_000) {
|
|
251
|
+
const remainingToCeiling = profile.ceilingMs - elapsedMs;
|
|
252
|
+
if (remainingToCeiling > 0) {
|
|
253
|
+
const extensionMs = Math.min(5 * 60_000, remainingToCeiling);
|
|
254
|
+
if (this.verbose) {
|
|
255
|
+
hlog(`[WATCHDOG] ${toolName} (${toolId}) hit timeout after ${Math.round(elapsedMs / 1000)}s, but stream active ${Math.round(tokenSilenceMs / 1000)}s ago — auto-extending ${Math.round(extensionMs / 1000)}s`);
|
|
256
|
+
}
|
|
257
|
+
this.scheduleActivityGatedTimeout(watch, toolId, toolName, toolInput, profile, extensionMs, onTimeout);
|
|
258
|
+
watch.timeoutMs = elapsedMs + extensionMs;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
if (!profile.useHaikuTiebreaker || !this.onTiebreaker || watch.tiebreakerAttempted) {
|
|
248
264
|
if (this.verbose) {
|
|
249
265
|
hlog(`[WATCHDOG] ${toolName} (${toolId}) timed out after ${Math.round(elapsedMs / 1000)}s, killing`);
|
|
@@ -294,6 +310,26 @@ export class ToolWatchdog {
|
|
|
294
310
|
return false;
|
|
295
311
|
}
|
|
296
312
|
|
|
313
|
+
/** Schedule an activity-gated timeout that re-enters the full timeout handler */
|
|
314
|
+
private scheduleActivityGatedTimeout(
|
|
315
|
+
watch: ActiveWatch,
|
|
316
|
+
toolId: string,
|
|
317
|
+
toolName: string,
|
|
318
|
+
toolInput: Record<string, unknown>,
|
|
319
|
+
profile: ToolTimeoutProfile,
|
|
320
|
+
extensionMs: number,
|
|
321
|
+
onTimeout: () => void,
|
|
322
|
+
): void {
|
|
323
|
+
watch.timer = setTimeout(async () => {
|
|
324
|
+
const w = this.activeWatches.get(toolId);
|
|
325
|
+
if (!w) return;
|
|
326
|
+
const extended = await this.handleTimeoutWithTiebreaker(toolId, toolName, toolInput, profile, onTimeout);
|
|
327
|
+
if (!extended) {
|
|
328
|
+
onTimeout();
|
|
329
|
+
}
|
|
330
|
+
}, extensionMs);
|
|
331
|
+
}
|
|
332
|
+
|
|
297
333
|
/** Schedule a post-extension timeout that kills without another tiebreaker */
|
|
298
334
|
private scheduleExtensionTimeout(
|
|
299
335
|
watch: ActiveWatch,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { AnalyticsEvents, trackEvent } from '../services/analytics.js';
|
|
10
10
|
import { hlog } from './headless/headless-logger.js';
|
|
11
11
|
import { HeadlessRunner } from './headless/index.js';
|
|
12
|
+
import { extractFinalTextBlock, isResponseAbandoned } from './headless/retry-strategies.js';
|
|
12
13
|
import { assessBestResult, assessContextLoss, assessPrematureCompletion, type ContextLossContext } from './headless/stall-assessor.js';
|
|
13
14
|
import type { ExecutionCheckpoint } from './headless/types.js';
|
|
14
15
|
import type { FileAttachment, HeadlessRunResult, ImprovisationOptions, MovementRecord, RetryLoopState, SessionHistory } from './improvisation-types.js';
|
|
@@ -455,38 +456,6 @@ function isPrematureCompletionCandidate(
|
|
|
455
456
|
return result.stopReason === 'max_tokens' || result.stopReason === 'end_turn';
|
|
456
457
|
}
|
|
457
458
|
|
|
458
|
-
/**
|
|
459
|
-
* Fast heuristic: detect response abandonment without a Haiku call.
|
|
460
|
-
* When thinking is significantly longer than the response and the response
|
|
461
|
-
* contains no tool calls, Claude likely planned work it never executed.
|
|
462
|
-
* This pattern occurs after context compaction or heavy parallel tool results.
|
|
463
|
-
*/
|
|
464
|
-
function isResponseAbandoned(result: HeadlessRunResult): boolean {
|
|
465
|
-
const thinkingLen = result.thinkingOutput?.length ?? 0;
|
|
466
|
-
const responseLen = result.assistantResponse?.length ?? 0;
|
|
467
|
-
const toolCallsInResponse = result.toolUseHistory?.filter(t => t.result !== undefined).length ?? 0;
|
|
468
|
-
|
|
469
|
-
if (thinkingLen < 500 || responseLen > 1000) return false;
|
|
470
|
-
if (toolCallsInResponse > 0 && responseLen > 200) return false;
|
|
471
|
-
|
|
472
|
-
return thinkingLen >= responseLen * 3;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Extract the final text block from a concatenated response.
|
|
477
|
-
* The assistantResponse concatenates all text deltas including interleaved
|
|
478
|
-
* progress messages between tool calls. The final paragraph (after the last
|
|
479
|
-
* double-newline break) is the actual conclusion — earlier fragments are
|
|
480
|
-
* progress updates that were already acted on via tool calls.
|
|
481
|
-
*/
|
|
482
|
-
function extractFinalTextBlock(response: string, maxLen: number): string {
|
|
483
|
-
const lastBreak = response.lastIndexOf('\n\n');
|
|
484
|
-
if (lastBreak !== -1 && response.length - lastBreak > 20) {
|
|
485
|
-
return response.slice(lastBreak + 2).slice(-maxLen);
|
|
486
|
-
}
|
|
487
|
-
return response.slice(-maxLen);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
459
|
/** Use Haiku to assess whether an end_turn response is genuinely complete */
|
|
491
460
|
async function assessEndTurnCompletion(result: HeadlessRunResult, verbose: boolean): Promise<boolean> {
|
|
492
461
|
if (!result.assistantResponse) return false;
|
|
@@ -201,7 +201,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
201
201
|
|
|
202
202
|
let result = await this.runRetryLoop(state, sequenceNumber, promptWithAttachments, imageAttachments, options?.workingDir);
|
|
203
203
|
|
|
204
|
-
if (this._cancelled) {
|
|
204
|
+
if (this._cancelled || this._cancelCompleteEmitted) {
|
|
205
205
|
return this.handleCancelledExecution(result, displayPrompt, sequenceNumber, _execStart);
|
|
206
206
|
}
|
|
207
207
|
|
|
@@ -218,7 +218,9 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
218
218
|
this._executionStartTimestamp = undefined;
|
|
219
219
|
this.executionEventLog = [];
|
|
220
220
|
|
|
221
|
-
this.
|
|
221
|
+
if (!this._cancelCompleteEmitted) {
|
|
222
|
+
this.emitMovementComplete(movement, result, _execStart, sequenceNumber);
|
|
223
|
+
}
|
|
222
224
|
this.maybeAutoContinue(result, userPrompt);
|
|
223
225
|
|
|
224
226
|
return movement;
|
|
@@ -271,9 +273,15 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
271
273
|
let result: HeadlessRunResult | undefined;
|
|
272
274
|
const callbacks = this.buildRetryCallbacks();
|
|
273
275
|
|
|
276
|
+
const RETRY_BACKOFF_MS = [1000, 5000, 30000];
|
|
274
277
|
// eslint-disable-next-line no-constant-condition
|
|
275
278
|
while (true) {
|
|
276
279
|
if (this._cancelled) break;
|
|
280
|
+
if (state.retryNumber > 0) {
|
|
281
|
+
const delay = RETRY_BACKOFF_MS[Math.min(state.retryNumber - 1, RETRY_BACKOFF_MS.length - 1)];
|
|
282
|
+
await new Promise(r => setTimeout(r, delay));
|
|
283
|
+
if (this._cancelled) break;
|
|
284
|
+
}
|
|
277
285
|
const iteration = await this.executeRetryIteration(state, callbacks, sequenceNumber, imageAttachments, workingDirOverride);
|
|
278
286
|
result = iteration.result;
|
|
279
287
|
if (this._cancelled) break;
|
|
@@ -580,6 +588,8 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
580
588
|
this.currentRunner = null;
|
|
581
589
|
}
|
|
582
590
|
|
|
591
|
+
this.destroyQueueTimer();
|
|
592
|
+
|
|
583
593
|
if (this._isExecuting && !this._cancelCompleteEmitted) {
|
|
584
594
|
this._cancelCompleteEmitted = true;
|
|
585
595
|
const execStart = this._executionStartTimestamp || Date.now();
|
|
@@ -609,11 +619,15 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
609
619
|
this.flushOutputQueue();
|
|
610
620
|
}
|
|
611
621
|
|
|
612
|
-
|
|
622
|
+
private destroyQueueTimer(): void {
|
|
613
623
|
if (this.queueTimer) {
|
|
614
624
|
clearInterval(this.queueTimer);
|
|
615
625
|
this.queueTimer = null;
|
|
616
626
|
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
destroy(): void {
|
|
630
|
+
this.destroyQueueTimer();
|
|
617
631
|
this.flushOutputQueue();
|
|
618
632
|
}
|
|
619
633
|
|
|
@@ -22,13 +22,27 @@ export function summarizeToolInput(input: Record<string, unknown>): string {
|
|
|
22
22
|
return JSON.stringify(input).slice(0, 100);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const NETWORK_TOOLS = new Set(['WebFetch', 'WebSearch']);
|
|
26
|
+
|
|
25
27
|
/** Format a list of timed-out tools for retry prompts */
|
|
26
28
|
export function formatTimedOutTools(tools: Array<{ toolName: string; input: Record<string, unknown>; timeoutMs: number }>): string[] {
|
|
27
29
|
const lines: string[] = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const networkTools = tools.filter(t => NETWORK_TOOLS.has(t.toolName));
|
|
31
|
+
const localTools = tools.filter(t => !NETWORK_TOOLS.has(t.toolName));
|
|
32
|
+
|
|
33
|
+
if (networkTools.length > 0) {
|
|
34
|
+
lines.push('### Network resources that timed out (DO NOT retry these URLs):');
|
|
35
|
+
for (const t of networkTools) {
|
|
36
|
+
const inputSummary = summarizeToolInput(t.input);
|
|
37
|
+
lines.push(`- **${t.toolName}**(${inputSummary}) — timed out after ${Math.round(t.timeoutMs / 1000)}s`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (localTools.length > 0) {
|
|
41
|
+
lines.push('### Tools that previously timed out (OK to retry with same or smaller content):');
|
|
42
|
+
for (const t of localTools) {
|
|
43
|
+
const inputSummary = summarizeToolInput(t.input);
|
|
44
|
+
lines.push(`- **${t.toolName}**(${inputSummary}) — timed out after ${Math.round(t.timeoutMs / 1000)}s`);
|
|
45
|
+
}
|
|
32
46
|
}
|
|
33
47
|
return lines;
|
|
34
48
|
}
|
|
@@ -211,17 +225,24 @@ export function buildResumeRetryPrompt(
|
|
|
211
225
|
`Your previous ${checkpoint.hungTool.toolName} call timed out after ${Math.round(checkpoint.hungTool.timeoutMs / 1000)}s${checkpoint.hungTool.url ? ` fetching: ${checkpoint.hungTool.url}` : ''}.`
|
|
212
226
|
);
|
|
213
227
|
|
|
214
|
-
if (allTimedOut && allTimedOut.length >
|
|
228
|
+
if (allTimedOut && allTimedOut.length > 0) {
|
|
229
|
+
const networkTools = allTimedOut.filter(t => NETWORK_TOOLS.has(t.toolName));
|
|
230
|
+
const localTools = allTimedOut.filter(t => !NETWORK_TOOLS.has(t.toolName));
|
|
215
231
|
parts.push('');
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
232
|
+
if (networkTools.length > 0) {
|
|
233
|
+
parts.push('Network resources that timed out (DO NOT retry these URLs):');
|
|
234
|
+
for (const t of networkTools) {
|
|
235
|
+
parts.push(`- ${t.toolName}(${summarizeToolInput(t.input)})`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (localTools.length > 0) {
|
|
239
|
+
parts.push('Tools that previously timed out (OK to retry):');
|
|
240
|
+
for (const t of localTools) {
|
|
241
|
+
parts.push(`- ${t.toolName}(${summarizeToolInput(t.input)})`);
|
|
242
|
+
}
|
|
220
243
|
}
|
|
221
|
-
} else {
|
|
222
|
-
parts.push('This URL/resource is unreachable. DO NOT retry the same URL or query.');
|
|
223
244
|
}
|
|
224
|
-
parts.push('Continue your task — find
|
|
245
|
+
parts.push('Continue your task — find alternative sources for network failures, or proceed with the results you already have.');
|
|
225
246
|
|
|
226
247
|
return parts.join('\n');
|
|
227
248
|
}
|
package/server/index.ts
CHANGED
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
import { createPlatformRelayContext, ensureClaudeSettings, setTerminalTitle, wrapWebSocket } from './server-setup.js'
|
|
28
28
|
import { AnalyticsEvents, initAnalytics, shutdownAnalytics, trackEvent } from './services/analytics.js'
|
|
29
29
|
import { AuthService } from './services/auth.js'
|
|
30
|
-
import { createAiBrokerRoutes, setDeployHealthUpdateListener, setDeployUsageReportListener } from './services/deploy/ai-broker.js'
|
|
31
30
|
import { FileService } from './services/files.js'
|
|
32
31
|
import { InstanceRegistry, type MstroInstance } from './services/instances.js'
|
|
33
32
|
import { PlatformConnection } from './services/platform.js'
|
|
@@ -82,7 +81,7 @@ app.use('*', cors({
|
|
|
82
81
|
app.use('*', logger())
|
|
83
82
|
|
|
84
83
|
const authMiddleware = async (c: Context, next: Next) => {
|
|
85
|
-
const publicPaths = ['/health', '/api/config'
|
|
84
|
+
const publicPaths = ['/health', '/api/config']
|
|
86
85
|
if (publicPaths.some(path => c.req.path.startsWith(path))) {
|
|
87
86
|
return next()
|
|
88
87
|
}
|
|
@@ -105,7 +104,6 @@ app.route('/api/shutdown', createShutdownRoute(instanceRegistry))
|
|
|
105
104
|
app.route('/api/improvise', createImproviseRoutes(WORKING_DIR))
|
|
106
105
|
app.route('/api/files', createFileRoutes(fileService))
|
|
107
106
|
app.route('/api/notifications', createNotificationRoutes(WORKING_DIR))
|
|
108
|
-
app.route('/api/deploy/ai', createAiBrokerRoutes())
|
|
109
107
|
|
|
110
108
|
app.post('/api/reload-pty', async (c) => {
|
|
111
109
|
const success = await reloadPty()
|
|
@@ -195,12 +193,6 @@ async function startServer() {
|
|
|
195
193
|
wsHandler.setUsageReporter((report) => {
|
|
196
194
|
platformConnection.send({ type: 'reportUsage', data: report })
|
|
197
195
|
})
|
|
198
|
-
setDeployUsageReportListener((report) => {
|
|
199
|
-
platformConnection.send({ type: 'deployUsageReport', data: report })
|
|
200
|
-
})
|
|
201
|
-
setDeployHealthUpdateListener((update) => {
|
|
202
|
-
platformConnection.send({ type: 'deployAiHealthUpdate', data: update })
|
|
203
|
-
})
|
|
204
196
|
},
|
|
205
197
|
onDisconnected: () => {
|
|
206
198
|
if (platformRelayContext) {
|
|
@@ -29,14 +29,14 @@ function buildOperation(toolName: string, toolInput: Record<string, unknown>): s
|
|
|
29
29
|
|
|
30
30
|
async function evaluate(rawInput: string): Promise<{ decision: string; reason: string }> {
|
|
31
31
|
if (!rawInput.trim()) {
|
|
32
|
-
return { decision: '
|
|
32
|
+
return { decision: 'deny', reason: 'Empty input — cannot evaluate safety' };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
let parsed: { tool_name?: string; toolName?: string; input?: Record<string, unknown>; toolInput?: Record<string, unknown> };
|
|
36
36
|
try {
|
|
37
37
|
parsed = JSON.parse(rawInput);
|
|
38
38
|
} catch {
|
|
39
|
-
return { decision: '
|
|
39
|
+
return { decision: 'deny', reason: 'Invalid JSON input — cannot evaluate safety' };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const toolName = parsed.tool_name || parsed.toolName || 'unknown';
|
|
@@ -68,6 +68,7 @@ async function main(): Promise<void> {
|
|
|
68
68
|
console.log(JSON.stringify(result));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
main().catch(() => {
|
|
72
|
-
console.
|
|
71
|
+
main().catch((err) => {
|
|
72
|
+
console.error('[Bouncer] Fatal error:', err);
|
|
73
|
+
console.log(JSON.stringify({ decision: 'deny', reason: 'Bouncer crash — denying for safety' }));
|
|
73
74
|
});
|
|
@@ -95,7 +95,7 @@ export async function analyzeWithHaiku(
|
|
|
95
95
|
return new Promise((resolve, reject) => {
|
|
96
96
|
const userRequest = request.context?.userRequest;
|
|
97
97
|
const userContextBlock = userRequest
|
|
98
|
-
? `\nUSER'S ORIGINAL REQUEST (what the user actually asked Claude to do):\n
|
|
98
|
+
? `\nUSER'S ORIGINAL REQUEST (what the user actually asked Claude to do):\n<user_request>\n${userRequest}\n</user_request>\n`
|
|
99
99
|
: '';
|
|
100
100
|
|
|
101
101
|
const prompt = loadSkillPrompt('check-injection', {
|