mstro-app 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +126 -0
- package/README.md +24 -23
- package/bin/commands/login.js +79 -49
- package/bin/mstro.js +305 -39
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +137 -30
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +2 -2
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts +6 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +59 -4
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +20 -1
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +30 -24
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +20 -2
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +30 -3
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +224 -31
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +6 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +53 -14
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +70 -7
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts +3 -3
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/server.js +3 -2
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/services/analytics.d.ts +2 -2
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js +13 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/files.js +7 -7
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/pathUtils.js +1 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/platform.d.ts +2 -2
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +13 -1
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sentry.d.ts +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +12 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +81 -6
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-utils.d.ts +4 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/file-utils.js +27 -8
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-handlers.d.ts +36 -0
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-handlers.js +797 -0
- package/dist/server/services/websocket/git-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.js +299 -0
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
- package/dist/server/services/websocket/handler-context.d.ts +32 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
- package/dist/server/services/websocket/handler-context.js +4 -0
- package/dist/server/services/websocket/handler-context.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +27 -359
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +68 -2329
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/index.d.ts +1 -1
- package/dist/server/services/websocket/index.d.ts.map +1 -1
- package/dist/server/services/websocket/index.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +10 -0
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/session-handlers.js +508 -0
- package/dist/server/services/websocket/session-handlers.js.map +1 -0
- package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/settings-handlers.js +125 -0
- package/dist/server/services/websocket/settings-handlers.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-handlers.js +131 -0
- package/dist/server/services/websocket/tab-handlers.js.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.js +220 -0
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +63 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.d.ts +22 -2
- package/dist/server/utils/agent-manager.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.js +2 -2
- package/dist/server/utils/agent-manager.js.map +1 -1
- package/dist/server/utils/port-manager.js.map +1 -1
- package/hooks/bouncer.sh +17 -3
- package/package.json +7 -3
- package/server/README.md +176 -159
- package/server/cli/headless/claude-invoker.ts +172 -43
- package/server/cli/headless/mcp-config.ts +8 -8
- package/server/cli/headless/runner.ts +57 -4
- package/server/cli/headless/stall-assessor.ts +25 -0
- package/server/cli/headless/tool-watchdog.ts +33 -25
- package/server/cli/headless/types.ts +11 -2
- package/server/cli/improvisation-session-manager.ts +285 -37
- package/server/index.ts +15 -13
- package/server/mcp/README.md +59 -67
- package/server/mcp/bouncer-cli.ts +73 -20
- package/server/mcp/bouncer-integration.ts +99 -16
- package/server/mcp/security-audit.ts +4 -4
- package/server/mcp/server.ts +6 -5
- package/server/services/analytics.ts +16 -4
- package/server/services/files.ts +13 -13
- package/server/services/pathUtils.ts +2 -2
- package/server/services/platform.ts +17 -6
- package/server/services/sentry.ts +1 -1
- package/server/services/terminal/pty-manager.ts +88 -11
- package/server/services/websocket/file-explorer-handlers.ts +587 -0
- package/server/services/websocket/file-utils.ts +28 -9
- package/server/services/websocket/git-handlers.ts +924 -0
- package/server/services/websocket/git-pr-handlers.ts +363 -0
- package/server/services/websocket/git-worktree-handlers.ts +403 -0
- package/server/services/websocket/handler-context.ts +44 -0
- package/server/services/websocket/handler.ts +85 -2680
- package/server/services/websocket/index.ts +1 -1
- package/server/services/websocket/session-handlers.ts +575 -0
- package/server/services/websocket/settings-handlers.ts +150 -0
- package/server/services/websocket/tab-handlers.ts +150 -0
- package/server/services/websocket/terminal-handlers.ts +277 -0
- package/server/services/websocket/types.ts +137 -0
- package/server/utils/agent-manager.ts +6 -6
- package/server/utils/port-manager.ts +1 -1
- package/bin/release.sh +0 -110
- package/server/services/platform.test.ts +0 -1304
- package/server/services/websocket/handler.test.ts +0 -20
|
@@ -21,11 +21,26 @@ import type {
|
|
|
21
21
|
ToolUseEvent,
|
|
22
22
|
} from './types.js';
|
|
23
23
|
|
|
24
|
+
/** Parsed JSON from Claude CLI stream — structure varies by event type */
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: external CLI stream JSON with heterogeneous shapes
|
|
26
|
+
type StreamJson = any;
|
|
27
|
+
|
|
24
28
|
export interface ClaudeInvokerOptions {
|
|
25
29
|
config: ResolvedHeadlessConfig;
|
|
26
30
|
runningProcesses: Map<number, ChildProcess>;
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
// ========== Signal Helpers ==========
|
|
34
|
+
|
|
35
|
+
/** Map a Node.js signal name to its numeric value for exit code computation */
|
|
36
|
+
function signalToNumber(signal: string): number | undefined {
|
|
37
|
+
const map: Record<string, number> = {
|
|
38
|
+
SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6,
|
|
39
|
+
SIGKILL: 9, SIGTERM: 15, SIGUSR1: 10, SIGUSR2: 12,
|
|
40
|
+
};
|
|
41
|
+
return map[signal];
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
// ========== Stall Detection Helpers ==========
|
|
30
45
|
|
|
31
46
|
/** Summarize a tool's input for stall assessment context */
|
|
@@ -261,10 +276,16 @@ interface StreamHandlerContext {
|
|
|
261
276
|
resumeAssessmentActive: boolean;
|
|
262
277
|
/** Buffered assistant text during resume assessment */
|
|
263
278
|
resumeAssessmentBuffer: string;
|
|
279
|
+
/** Cumulative API token usage from message_start/message_delta events */
|
|
280
|
+
apiTokenUsage: { inputTokens: number; outputTokens: number };
|
|
281
|
+
/** Tracks cumulative output_tokens within the current step (message_delta is cumulative per-step) */
|
|
282
|
+
currentStepOutputTokens: number;
|
|
283
|
+
/** Timestamp of the last token usage change (tokens still flowing = process alive) */
|
|
284
|
+
lastTokenActivityTime: number;
|
|
264
285
|
}
|
|
265
286
|
|
|
266
287
|
function handleSessionCapture(
|
|
267
|
-
parsed:
|
|
288
|
+
parsed: StreamJson,
|
|
268
289
|
captured: { claudeSessionId?: string }
|
|
269
290
|
): void {
|
|
270
291
|
if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.session_id) {
|
|
@@ -275,7 +296,7 @@ function handleSessionCapture(
|
|
|
275
296
|
}
|
|
276
297
|
}
|
|
277
298
|
|
|
278
|
-
function handleThinkingDelta(event:
|
|
299
|
+
function handleThinkingDelta(event: StreamJson, ctx: StreamHandlerContext): string {
|
|
279
300
|
if (
|
|
280
301
|
event.type !== 'content_block_delta' ||
|
|
281
302
|
event.delta?.type !== 'thinking_delta' ||
|
|
@@ -307,7 +328,7 @@ function handleThinkingDelta(event: any, ctx: StreamHandlerContext): string {
|
|
|
307
328
|
return updated;
|
|
308
329
|
}
|
|
309
330
|
|
|
310
|
-
function handleTextDelta(event:
|
|
331
|
+
function handleTextDelta(event: StreamJson, ctx: StreamHandlerContext): string {
|
|
311
332
|
if (
|
|
312
333
|
event.type !== 'content_block_delta' ||
|
|
313
334
|
event.delta?.type !== 'text_delta' ||
|
|
@@ -349,7 +370,7 @@ function handleTextDelta(event: any, ctx: StreamHandlerContext): string {
|
|
|
349
370
|
return updated;
|
|
350
371
|
}
|
|
351
372
|
|
|
352
|
-
function handleToolStart(event:
|
|
373
|
+
function handleToolStart(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
353
374
|
if (
|
|
354
375
|
event.type !== 'content_block_start' ||
|
|
355
376
|
event.content_block?.type !== 'tool_use'
|
|
@@ -382,7 +403,7 @@ function handleToolStart(event: any, ctx: StreamHandlerContext): void {
|
|
|
382
403
|
}
|
|
383
404
|
}
|
|
384
405
|
|
|
385
|
-
function handleToolInputDelta(event:
|
|
406
|
+
function handleToolInputDelta(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
386
407
|
if (
|
|
387
408
|
event.type !== 'content_block_delta' ||
|
|
388
409
|
event.delta?.type !== 'input_json_delta'
|
|
@@ -403,7 +424,7 @@ function handleToolInputDelta(event: any, ctx: StreamHandlerContext): void {
|
|
|
403
424
|
}
|
|
404
425
|
}
|
|
405
426
|
|
|
406
|
-
function handleToolComplete(event:
|
|
427
|
+
function handleToolComplete(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
407
428
|
if (event.type !== 'content_block_stop') {
|
|
408
429
|
return;
|
|
409
430
|
}
|
|
@@ -414,7 +435,7 @@ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
|
|
|
414
435
|
return;
|
|
415
436
|
}
|
|
416
437
|
|
|
417
|
-
let completeInput:
|
|
438
|
+
let completeInput: Record<string, unknown> = {};
|
|
418
439
|
try {
|
|
419
440
|
completeInput = JSON.parse(toolBuffer.inputJson);
|
|
420
441
|
} catch (_e) {
|
|
@@ -428,6 +449,9 @@ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
|
|
|
428
449
|
startTime: toolBuffer.startTime
|
|
429
450
|
});
|
|
430
451
|
|
|
452
|
+
// Clean up the input buffer — it's no longer needed after accumulation
|
|
453
|
+
ctx.toolInputBuffers.delete(index);
|
|
454
|
+
|
|
431
455
|
if (ctx.config.toolUseCallback) {
|
|
432
456
|
ctx.config.toolUseCallback({
|
|
433
457
|
type: 'tool_complete',
|
|
@@ -439,7 +463,81 @@ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
|
|
|
439
463
|
}
|
|
440
464
|
}
|
|
441
465
|
|
|
442
|
-
|
|
466
|
+
/** Accumulate input tokens from a message_start event. Returns true if any tokens were added. */
|
|
467
|
+
function handleMessageStartTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
|
|
468
|
+
if (event.type !== 'message_start' || !event.message?.usage) return false;
|
|
469
|
+
const usage = event.message.usage;
|
|
470
|
+
ctx.currentStepOutputTokens = 0;
|
|
471
|
+
let changed = false;
|
|
472
|
+
if (typeof usage.input_tokens === 'number') {
|
|
473
|
+
ctx.apiTokenUsage.inputTokens += usage.input_tokens;
|
|
474
|
+
changed = true;
|
|
475
|
+
}
|
|
476
|
+
if (typeof usage.cache_creation_input_tokens === 'number') {
|
|
477
|
+
ctx.apiTokenUsage.inputTokens += usage.cache_creation_input_tokens;
|
|
478
|
+
changed = true;
|
|
479
|
+
}
|
|
480
|
+
if (typeof usage.cache_read_input_tokens === 'number') {
|
|
481
|
+
ctx.apiTokenUsage.inputTokens += usage.cache_read_input_tokens;
|
|
482
|
+
changed = true;
|
|
483
|
+
}
|
|
484
|
+
verboseLog(ctx.config.verbose,
|
|
485
|
+
`[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}`);
|
|
486
|
+
return changed;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Accumulate output tokens from a message_delta event. Returns true if any tokens were added.
|
|
490
|
+
* message_delta carries CUMULATIVE output token count for the current step.
|
|
491
|
+
* Per Anthropic docs: "The token counts shown in the usage field of the
|
|
492
|
+
* message_delta event are cumulative" and there can be "one or more message_delta
|
|
493
|
+
* events" per message. We track the delta from the previous value to avoid
|
|
494
|
+
* double-counting when multiple message_delta events fire per step. */
|
|
495
|
+
function handleMessageDeltaTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
|
|
496
|
+
if (event.type !== 'message_delta' || !event.usage) return false;
|
|
497
|
+
if (typeof event.usage.output_tokens !== 'number') return false;
|
|
498
|
+
const increment = event.usage.output_tokens - ctx.currentStepOutputTokens;
|
|
499
|
+
verboseLog(ctx.config.verbose,
|
|
500
|
+
`[TOKENS] message_delta: output=${event.usage.output_tokens} (step_prev=${ctx.currentStepOutputTokens} increment=${increment}) → total_output=${ctx.apiTokenUsage.outputTokens + Math.max(increment, 0)}`);
|
|
501
|
+
if (increment <= 0) return false;
|
|
502
|
+
ctx.apiTokenUsage.outputTokens += increment;
|
|
503
|
+
ctx.currentStepOutputTokens = event.usage.output_tokens;
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function handleTokenUsage(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
508
|
+
const changed = handleMessageStartTokens(event, ctx) || handleMessageDeltaTokens(event, ctx);
|
|
509
|
+
if (changed) {
|
|
510
|
+
ctx.lastTokenActivityTime = Date.now();
|
|
511
|
+
ctx.config.tokenUsageCallback?.({ ...ctx.apiTokenUsage });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Extract definitive token usage from the result event emitted at the end of a Claude session.
|
|
517
|
+
* The result event's `usage` field contains the authoritative total — it overrides any
|
|
518
|
+
* accumulated stream-based counts which may be incomplete (e.g., when extended thinking
|
|
519
|
+
* suppresses stream_event emissions).
|
|
520
|
+
*/
|
|
521
|
+
function handleResultTokenUsage(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
522
|
+
if (!parsed.usage) return;
|
|
523
|
+
const u = parsed.usage;
|
|
524
|
+
const input = (typeof u.input_tokens === 'number' ? u.input_tokens : 0)
|
|
525
|
+
+ (typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0)
|
|
526
|
+
+ (typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0);
|
|
527
|
+
const output = typeof u.output_tokens === 'number' ? u.output_tokens : 0;
|
|
528
|
+
|
|
529
|
+
if (input > 0 || output > 0) {
|
|
530
|
+
verboseLog(ctx.config.verbose,
|
|
531
|
+
`[TOKENS] Result event usage: input=${input} output=${output} ` +
|
|
532
|
+
`(stream accumulated: input=${ctx.apiTokenUsage.inputTokens} output=${ctx.apiTokenUsage.outputTokens})`);
|
|
533
|
+
// Replace with authoritative counts from the result event
|
|
534
|
+
ctx.apiTokenUsage = { inputTokens: input, outputTokens: output };
|
|
535
|
+
ctx.lastTokenActivityTime = Date.now();
|
|
536
|
+
ctx.config.tokenUsageCallback?.({ ...ctx.apiTokenUsage });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function handleToolResult(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
443
541
|
if (parsed.type !== 'user' || !parsed.message?.content) {
|
|
444
542
|
return;
|
|
445
543
|
}
|
|
@@ -489,7 +587,7 @@ function processStreamLines(
|
|
|
489
587
|
return remainder;
|
|
490
588
|
}
|
|
491
589
|
|
|
492
|
-
function processStreamEvent(parsed:
|
|
590
|
+
function processStreamEvent(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
493
591
|
// Handle error events from Claude CLI (API errors, model errors, etc.)
|
|
494
592
|
if (parsed.type === 'error') {
|
|
495
593
|
const errorMessage = parsed.error?.message || parsed.message || JSON.stringify(parsed);
|
|
@@ -497,11 +595,14 @@ function processStreamEvent(parsed: any, ctx: StreamHandlerContext): void {
|
|
|
497
595
|
return;
|
|
498
596
|
}
|
|
499
597
|
|
|
500
|
-
// Handle result events
|
|
501
|
-
if (parsed.type === 'result'
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
598
|
+
// Handle result events — extract definitive token usage and surface errors
|
|
599
|
+
if (parsed.type === 'result') {
|
|
600
|
+
handleResultTokenUsage(parsed, ctx);
|
|
601
|
+
if (parsed.is_error) {
|
|
602
|
+
const errorMessage = parsed.error || parsed.result || 'Unknown error in result';
|
|
603
|
+
ctx.config.outputCallback?.(`\n[[MSTRO_ERROR:CLAUDE_RESULT_ERROR]] ${errorMessage}\n`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
505
606
|
}
|
|
506
607
|
|
|
507
608
|
if (parsed.type === 'stream_event' && parsed.event) {
|
|
@@ -511,6 +612,7 @@ function processStreamEvent(parsed: any, ctx: StreamHandlerContext): void {
|
|
|
511
612
|
handleToolStart(event, ctx);
|
|
512
613
|
handleToolInputDelta(event, ctx);
|
|
513
614
|
handleToolComplete(event, ctx);
|
|
615
|
+
handleTokenUsage(event, ctx);
|
|
514
616
|
}
|
|
515
617
|
handleToolResult(parsed, ctx);
|
|
516
618
|
}
|
|
@@ -672,11 +774,13 @@ async function runStallCheckTick(
|
|
|
672
774
|
claudeProcess: ChildProcess;
|
|
673
775
|
stallCheckInterval: ReturnType<typeof setInterval>;
|
|
674
776
|
config: ResolvedHeadlessConfig;
|
|
777
|
+
lastTokenActivityTime: number;
|
|
675
778
|
},
|
|
676
779
|
): Promise<void> {
|
|
677
780
|
const now = Date.now();
|
|
678
781
|
const silenceMs = now - state.lastActivityTime;
|
|
679
782
|
const totalElapsed = now - opts.perfStart;
|
|
783
|
+
const tokenSilenceMs = now - opts.lastTokenActivityTime;
|
|
680
784
|
|
|
681
785
|
if (totalElapsed >= opts.stallHardCapMs) {
|
|
682
786
|
terminateStallProcess(opts.claudeProcess, opts.stallCheckInterval, opts.config,
|
|
@@ -685,6 +789,13 @@ async function runStallCheckTick(
|
|
|
685
789
|
return;
|
|
686
790
|
}
|
|
687
791
|
|
|
792
|
+
// Token activity pushes the kill deadline forward — tokens flowing means
|
|
793
|
+
// the process is alive even if stdout is silent (e.g. silent thinking).
|
|
794
|
+
if (tokenSilenceMs < 60_000 && now < state.currentKillDeadline) {
|
|
795
|
+
const killMs = opts.config.stallKillMs ?? 1_800_000;
|
|
796
|
+
state.currentKillDeadline = Math.max(state.currentKillDeadline, now + killMs);
|
|
797
|
+
}
|
|
798
|
+
|
|
688
799
|
if (now >= state.currentKillDeadline) {
|
|
689
800
|
terminateStallProcess(opts.claudeProcess, opts.stallCheckInterval, opts.config,
|
|
690
801
|
`\n[[MSTRO_ERROR:EXECUTION_STALLED]] No output for ${Math.round(silenceMs / 60_000)} minutes. Terminating process.\n`
|
|
@@ -703,6 +814,7 @@ async function runStallCheckTick(
|
|
|
703
814
|
pendingToolNames: new Set(opts.pendingTools.values()),
|
|
704
815
|
totalToolCalls: opts.totalToolCalls,
|
|
705
816
|
elapsedTotalMs: totalElapsed,
|
|
817
|
+
tokenSilenceMs,
|
|
706
818
|
};
|
|
707
819
|
|
|
708
820
|
if (opts.stallAssessEnabled && state.extensionsGranted < opts.maxExtensions) {
|
|
@@ -762,12 +874,13 @@ function onToolStart(event: ToolUseEvent, s: ToolTrackingState): void {
|
|
|
762
874
|
/** Handle tool_complete events. Extracted to reduce cognitive complexity. */
|
|
763
875
|
function onToolComplete(event: ToolUseEvent, s: ToolTrackingState): void {
|
|
764
876
|
const id = event.toolId!;
|
|
765
|
-
|
|
766
|
-
s.
|
|
877
|
+
const input = event.completeInput ?? {};
|
|
878
|
+
s.counters.lastToolInputSummary = summarizeToolInput(input);
|
|
879
|
+
s.toolIdToInput.set(id, input);
|
|
767
880
|
if (!s.watchdog) return;
|
|
768
881
|
const toolName = s.toolIdToName.get(id);
|
|
769
882
|
if (toolName) {
|
|
770
|
-
s.watchdog.startWatch(id, toolName,
|
|
883
|
+
s.watchdog.startWatch(id, toolName, input, () => { s.onTimeout(id); });
|
|
771
884
|
}
|
|
772
885
|
}
|
|
773
886
|
|
|
@@ -847,8 +960,12 @@ function setupToolTracking(
|
|
|
847
960
|
? new ToolWatchdog({
|
|
848
961
|
profiles: config.toolTimeoutProfiles,
|
|
849
962
|
verbose: config.verbose,
|
|
850
|
-
onTiebreaker: async (toolName, toolInput, elapsedMs) => {
|
|
851
|
-
return assessToolTimeout(toolName, toolInput, elapsedMs, config.claudeCommand, config.verbose);
|
|
963
|
+
onTiebreaker: async (toolName, toolInput, elapsedMs, tokenSilenceMs) => {
|
|
964
|
+
return assessToolTimeout(toolName, toolInput, elapsedMs, config.claudeCommand, config.verbose, tokenSilenceMs);
|
|
965
|
+
},
|
|
966
|
+
getTokenSilenceMs: () => {
|
|
967
|
+
const last = ctx.lastTokenActivityTime;
|
|
968
|
+
return last > 0 ? Date.now() - last : undefined;
|
|
852
969
|
},
|
|
853
970
|
})
|
|
854
971
|
: null;
|
|
@@ -978,6 +1095,9 @@ export async function executeClaudeCommand(
|
|
|
978
1095
|
nativeTimeoutDetector: new NativeTimeoutDetector(),
|
|
979
1096
|
resumeAssessmentActive: isResumeMode,
|
|
980
1097
|
resumeAssessmentBuffer: '',
|
|
1098
|
+
apiTokenUsage: { inputTokens: 0, outputTokens: 0 },
|
|
1099
|
+
currentStepOutputTokens: 0,
|
|
1100
|
+
lastTokenActivityTime: Date.now(),
|
|
981
1101
|
};
|
|
982
1102
|
|
|
983
1103
|
// Stall detection state (mutable object shared with runStallCheckTick)
|
|
@@ -1044,7 +1164,7 @@ export async function executeClaudeCommand(
|
|
|
1044
1164
|
runStallCheckTick(stallState, {
|
|
1045
1165
|
perfStart, stallWarningMs, stallHardCapMs, maxExtensions, stallAssessEnabled,
|
|
1046
1166
|
toolWatchdogActive, prompt, pendingTools, lastToolInputSummary: toolCounters.lastToolInputSummary, totalToolCalls: toolCounters.totalToolCalls,
|
|
1047
|
-
claudeProcess, stallCheckInterval, config,
|
|
1167
|
+
claudeProcess, stallCheckInterval, config, lastTokenActivityTime: ctx.lastTokenActivityTime,
|
|
1048
1168
|
});
|
|
1049
1169
|
}, 10_000);
|
|
1050
1170
|
|
|
@@ -1052,38 +1172,47 @@ export async function executeClaudeCommand(
|
|
|
1052
1172
|
toolTracking.setKillContext(claudeProcess, stallCheckInterval);
|
|
1053
1173
|
|
|
1054
1174
|
return new Promise((resolve, reject) => {
|
|
1055
|
-
claudeProcess.on('close', async (code) => {
|
|
1175
|
+
claudeProcess.on('close', async (code, signal) => {
|
|
1056
1176
|
clearInterval(stallCheckInterval);
|
|
1057
1177
|
watchdog?.clearAll();
|
|
1058
|
-
|
|
1059
|
-
const postTimeout = flushNativeTimeoutBuffers(ctx);
|
|
1060
1178
|
await classifyUnmatchedStderr(stderr, errorAlreadySurfaced, code, config);
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
if (claudeProcess.pid) {
|
|
1064
|
-
runningProcesses.delete(claudeProcess.pid);
|
|
1065
|
-
}
|
|
1066
|
-
resolve({
|
|
1067
|
-
output: stdout,
|
|
1068
|
-
error: stderr || undefined,
|
|
1069
|
-
exitCode: code || 0,
|
|
1070
|
-
assistantResponse: ctx.accumulatedAssistantResponse || undefined,
|
|
1071
|
-
thinkingOutput: ctx.accumulatedThinking || undefined,
|
|
1072
|
-
toolUseHistory: ctx.accumulatedToolUse.length > 0 ? ctx.accumulatedToolUse : undefined,
|
|
1073
|
-
claudeSessionId: sessionCapture.claudeSessionId,
|
|
1074
|
-
nativeTimeoutCount: ctx.nativeTimeoutDetector.timeoutCount || undefined,
|
|
1075
|
-
postTimeoutOutput: postTimeout,
|
|
1076
|
-
resumeBufferedOutput: resumeBuffered,
|
|
1077
|
-
});
|
|
1179
|
+
if (claudeProcess.pid) runningProcesses.delete(claudeProcess.pid);
|
|
1180
|
+
resolve(buildCloseResult(ctx, stdout, stderr, code, signal, sessionCapture));
|
|
1078
1181
|
});
|
|
1079
1182
|
|
|
1080
1183
|
claudeProcess.on('error', (error: NodeJS.ErrnoException) => {
|
|
1081
1184
|
clearInterval(stallCheckInterval);
|
|
1082
1185
|
watchdog?.clearAll();
|
|
1083
|
-
if (claudeProcess.pid)
|
|
1084
|
-
runningProcesses.delete(claudeProcess.pid);
|
|
1085
|
-
}
|
|
1186
|
+
if (claudeProcess.pid) runningProcesses.delete(claudeProcess.pid);
|
|
1086
1187
|
handleSpawnError(error, config, reject);
|
|
1087
1188
|
});
|
|
1088
1189
|
});
|
|
1089
1190
|
}
|
|
1191
|
+
|
|
1192
|
+
function buildCloseResult(
|
|
1193
|
+
ctx: StreamHandlerContext,
|
|
1194
|
+
stdout: string,
|
|
1195
|
+
stderr: string,
|
|
1196
|
+
code: number | null,
|
|
1197
|
+
signal: NodeJS.Signals | null,
|
|
1198
|
+
sessionCapture: { claudeSessionId?: string },
|
|
1199
|
+
): ExecutionResult {
|
|
1200
|
+
const postTimeout = flushNativeTimeoutBuffers(ctx);
|
|
1201
|
+
const resumeBuffered = ctx.resumeAssessmentActive ? (ctx.resumeAssessmentBuffer || undefined) : undefined;
|
|
1202
|
+
const exitCode = code ?? (signal ? 128 + (signalToNumber(signal) ?? 0) : 0);
|
|
1203
|
+
const hasTokenUsage = ctx.apiTokenUsage.inputTokens > 0 || ctx.apiTokenUsage.outputTokens > 0;
|
|
1204
|
+
return {
|
|
1205
|
+
output: stdout,
|
|
1206
|
+
error: stderr || undefined,
|
|
1207
|
+
exitCode,
|
|
1208
|
+
signalName: signal || undefined,
|
|
1209
|
+
assistantResponse: ctx.accumulatedAssistantResponse || undefined,
|
|
1210
|
+
thinkingOutput: ctx.accumulatedThinking || undefined,
|
|
1211
|
+
toolUseHistory: ctx.accumulatedToolUse.length > 0 ? ctx.accumulatedToolUse : undefined,
|
|
1212
|
+
claudeSessionId: sessionCapture.claudeSessionId,
|
|
1213
|
+
nativeTimeoutCount: ctx.nativeTimeoutDetector.timeoutCount || undefined,
|
|
1214
|
+
postTimeoutOutput: postTimeout,
|
|
1215
|
+
resumeBufferedOutput: resumeBuffered,
|
|
1216
|
+
apiTokenUsage: hasTokenUsage ? { ...ctx.apiTokenUsage } : undefined,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
@@ -12,8 +12,8 @@ import { MCP_SERVER_PATH, MSTRO_ROOT } from '../../utils/paths.js';
|
|
|
12
12
|
/**
|
|
13
13
|
* Load user's MCP servers from ~/.claude.json (global + project-level)
|
|
14
14
|
*/
|
|
15
|
-
function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string,
|
|
16
|
-
const servers: Record<string,
|
|
15
|
+
function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string, unknown> {
|
|
16
|
+
const servers: Record<string, unknown> = {};
|
|
17
17
|
const claudeConfigPath = join(homedir(), '.claude.json');
|
|
18
18
|
|
|
19
19
|
if (!existsSync(claudeConfigPath)) {
|
|
@@ -29,7 +29,7 @@ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string
|
|
|
29
29
|
|
|
30
30
|
if (claudeConfig.projects && typeof claudeConfig.projects === 'object') {
|
|
31
31
|
for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
|
|
32
|
-
const projectServers = (projectConfig as
|
|
32
|
+
const projectServers = (projectConfig as Record<string, unknown>)?.mcpServers;
|
|
33
33
|
if (workingDir.startsWith(projectPath) && typeof projectServers === 'object') {
|
|
34
34
|
Object.assign(servers, projectServers);
|
|
35
35
|
}
|
|
@@ -39,8 +39,8 @@ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string
|
|
|
39
39
|
if (verbose) {
|
|
40
40
|
console.log(`[${new Date().toISOString()}] Loaded ${Object.keys(servers).length} user MCP servers from ~/.claude.json`);
|
|
41
41
|
}
|
|
42
|
-
} catch (parseError:
|
|
43
|
-
console.error(`[${new Date().toISOString()}] Failed to parse ~/.claude.json: ${parseError.message}`);
|
|
42
|
+
} catch (parseError: unknown) {
|
|
43
|
+
console.error(`[${new Date().toISOString()}] Failed to parse ~/.claude.json: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return servers;
|
|
@@ -57,7 +57,7 @@ export function generateMcpConfig(workingDir: string, verbose: boolean = false):
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const mcpServers: Record<string,
|
|
60
|
+
const mcpServers: Record<string, unknown> = {
|
|
61
61
|
'mstro-bouncer': {
|
|
62
62
|
command: 'npx',
|
|
63
63
|
args: ['tsx', MCP_SERVER_PATH],
|
|
@@ -80,8 +80,8 @@ export function generateMcpConfig(workingDir: string, verbose: boolean = false):
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
return configPath;
|
|
83
|
-
} catch (error:
|
|
84
|
-
console.error(`[${new Date().toISOString()}] Failed to generate MCP config: ${error.message}`);
|
|
83
|
+
} catch (error: unknown) {
|
|
84
|
+
console.error(`[${new Date().toISOString()}] Failed to generate MCP config: ${error instanceof Error ? error.message : String(error)}`);
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -89,6 +89,29 @@ export class HeadlessRunner {
|
|
|
89
89
|
const result = await this.executePromptCommand(enrichedPrompt, 'main', 1);
|
|
90
90
|
|
|
91
91
|
if (result.exitCode !== 0) {
|
|
92
|
+
// Signal exits (128+) with meaningful output are successful completions —
|
|
93
|
+
// Claude finished its work but the process was killed by signal (e.g., stall watchdog SIGTERM)
|
|
94
|
+
const isSignalExit = result.exitCode >= 128;
|
|
95
|
+
const hasOutput = !!(result.assistantResponse || (result.toolUseHistory && result.toolUseHistory.length > 0));
|
|
96
|
+
|
|
97
|
+
if (isSignalExit && hasOutput) {
|
|
98
|
+
const tokens = estimateTokensFromOutput(result.output);
|
|
99
|
+
return {
|
|
100
|
+
completed: true,
|
|
101
|
+
needsHandoff: false,
|
|
102
|
+
totalTokens: tokens,
|
|
103
|
+
sessionId,
|
|
104
|
+
signalName: result.signalName,
|
|
105
|
+
assistantResponse: result.assistantResponse,
|
|
106
|
+
thinkingOutput: result.thinkingOutput,
|
|
107
|
+
toolUseHistory: result.toolUseHistory,
|
|
108
|
+
claudeSessionId: result.claudeSessionId,
|
|
109
|
+
nativeTimeoutCount: result.nativeTimeoutCount,
|
|
110
|
+
postTimeoutOutput: result.postTimeoutOutput,
|
|
111
|
+
resumeBufferedOutput: result.resumeBufferedOutput,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
// Build meaningful error: prefer stderr, fall back to non-JSON stdout lines
|
|
93
116
|
let errorMessage = result.error;
|
|
94
117
|
if (!errorMessage && result.output) {
|
|
@@ -106,6 +129,7 @@ export class HeadlessRunner {
|
|
|
106
129
|
totalTokens: 0,
|
|
107
130
|
sessionId,
|
|
108
131
|
error: errorMessage || `Claude exited with code ${result.exitCode}`,
|
|
132
|
+
signalName: result.signalName,
|
|
109
133
|
assistantResponse: result.assistantResponse,
|
|
110
134
|
thinkingOutput: result.thinkingOutput,
|
|
111
135
|
toolUseHistory: result.toolUseHistory,
|
|
@@ -123,6 +147,7 @@ export class HeadlessRunner {
|
|
|
123
147
|
needsHandoff: false,
|
|
124
148
|
totalTokens: tokens,
|
|
125
149
|
sessionId,
|
|
150
|
+
signalName: result.signalName,
|
|
126
151
|
assistantResponse: result.assistantResponse,
|
|
127
152
|
thinkingOutput: result.thinkingOutput,
|
|
128
153
|
toolUseHistory: result.toolUseHistory,
|
|
@@ -150,12 +175,40 @@ export class HeadlessRunner {
|
|
|
150
175
|
}
|
|
151
176
|
|
|
152
177
|
/**
|
|
153
|
-
* Cleanup on exit
|
|
178
|
+
* Cleanup on exit — SIGTERM all tracked processes, then SIGKILL stragglers after 5s
|
|
154
179
|
*/
|
|
155
180
|
cleanup(): void {
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
if (this.runningProcesses.size === 0) return;
|
|
182
|
+
|
|
183
|
+
const pids = new Set<number>();
|
|
184
|
+
for (const [pid, proc] of this.runningProcesses) {
|
|
185
|
+
pids.add(pid);
|
|
186
|
+
try { proc.kill('SIGTERM'); } catch { /* already dead */ }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// SIGKILL fallback after 5 seconds for any process that didn't exit
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
for (const [pid, proc] of this.runningProcesses) {
|
|
192
|
+
if (pids.has(pid) && !proc.killed) {
|
|
193
|
+
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.runningProcesses.clear();
|
|
197
|
+
}, 5000);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sweep for zombie processes — entries in runningProcesses whose underlying
|
|
202
|
+
* process has already exited but whose 'close' event was missed.
|
|
203
|
+
*/
|
|
204
|
+
sweepZombies(): number {
|
|
205
|
+
let swept = 0;
|
|
206
|
+
for (const [pid, proc] of this.runningProcesses) {
|
|
207
|
+
if (proc.exitCode !== null || proc.killed) {
|
|
208
|
+
this.runningProcesses.delete(pid);
|
|
209
|
+
swept++;
|
|
210
|
+
}
|
|
158
211
|
}
|
|
159
|
-
|
|
212
|
+
return swept;
|
|
160
213
|
}
|
|
161
214
|
}
|
|
@@ -35,6 +35,8 @@ export interface StallContext {
|
|
|
35
35
|
totalToolCalls: number;
|
|
36
36
|
/** Total wall-clock time since process started (ms) */
|
|
37
37
|
elapsedTotalMs: number;
|
|
38
|
+
/** Time since the last token usage event (ms). Undefined if no token events yet. */
|
|
39
|
+
tokenSilenceMs?: number;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface StallVerdict {
|
|
@@ -57,6 +59,17 @@ function quickHeuristic(ctx: StallContext, toolWatchdogActive = false): StallVer
|
|
|
57
59
|
const pendingNames = ctx.pendingToolNames ?? new Set<string>();
|
|
58
60
|
const hasPendingTools = ctx.pendingToolCount > 0;
|
|
59
61
|
|
|
62
|
+
// Tokens still flowing = process is alive and actively processing.
|
|
63
|
+
// Extend generously when token activity is recent (< 60s), regardless
|
|
64
|
+
// of stdout silence. This covers silent thinking and tool result processing.
|
|
65
|
+
if (ctx.tokenSilenceMs !== undefined && ctx.tokenSilenceMs < 60_000) {
|
|
66
|
+
return {
|
|
67
|
+
action: 'extend',
|
|
68
|
+
extensionMs: 10 * 60_000,
|
|
69
|
+
reason: `Tokens still flowing (last activity ${Math.round(ctx.tokenSilenceMs / 1000)}s ago) — process is alive`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
// When the watchdog is active and tools are pending, always defer.
|
|
61
74
|
// The watchdog manages per-tool timeouts; the stall detector should only
|
|
62
75
|
// fire when no tools are running and there's genuine silence.
|
|
@@ -156,6 +169,7 @@ export async function assessToolTimeout(
|
|
|
156
169
|
elapsedMs: number,
|
|
157
170
|
claudeCommand: string,
|
|
158
171
|
verbose: boolean,
|
|
172
|
+
tokenSilenceMs?: number,
|
|
159
173
|
): Promise<StallVerdict> {
|
|
160
174
|
const elapsedSec = Math.round(elapsedMs / 1000);
|
|
161
175
|
|
|
@@ -181,13 +195,19 @@ export async function assessToolTimeout(
|
|
|
181
195
|
};
|
|
182
196
|
const toolDesc = toolDescriptions[toolName] || `executes the ${toolName} tool`;
|
|
183
197
|
|
|
198
|
+
const tokenLine = tokenSilenceMs !== undefined
|
|
199
|
+
? `Token activity: last token event ${Math.round(tokenSilenceMs / 1000)}s ago (recent tokens = process is alive and processing)`
|
|
200
|
+
: 'Token activity: no token events observed';
|
|
201
|
+
|
|
184
202
|
const prompt = [
|
|
185
203
|
`You are a process health monitor. A ${toolName} tool call has been running for ${elapsedSec}s.`,
|
|
186
204
|
`${toolName} ${toolDesc}.`,
|
|
187
205
|
`Tool input: ${inputSummary}`,
|
|
206
|
+
tokenLine,
|
|
188
207
|
'',
|
|
189
208
|
`Is this tool call likely still working, or is it hung/frozen?`,
|
|
190
209
|
'Consider: network latency, server response times, anti-bot protections, large page sizes, complex operations.',
|
|
210
|
+
'IMPORTANT: If tokens were active recently (< 60s ago), the process is likely still alive and processing — strongly favor WORKING.',
|
|
191
211
|
'',
|
|
192
212
|
'Respond in EXACTLY this format (3 lines, no extra text):',
|
|
193
213
|
'VERDICT: WORKING or STALLED',
|
|
@@ -305,6 +325,10 @@ function buildAssessmentPrompt(ctx: StallContext): string {
|
|
|
305
325
|
? `${ctx.originalPrompt.slice(0, 500)}...`
|
|
306
326
|
: ctx.originalPrompt;
|
|
307
327
|
|
|
328
|
+
const tokenLine = ctx.tokenSilenceMs !== undefined
|
|
329
|
+
? `Token activity: last token event ${Math.round(ctx.tokenSilenceMs / 1000)}s ago (tokens flowing = process alive)`
|
|
330
|
+
: 'Token activity: no token events observed';
|
|
331
|
+
|
|
308
332
|
return [
|
|
309
333
|
'You are a process health monitor. A Claude Code subprocess has been silent (no stdout) and you must determine if it is working or stalled.',
|
|
310
334
|
'',
|
|
@@ -314,6 +338,7 @@ function buildAssessmentPrompt(ctx: StallContext): string {
|
|
|
314
338
|
ctx.lastToolInputSummary ? `Last tool input: ${ctx.lastToolInputSummary}` : '',
|
|
315
339
|
`Pending tool calls: ${ctx.pendingToolCount}`,
|
|
316
340
|
`Total tool calls this session: ${ctx.totalToolCalls}`,
|
|
341
|
+
tokenLine,
|
|
317
342
|
`Task being executed: ${promptPreview}`,
|
|
318
343
|
'',
|
|
319
344
|
'Respond in EXACTLY this format (3 lines, no extra text):',
|