taskplane 0.29.2 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- package/extensions/reviewer-extension.ts +17 -11
- package/extensions/taskplane/abort.ts +50 -18
- package/extensions/taskplane/agent-bridge-extension.ts +232 -105
- package/extensions/taskplane/agent-host.ts +224 -97
- package/extensions/taskplane/cleanup.ts +71 -42
- package/extensions/taskplane/config-loader.ts +142 -58
- package/extensions/taskplane/config-schema.ts +6 -13
- package/extensions/taskplane/config.ts +10 -2
- package/extensions/taskplane/diagnostic-reports.ts +59 -47
- package/extensions/taskplane/diagnostics.ts +13 -13
- package/extensions/taskplane/discovery.ts +35 -61
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +426 -206
- package/extensions/taskplane/extension.ts +1073 -598
- package/extensions/taskplane/formatting.ts +136 -124
- package/extensions/taskplane/git.ts +0 -2
- package/extensions/taskplane/lane-runner.ts +542 -311
- package/extensions/taskplane/mailbox.ts +57 -49
- package/extensions/taskplane/merge.ts +662 -383
- package/extensions/taskplane/messages.ts +109 -51
- package/extensions/taskplane/migrations.ts +1 -1
- package/extensions/taskplane/path-resolver.ts +8 -9
- package/extensions/taskplane/persistence.ts +425 -262
- package/extensions/taskplane/process-registry.ts +36 -7
- package/extensions/taskplane/quality-gate.ts +107 -55
- package/extensions/taskplane/resume.ts +774 -267
- package/extensions/taskplane/sessions.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +505 -164
- package/extensions/taskplane/sidecar-telemetry.ts +25 -10
- package/extensions/taskplane/supervisor.ts +477 -270
- package/extensions/taskplane/task-executor-core.ts +178 -53
- package/extensions/taskplane/types.ts +186 -108
- package/extensions/taskplane/verification.ts +27 -22
- package/extensions/taskplane/waves.ts +59 -43
- package/extensions/taskplane/workspace.ts +14 -12
- package/extensions/taskplane/worktree.ts +218 -196
- package/package.json +14 -2
|
@@ -22,8 +22,13 @@
|
|
|
22
22
|
|
|
23
23
|
import { spawn, type ChildProcess } from "child_process";
|
|
24
24
|
import {
|
|
25
|
-
readFileSync,
|
|
26
|
-
|
|
25
|
+
readFileSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
appendFileSync,
|
|
28
|
+
mkdirSync,
|
|
29
|
+
existsSync,
|
|
30
|
+
readdirSync,
|
|
31
|
+
renameSync,
|
|
27
32
|
} from "fs";
|
|
28
33
|
import { join, dirname, basename, resolve } from "path";
|
|
29
34
|
import { StringDecoder } from "string_decoder";
|
|
@@ -121,13 +126,19 @@ import { DEFAULT_WORKER_USER_TOOLS } from "./tool-allowlist-constants.ts";
|
|
|
121
126
|
*/
|
|
122
127
|
export function buildWorkerToolsAllowlist(userTools: string | undefined | null): string {
|
|
123
128
|
const userPart = (userTools && userTools.trim()) || DEFAULT_WORKER_USER_TOOLS;
|
|
124
|
-
const rawUserList = userPart
|
|
129
|
+
const rawUserList = userPart
|
|
130
|
+
.split(",")
|
|
131
|
+
.map((s) => s.trim())
|
|
132
|
+
.filter(Boolean);
|
|
125
133
|
// Guard against delimiter-only / whitespace-only inputs (e.g. ",", " , ")
|
|
126
134
|
// that would otherwise parse to an empty list and yield bridge-tools-only
|
|
127
135
|
// workers with no file/shell capabilities.
|
|
128
|
-
const userList =
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
const userList =
|
|
137
|
+
rawUserList.length > 0
|
|
138
|
+
? rawUserList
|
|
139
|
+
: DEFAULT_WORKER_USER_TOOLS.split(",")
|
|
140
|
+
.map((s) => s.trim())
|
|
141
|
+
.filter(Boolean);
|
|
131
142
|
const merged = new Set<string>(userList);
|
|
132
143
|
for (const t of ENGINE_BRIDGE_TOOLS) merged.add(t);
|
|
133
144
|
return Array.from(merged).join(",");
|
|
@@ -155,9 +166,13 @@ function extractAssistantText(message: Record<string, unknown>): string {
|
|
|
155
166
|
// Guard: skip null/non-object entries to prevent TypeError on malformed streams
|
|
156
167
|
if (Array.isArray(message.content)) {
|
|
157
168
|
const textBlocks = message.content
|
|
158
|
-
.filter(
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
.filter(
|
|
170
|
+
(b: unknown): b is { type: string; text: string } =>
|
|
171
|
+
typeof b === "object" &&
|
|
172
|
+
b !== null &&
|
|
173
|
+
(b as any).type === "text" &&
|
|
174
|
+
typeof (b as any).text === "string",
|
|
175
|
+
)
|
|
161
176
|
.map((b) => b.text);
|
|
162
177
|
if (textBlocks.length > 0) return textBlocks.join("\n");
|
|
163
178
|
}
|
|
@@ -304,8 +319,10 @@ function isValidMailboxMessage(obj: any): boolean {
|
|
|
304
319
|
typeof obj.batchId === "string" &&
|
|
305
320
|
typeof obj.from === "string" &&
|
|
306
321
|
typeof obj.to === "string" &&
|
|
307
|
-
typeof obj.timestamp === "number" &&
|
|
308
|
-
|
|
322
|
+
typeof obj.timestamp === "number" &&
|
|
323
|
+
Number.isFinite(obj.timestamp) &&
|
|
324
|
+
typeof obj.type === "string" &&
|
|
325
|
+
MAILBOX_MESSAGE_TYPES.has(obj.type) &&
|
|
309
326
|
typeof obj.content === "string"
|
|
310
327
|
);
|
|
311
328
|
}
|
|
@@ -330,7 +347,6 @@ export function spawnAgent(
|
|
|
330
347
|
onEvent?: AgentEventCallback,
|
|
331
348
|
onTelemetry?: AgentTelemetryCallback,
|
|
332
349
|
): { promise: Promise<AgentHostResult>; kill: () => void } {
|
|
333
|
-
|
|
334
350
|
const cliPath = resolvePiCliPath();
|
|
335
351
|
const closeDelayMs = opts.closeDelayMs ?? 100;
|
|
336
352
|
const timeoutMs = opts.timeoutMs ?? 0;
|
|
@@ -369,9 +385,16 @@ export function spawnAgent(
|
|
|
369
385
|
let stdinClosed = false;
|
|
370
386
|
let assistantMessageEnds = 0;
|
|
371
387
|
const STATS_REFRESH_EVERY_ASSISTANT_MESSAGES = 5;
|
|
372
|
-
let inputTokens = 0,
|
|
373
|
-
|
|
374
|
-
|
|
388
|
+
let inputTokens = 0,
|
|
389
|
+
outputTokens = 0,
|
|
390
|
+
cacheReadTokens = 0,
|
|
391
|
+
cacheWriteTokens = 0;
|
|
392
|
+
let costUsd = 0,
|
|
393
|
+
toolCalls = 0,
|
|
394
|
+
retries = 0,
|
|
395
|
+
compactions = 0;
|
|
396
|
+
let lastTool = "",
|
|
397
|
+
error: string | null = null;
|
|
375
398
|
let contextUsage: AgentHostResult["contextUsage"] = null;
|
|
376
399
|
let stderrBuffer = "";
|
|
377
400
|
const STDERR_MAX = 2048;
|
|
@@ -388,7 +411,11 @@ export function spawnAgent(
|
|
|
388
411
|
timeoutHandle = setTimeout(() => {
|
|
389
412
|
timedOut = true;
|
|
390
413
|
killed = true;
|
|
391
|
-
try {
|
|
414
|
+
try {
|
|
415
|
+
proc.kill("SIGTERM");
|
|
416
|
+
} catch {
|
|
417
|
+
/* ignore */
|
|
418
|
+
}
|
|
392
419
|
}, timeoutMs);
|
|
393
420
|
}
|
|
394
421
|
|
|
@@ -397,12 +424,14 @@ export function spawnAgent(
|
|
|
397
424
|
const refreshRegistrySnapshot = (force: boolean = false) => {
|
|
398
425
|
if (!opts.stateRoot) return;
|
|
399
426
|
const now = Date.now();
|
|
400
|
-
if (!force &&
|
|
427
|
+
if (!force && now - lastRegistryRefreshAt < REGISTRY_REFRESH_INTERVAL_MS) return;
|
|
401
428
|
try {
|
|
402
429
|
const snapshot = buildRegistrySnapshot(opts.stateRoot, opts.batchId);
|
|
403
430
|
writeRegistrySnapshot(opts.stateRoot, snapshot);
|
|
404
431
|
lastRegistryRefreshAt = now;
|
|
405
|
-
} catch {
|
|
432
|
+
} catch {
|
|
433
|
+
/* best effort */
|
|
434
|
+
}
|
|
406
435
|
};
|
|
407
436
|
|
|
408
437
|
// Registry integration: write manifest before process is considered visible
|
|
@@ -430,10 +459,18 @@ export function spawnAgent(
|
|
|
430
459
|
stdinClosed = true;
|
|
431
460
|
if (closeDelayMs > 0) {
|
|
432
461
|
setTimeout(() => {
|
|
433
|
-
try {
|
|
462
|
+
try {
|
|
463
|
+
proc.stdin?.end();
|
|
464
|
+
} catch {
|
|
465
|
+
/* ignore */
|
|
466
|
+
}
|
|
434
467
|
}, closeDelayMs);
|
|
435
468
|
} else {
|
|
436
|
-
try {
|
|
469
|
+
try {
|
|
470
|
+
proc.stdin?.end();
|
|
471
|
+
} catch {
|
|
472
|
+
/* ignore */
|
|
473
|
+
}
|
|
437
474
|
}
|
|
438
475
|
}
|
|
439
476
|
|
|
@@ -456,7 +493,9 @@ export function spawnAgent(
|
|
|
456
493
|
try {
|
|
457
494
|
mkdirSync(dirname(opts.eventsPath), { recursive: true });
|
|
458
495
|
appendFileSync(opts.eventsPath, JSON.stringify(event) + "\n", "utf-8");
|
|
459
|
-
} catch {
|
|
496
|
+
} catch {
|
|
497
|
+
/* best effort */
|
|
498
|
+
}
|
|
460
499
|
}
|
|
461
500
|
}
|
|
462
501
|
|
|
@@ -481,9 +520,15 @@ export function spawnAgent(
|
|
|
481
520
|
if (!existsSync(inboxDir)) continue;
|
|
482
521
|
|
|
483
522
|
let entries: string[];
|
|
484
|
-
try {
|
|
523
|
+
try {
|
|
524
|
+
entries = readdirSync(inboxDir);
|
|
525
|
+
} catch {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
485
528
|
|
|
486
|
-
const msgFiles = entries
|
|
529
|
+
const msgFiles = entries
|
|
530
|
+
.filter((f) => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp"))
|
|
531
|
+
.sort();
|
|
487
532
|
if (msgFiles.length === 0) continue;
|
|
488
533
|
|
|
489
534
|
const ackDir = join(opts.mailboxDir, "ack");
|
|
@@ -509,12 +554,24 @@ export function spawnAgent(
|
|
|
509
554
|
if (isBroadcast) {
|
|
510
555
|
// Do NOT remove the shared broadcast inbox file. Persist a per-agent
|
|
511
556
|
// ack marker so all agents can consume the same broadcast exactly once.
|
|
512
|
-
try {
|
|
557
|
+
try {
|
|
558
|
+
writeFileSync(ackPath, raw, "utf-8");
|
|
559
|
+
} catch {
|
|
560
|
+
/* best effort */
|
|
561
|
+
}
|
|
513
562
|
} else {
|
|
514
|
-
try {
|
|
563
|
+
try {
|
|
564
|
+
renameSync(join(inboxDir, filename), ackPath);
|
|
565
|
+
} catch {
|
|
566
|
+
/* race ok */
|
|
567
|
+
}
|
|
515
568
|
}
|
|
516
569
|
|
|
517
|
-
emitEvent("message_delivered", {
|
|
570
|
+
emitEvent("message_delivered", {
|
|
571
|
+
messageId: msg.id,
|
|
572
|
+
content: msg.content,
|
|
573
|
+
broadcast: isBroadcast,
|
|
574
|
+
});
|
|
518
575
|
if (opts.stateRoot) {
|
|
519
576
|
appendMailboxAuditEvent(opts.stateRoot, expectedBatchId, {
|
|
520
577
|
type: "message_delivered",
|
|
@@ -530,11 +587,18 @@ export function spawnAgent(
|
|
|
530
587
|
// TP-090: steering-pending flag
|
|
531
588
|
if (opts.steeringPendingPath) {
|
|
532
589
|
try {
|
|
533
|
-
appendFileSync(
|
|
534
|
-
|
|
535
|
-
|
|
590
|
+
appendFileSync(
|
|
591
|
+
opts.steeringPendingPath,
|
|
592
|
+
JSON.stringify({ ts: msg.timestamp, content: msg.content, id: msg.id }) + "\n",
|
|
593
|
+
"utf-8",
|
|
594
|
+
);
|
|
595
|
+
} catch {
|
|
596
|
+
/* best effort */
|
|
597
|
+
}
|
|
536
598
|
}
|
|
537
|
-
} catch {
|
|
599
|
+
} catch {
|
|
600
|
+
/* skip malformed */
|
|
601
|
+
}
|
|
538
602
|
}
|
|
539
603
|
}
|
|
540
604
|
}
|
|
@@ -576,9 +640,15 @@ export function spawnAgent(
|
|
|
576
640
|
const summary = {
|
|
577
641
|
exitCode: result.exitCode,
|
|
578
642
|
exitSignal: result.signal,
|
|
579
|
-
tokens:
|
|
580
|
-
|
|
581
|
-
|
|
643
|
+
tokens:
|
|
644
|
+
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens > 0
|
|
645
|
+
? {
|
|
646
|
+
input: inputTokens,
|
|
647
|
+
output: outputTokens,
|
|
648
|
+
cacheRead: cacheReadTokens,
|
|
649
|
+
cacheWrite: cacheWriteTokens,
|
|
650
|
+
}
|
|
651
|
+
: null,
|
|
582
652
|
cost: costUsd > 0 ? costUsd : null,
|
|
583
653
|
toolCalls,
|
|
584
654
|
retries,
|
|
@@ -589,23 +659,29 @@ export function spawnAgent(
|
|
|
589
659
|
contextUsage: contextUsage || null,
|
|
590
660
|
};
|
|
591
661
|
writeFileSync(opts.exitSummaryPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
|
|
592
|
-
} catch {
|
|
662
|
+
} catch {
|
|
663
|
+
/* best effort */
|
|
664
|
+
}
|
|
593
665
|
}
|
|
594
666
|
|
|
595
|
-
const exitEventType: RuntimeAgentEventType =
|
|
596
|
-
|
|
597
|
-
killed
|
|
598
|
-
|
|
599
|
-
|
|
667
|
+
const exitEventType: RuntimeAgentEventType = timedOut
|
|
668
|
+
? "agent_timeout"
|
|
669
|
+
: killed
|
|
670
|
+
? "agent_killed"
|
|
671
|
+
: exitCode === 0 && agentEnded
|
|
672
|
+
? "agent_exited"
|
|
673
|
+
: "agent_crashed";
|
|
600
674
|
emitEvent(exitEventType, { exitCode, signal, durationMs: result.durationMs, timedOut });
|
|
601
675
|
|
|
602
676
|
// Registry integration: update manifest to terminal status
|
|
603
677
|
if (opts.stateRoot) {
|
|
604
|
-
const terminalStatus =
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
678
|
+
const terminalStatus = timedOut
|
|
679
|
+
? ("timed_out" as const)
|
|
680
|
+
: killed
|
|
681
|
+
? ("killed" as const)
|
|
682
|
+
: exitCode === 0 && agentEnded
|
|
683
|
+
? ("exited" as const)
|
|
684
|
+
: ("crashed" as const);
|
|
609
685
|
updateManifestStatus(opts.stateRoot, opts.batchId, opts.agentId, terminalStatus);
|
|
610
686
|
refreshRegistrySnapshot(true);
|
|
611
687
|
}
|
|
@@ -623,7 +699,11 @@ export function spawnAgent(
|
|
|
623
699
|
if (!line.trim()) continue;
|
|
624
700
|
|
|
625
701
|
let event: any;
|
|
626
|
-
try {
|
|
702
|
+
try {
|
|
703
|
+
event = JSON.parse(line);
|
|
704
|
+
} catch {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
627
707
|
if (!event || !event.type) continue;
|
|
628
708
|
|
|
629
709
|
// Accumulate telemetry
|
|
@@ -636,7 +716,12 @@ export function spawnAgent(
|
|
|
636
716
|
cacheReadTokens += usage.cacheRead || 0;
|
|
637
717
|
cacheWriteTokens += usage.cacheWrite || 0;
|
|
638
718
|
if (usage.cost) {
|
|
639
|
-
costUsd +=
|
|
719
|
+
costUsd +=
|
|
720
|
+
typeof usage.cost === "object"
|
|
721
|
+
? usage.cost.total || 0
|
|
722
|
+
: typeof usage.cost === "number"
|
|
723
|
+
? usage.cost
|
|
724
|
+
: 0;
|
|
640
725
|
}
|
|
641
726
|
}
|
|
642
727
|
// TP-111: Emit assistant_message with bounded content
|
|
@@ -652,8 +737,15 @@ export function spawnAgent(
|
|
|
652
737
|
// then periodically at a bounded cadence to refresh context usage.
|
|
653
738
|
if (event.message?.role === "assistant") {
|
|
654
739
|
assistantMessageEnds += 1;
|
|
655
|
-
if (
|
|
656
|
-
|
|
740
|
+
if (
|
|
741
|
+
assistantMessageEnds === 1 ||
|
|
742
|
+
assistantMessageEnds % STATS_REFRESH_EVERY_ASSISTANT_MESSAGES === 0
|
|
743
|
+
) {
|
|
744
|
+
try {
|
|
745
|
+
proc.stdin?.write(JSON.stringify({ type: "get_session_stats" }) + "\n");
|
|
746
|
+
} catch {
|
|
747
|
+
/* ignore */
|
|
748
|
+
}
|
|
657
749
|
}
|
|
658
750
|
}
|
|
659
751
|
// Check mailbox
|
|
@@ -662,7 +754,16 @@ export function spawnAgent(
|
|
|
662
754
|
refreshRegistrySnapshot(false);
|
|
663
755
|
// Emit telemetry update
|
|
664
756
|
if (onTelemetry) {
|
|
665
|
-
onTelemetry({
|
|
757
|
+
onTelemetry({
|
|
758
|
+
inputTokens,
|
|
759
|
+
outputTokens,
|
|
760
|
+
cacheReadTokens,
|
|
761
|
+
cacheWriteTokens,
|
|
762
|
+
costUsd,
|
|
763
|
+
toolCalls,
|
|
764
|
+
lastTool,
|
|
765
|
+
contextUsage,
|
|
766
|
+
});
|
|
666
767
|
}
|
|
667
768
|
break;
|
|
668
769
|
}
|
|
@@ -670,8 +771,12 @@ export function spawnAgent(
|
|
|
670
771
|
toolCalls++;
|
|
671
772
|
currentTurnHadToolCalls = true;
|
|
672
773
|
const toolName = event.toolName || "tool";
|
|
673
|
-
const argPreview =
|
|
674
|
-
|
|
774
|
+
const argPreview =
|
|
775
|
+
typeof event.args === "string"
|
|
776
|
+
? event.args.slice(0, 300)
|
|
777
|
+
: event.args && typeof Object.values(event.args)[0] === "string"
|
|
778
|
+
? String(Object.values(event.args)[0]).slice(0, 300)
|
|
779
|
+
: "";
|
|
675
780
|
lastTool = argPreview ? `${toolName}: ${argPreview}` : toolName;
|
|
676
781
|
// TP-111: Bounded payload only — no raw args in durable event log
|
|
677
782
|
const toolPath = event.args?.path ? String(event.args.path).slice(0, 200) : "";
|
|
@@ -680,14 +785,21 @@ export function spawnAgent(
|
|
|
680
785
|
}
|
|
681
786
|
case "tool_execution_end": {
|
|
682
787
|
// TP-111: Include bounded result summary for dashboard display
|
|
683
|
-
const toolResultSummary =
|
|
684
|
-
|
|
788
|
+
const toolResultSummary =
|
|
789
|
+
typeof event.result === "string"
|
|
790
|
+
? event.result.slice(0, 200)
|
|
791
|
+
: event.output
|
|
792
|
+
? String(event.output).slice(0, 200)
|
|
793
|
+
: "";
|
|
685
794
|
emitEvent("tool_result", { tool: event.toolName, summary: toolResultSummary });
|
|
686
795
|
break;
|
|
687
796
|
}
|
|
688
797
|
case "auto_retry_start": {
|
|
689
798
|
retries++;
|
|
690
|
-
emitEvent("retry_started", {
|
|
799
|
+
emitEvent("retry_started", {
|
|
800
|
+
attempt: event.attempt,
|
|
801
|
+
error: event.errorMessage || event.error,
|
|
802
|
+
});
|
|
691
803
|
break;
|
|
692
804
|
}
|
|
693
805
|
case "auto_compaction_start": {
|
|
@@ -704,7 +816,16 @@ export function spawnAgent(
|
|
|
704
816
|
emitEvent("context_usage", { ...event.data.contextUsage });
|
|
705
817
|
// Emit telemetry immediately so context % is live in dashboard
|
|
706
818
|
if (onTelemetry) {
|
|
707
|
-
onTelemetry({
|
|
819
|
+
onTelemetry({
|
|
820
|
+
inputTokens,
|
|
821
|
+
outputTokens,
|
|
822
|
+
cacheReadTokens,
|
|
823
|
+
cacheWriteTokens,
|
|
824
|
+
costUsd,
|
|
825
|
+
toolCalls,
|
|
826
|
+
lastTool,
|
|
827
|
+
contextUsage,
|
|
828
|
+
});
|
|
708
829
|
}
|
|
709
830
|
}
|
|
710
831
|
break;
|
|
@@ -717,60 +838,62 @@ export function spawnAgent(
|
|
|
717
838
|
// because workers commonly use tools (reads/greps) then exit
|
|
718
839
|
// with a text declaration ("Now let me fix this:") without
|
|
719
840
|
// actually making the edit.
|
|
720
|
-
const shouldIntercept = opts.onPrematureExit
|
|
721
|
-
&& exitInterceptionCount < maxExitInterceptions;
|
|
841
|
+
const shouldIntercept = opts.onPrematureExit && exitInterceptionCount < maxExitInterceptions;
|
|
722
842
|
if (shouldIntercept) {
|
|
723
843
|
exitInterceptionCount++;
|
|
724
844
|
const INTERCEPTION_TIMEOUT_MS = 120_000; // 2 minute safety timeout
|
|
725
845
|
// Wrap in Promise.resolve().then() to catch synchronous throws
|
|
726
846
|
const interceptPromise = Promise.resolve().then(() =>
|
|
727
|
-
opts.onPrematureExit!(lastAssistantMessage)
|
|
847
|
+
opts.onPrematureExit!(lastAssistantMessage),
|
|
848
|
+
);
|
|
728
849
|
const timeoutPromise = new Promise<null>((res) =>
|
|
729
|
-
setTimeout(() => res(null), INTERCEPTION_TIMEOUT_MS)
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
850
|
+
setTimeout(() => res(null), INTERCEPTION_TIMEOUT_MS),
|
|
851
|
+
);
|
|
852
|
+
Promise.race([interceptPromise, timeoutPromise]).then(
|
|
853
|
+
(newPrompt: string | null) => {
|
|
854
|
+
if (newPrompt && !stdinClosed && proc.stdin && !proc.stdin.destroyed) {
|
|
855
|
+
// Re-prompt the agent with supervisor guidance
|
|
856
|
+
agentEnded = false; // Reset for the new turn
|
|
857
|
+
currentTurnHadToolCalls = false; // Reset for new turn
|
|
858
|
+
proc.stdin.write(JSON.stringify({ type: "prompt", message: newPrompt }) + "\n");
|
|
859
|
+
emitEvent("exit_intercepted", {
|
|
860
|
+
interceptionCount: exitInterceptionCount,
|
|
861
|
+
assistantMessage: truncatePayload(lastAssistantMessage, 500),
|
|
862
|
+
supervisorConsulted: true,
|
|
863
|
+
action: "reprompt",
|
|
864
|
+
newPromptPreview: truncatePayload(newPrompt, MAX_CONV_PAYLOAD_CHARS),
|
|
865
|
+
});
|
|
866
|
+
} else {
|
|
867
|
+
// Callback returned null or stdin already closed — close session
|
|
868
|
+
const reason = stdinClosed
|
|
869
|
+
? "stdin_closed"
|
|
870
|
+
: newPrompt === null
|
|
871
|
+
? "callback_returned_null"
|
|
749
872
|
: "unknown";
|
|
750
|
-
emitEvent("exit_intercepted", {
|
|
751
|
-
interceptionCount: exitInterceptionCount,
|
|
752
|
-
assistantMessage: truncatePayload(lastAssistantMessage, 500),
|
|
753
|
-
supervisorConsulted: true,
|
|
754
|
-
action: "close",
|
|
755
|
-
reason,
|
|
756
|
-
});
|
|
757
|
-
closeStdin();
|
|
758
|
-
}
|
|
759
|
-
},
|
|
760
|
-
(err: unknown) => {
|
|
761
|
-
// Callback rejected — emit single diagnostic event and close
|
|
762
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
763
873
|
emitEvent("exit_intercepted", {
|
|
764
874
|
interceptionCount: exitInterceptionCount,
|
|
765
875
|
assistantMessage: truncatePayload(lastAssistantMessage, 500),
|
|
766
|
-
supervisorConsulted:
|
|
876
|
+
supervisorConsulted: true,
|
|
767
877
|
action: "close",
|
|
768
|
-
reason
|
|
769
|
-
error: msg,
|
|
878
|
+
reason,
|
|
770
879
|
});
|
|
771
880
|
closeStdin();
|
|
772
|
-
}
|
|
773
|
-
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
(err: unknown) => {
|
|
884
|
+
// Callback rejected — emit single diagnostic event and close
|
|
885
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
886
|
+
emitEvent("exit_intercepted", {
|
|
887
|
+
interceptionCount: exitInterceptionCount,
|
|
888
|
+
assistantMessage: truncatePayload(lastAssistantMessage, 500),
|
|
889
|
+
supervisorConsulted: false,
|
|
890
|
+
action: "close",
|
|
891
|
+
reason: "callback_error",
|
|
892
|
+
error: msg,
|
|
893
|
+
});
|
|
894
|
+
closeStdin();
|
|
895
|
+
},
|
|
896
|
+
);
|
|
774
897
|
} else {
|
|
775
898
|
// No callback, had tool calls, or interception limit reached — close normally
|
|
776
899
|
if (opts.onPrematureExit && exitInterceptionCount >= maxExitInterceptions) {
|
|
@@ -820,7 +943,11 @@ export function spawnAgent(
|
|
|
820
943
|
|
|
821
944
|
const kill = () => {
|
|
822
945
|
killed = true;
|
|
823
|
-
try {
|
|
946
|
+
try {
|
|
947
|
+
proc.kill("SIGTERM");
|
|
948
|
+
} catch {
|
|
949
|
+
/* ignore */
|
|
950
|
+
}
|
|
824
951
|
};
|
|
825
952
|
|
|
826
953
|
return { promise, kill };
|