taskplane 0.29.2 → 0.30.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/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- package/dashboard/public/app.js +124 -15
- package/dashboard/public/style.css +83 -2
- 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 +78 -63
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +469 -207
- 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 +652 -319
- 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 +832 -280
- 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 +209 -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
package/bin/rpc-wrapper.mjs
CHANGED
|
@@ -28,7 +28,15 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { spawn } from "node:child_process";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
readFileSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
appendFileSync,
|
|
35
|
+
mkdirSync,
|
|
36
|
+
readdirSync,
|
|
37
|
+
renameSync,
|
|
38
|
+
unlinkSync,
|
|
39
|
+
} from "node:fs";
|
|
32
40
|
import { dirname, resolve, join, basename } from "node:path";
|
|
33
41
|
import { StringDecoder } from "node:string_decoder";
|
|
34
42
|
|
|
@@ -71,10 +79,16 @@ function parseArgs(argv) {
|
|
|
71
79
|
args.promptFile = argv[++i];
|
|
72
80
|
i++;
|
|
73
81
|
} else if (arg === "--tools" && i + 1 < argv.length) {
|
|
74
|
-
args.tools = argv[++i]
|
|
82
|
+
args.tools = argv[++i]
|
|
83
|
+
.split(",")
|
|
84
|
+
.map((t) => t.trim())
|
|
85
|
+
.filter(Boolean);
|
|
75
86
|
i++;
|
|
76
87
|
} else if (arg === "--extensions" && i + 1 < argv.length) {
|
|
77
|
-
args.extensions = argv[++i]
|
|
88
|
+
args.extensions = argv[++i]
|
|
89
|
+
.split(",")
|
|
90
|
+
.map((e) => e.trim())
|
|
91
|
+
.filter(Boolean);
|
|
78
92
|
i++;
|
|
79
93
|
} else if (arg === "--mailbox-dir" && i + 1 < argv.length) {
|
|
80
94
|
args.mailboxDir = argv[++i];
|
|
@@ -114,7 +128,7 @@ Optional:
|
|
|
114
128
|
--mailbox-dir <path> Mailbox directory for agent steering (TP-089)
|
|
115
129
|
--steering-pending-path <p> Path to .steering-pending JSONL flag file (TP-090)
|
|
116
130
|
-h, --help Show this help
|
|
117
|
-
|
|
131
|
+
`,
|
|
118
132
|
);
|
|
119
133
|
}
|
|
120
134
|
|
|
@@ -177,9 +191,9 @@ function redactValue(val) {
|
|
|
177
191
|
if (val === null || val === undefined) return val;
|
|
178
192
|
|
|
179
193
|
if (typeof val === "string") {
|
|
180
|
-
return redactString(
|
|
181
|
-
? val.slice(0, MAX_TOOL_ARG_LENGTH) + "…[truncated]"
|
|
182
|
-
|
|
194
|
+
return redactString(
|
|
195
|
+
val.length > MAX_TOOL_ARG_LENGTH ? val.slice(0, MAX_TOOL_ARG_LENGTH) + "…[truncated]" : val,
|
|
196
|
+
);
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
if (Array.isArray(val)) {
|
|
@@ -237,7 +251,7 @@ function redactSummary(summary) {
|
|
|
237
251
|
redacted.lastToolCall = redactString(
|
|
238
252
|
redacted.lastToolCall.length > MAX_TOOL_ARG_LENGTH
|
|
239
253
|
? redacted.lastToolCall.slice(0, MAX_TOOL_ARG_LENGTH) + "…[truncated]"
|
|
240
|
-
: redacted.lastToolCall
|
|
254
|
+
: redacted.lastToolCall,
|
|
241
255
|
);
|
|
242
256
|
}
|
|
243
257
|
|
|
@@ -276,7 +290,8 @@ function writeSidecarEvent(sidecarPath, event) {
|
|
|
276
290
|
function displayProgress(state) {
|
|
277
291
|
const parts = [];
|
|
278
292
|
if (state.currentTool) parts.push(`tool: ${state.currentTool}`);
|
|
279
|
-
const totalTokens =
|
|
293
|
+
const totalTokens =
|
|
294
|
+
state.tokens.input + state.tokens.output + state.tokens.cacheRead + state.tokens.cacheWrite;
|
|
280
295
|
if (totalTokens > 0) parts.push(`tokens: ${totalTokens.toLocaleString()}`);
|
|
281
296
|
if (state.cost > 0) parts.push(`cost: $${state.cost.toFixed(4)}`);
|
|
282
297
|
if (state.toolCalls > 0) parts.push(`tools: ${state.toolCalls}`);
|
|
@@ -363,7 +378,12 @@ function applyEvent(state, event) {
|
|
|
363
378
|
state.tokens.cacheRead += usage.cacheRead || 0;
|
|
364
379
|
state.tokens.cacheWrite += usage.cacheWrite || 0;
|
|
365
380
|
if (usage.cost) {
|
|
366
|
-
state.cost +=
|
|
381
|
+
state.cost +=
|
|
382
|
+
typeof usage.cost === "object"
|
|
383
|
+
? usage.cost.total || 0
|
|
384
|
+
: typeof usage.cost === "number"
|
|
385
|
+
? usage.cost
|
|
386
|
+
: 0;
|
|
367
387
|
}
|
|
368
388
|
}
|
|
369
389
|
break;
|
|
@@ -453,16 +473,20 @@ function applyEvent(state, event) {
|
|
|
453
473
|
function buildExitSummary(state, exitCode, exitSignal, errorOverride, startTime) {
|
|
454
474
|
const durationSec = Math.round((Date.now() - startTime) / 1000);
|
|
455
475
|
const finalError = errorOverride || state.error || null;
|
|
456
|
-
const normalizedExitCode =
|
|
457
|
-
|
|
458
|
-
|
|
476
|
+
const normalizedExitCode =
|
|
477
|
+
typeof exitCode === "number" && Number.isFinite(exitCode) && exitCode >= 0
|
|
478
|
+
? exitCode
|
|
479
|
+
: exitCode === null || exitCode === undefined
|
|
480
|
+
? null
|
|
481
|
+
: 1;
|
|
459
482
|
|
|
460
483
|
const rawSummary = {
|
|
461
484
|
exitCode: normalizedExitCode,
|
|
462
485
|
exitSignal: exitSignal || null,
|
|
463
|
-
tokens:
|
|
464
|
-
|
|
465
|
-
|
|
486
|
+
tokens:
|
|
487
|
+
state.tokens.input + state.tokens.output + state.tokens.cacheRead + state.tokens.cacheWrite > 0
|
|
488
|
+
? { ...state.tokens }
|
|
489
|
+
: null,
|
|
466
490
|
cost: state.cost > 0 ? state.cost : null,
|
|
467
491
|
toolCalls: state.toolCalls,
|
|
468
492
|
retries: state.retries,
|
|
@@ -537,7 +561,7 @@ function checkMailboxAndSteer(mailboxDir, proc, steeringPendingPath) {
|
|
|
537
561
|
}
|
|
538
562
|
|
|
539
563
|
// Filter: only *.msg.json files (excludes .msg.json.tmp temp files)
|
|
540
|
-
const msgFiles = entries.filter(f => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp"));
|
|
564
|
+
const msgFiles = entries.filter((f) => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp"));
|
|
541
565
|
if (msgFiles.length === 0) return stats;
|
|
542
566
|
|
|
543
567
|
// Read and validate all messages
|
|
@@ -572,14 +596,18 @@ function checkMailboxAndSteer(mailboxDir, proc, steeringPendingPath) {
|
|
|
572
596
|
|
|
573
597
|
// Validate batchId (derived from path, not message content)
|
|
574
598
|
if (msg.batchId !== expectedBatchId) {
|
|
575
|
-
process.stderr.write(
|
|
599
|
+
process.stderr.write(
|
|
600
|
+
`\n[STEERING] WARNING: batchId mismatch in ${filename} (expected ${expectedBatchId}, got ${msg.batchId}), skipping\n`,
|
|
601
|
+
);
|
|
576
602
|
stats.skipped++;
|
|
577
603
|
continue;
|
|
578
604
|
}
|
|
579
605
|
|
|
580
606
|
// Validate to (no misdelivery)
|
|
581
607
|
if (msg.to !== expectedSessionName) {
|
|
582
|
-
process.stderr.write(
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
`\n[STEERING] WARNING: misdelivery in ${filename} (to=${msg.to}, expected ${expectedSessionName}), skipping\n`,
|
|
610
|
+
);
|
|
583
611
|
stats.skipped++;
|
|
584
612
|
continue;
|
|
585
613
|
}
|
|
@@ -609,7 +637,11 @@ function checkMailboxAndSteer(mailboxDir, proc, steeringPendingPath) {
|
|
|
609
637
|
|
|
610
638
|
// Move to ack/ (delivery proof)
|
|
611
639
|
const ackDir = join(mailboxDir, "ack");
|
|
612
|
-
try {
|
|
640
|
+
try {
|
|
641
|
+
mkdirSync(ackDir, { recursive: true });
|
|
642
|
+
} catch {
|
|
643
|
+
/* exists */
|
|
644
|
+
}
|
|
613
645
|
try {
|
|
614
646
|
renameSync(join(inboxDir, filename), join(ackDir, filename));
|
|
615
647
|
} catch (err) {
|
|
@@ -626,10 +658,13 @@ function checkMailboxAndSteer(mailboxDir, proc, steeringPendingPath) {
|
|
|
626
658
|
// Worker-only: steeringPendingPath is only set for worker sessions.
|
|
627
659
|
if (steeringPendingPath) {
|
|
628
660
|
try {
|
|
629
|
-
const entry =
|
|
661
|
+
const entry =
|
|
662
|
+
JSON.stringify({ ts: message.timestamp, content: message.content, id: message.id }) + "\n";
|
|
630
663
|
appendFileSync(steeringPendingPath, entry, "utf-8");
|
|
631
664
|
} catch (err) {
|
|
632
|
-
process.stderr.write(
|
|
665
|
+
process.stderr.write(
|
|
666
|
+
`\n[STEERING] WARNING: failed to write .steering-pending: ${err.message}\n`,
|
|
667
|
+
);
|
|
633
668
|
}
|
|
634
669
|
}
|
|
635
670
|
} catch (err) {
|
|
@@ -656,8 +691,10 @@ function isValidMailboxMessageShape(obj) {
|
|
|
656
691
|
typeof obj.batchId === "string" &&
|
|
657
692
|
typeof obj.from === "string" &&
|
|
658
693
|
typeof obj.to === "string" &&
|
|
659
|
-
typeof obj.timestamp === "number" &&
|
|
660
|
-
|
|
694
|
+
typeof obj.timestamp === "number" &&
|
|
695
|
+
Number.isFinite(obj.timestamp) &&
|
|
696
|
+
typeof obj.type === "string" &&
|
|
697
|
+
MAILBOX_MESSAGE_TYPES.has(obj.type) &&
|
|
661
698
|
typeof obj.content === "string"
|
|
662
699
|
);
|
|
663
700
|
}
|
|
@@ -689,398 +726,414 @@ export {
|
|
|
689
726
|
// import.meta.url ends with the script name; process.argv[1] is the entry point.
|
|
690
727
|
// On Windows with shell:true, argv[1] may differ, so also check for --help being
|
|
691
728
|
// processed as a signal that we're the entry point.
|
|
692
|
-
const _isMain =
|
|
729
|
+
const _isMain =
|
|
730
|
+
process.argv[1] &&
|
|
693
731
|
(import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/")) ||
|
|
694
|
-
|
|
695
|
-
|
|
732
|
+
import.meta.url.endsWith("/" + process.argv[1].replace(/\\/g, "/").split("/").pop()) ||
|
|
733
|
+
process.argv[1].endsWith("rpc-wrapper.mjs"));
|
|
696
734
|
|
|
697
735
|
if (_isMain) {
|
|
698
736
|
_main();
|
|
699
737
|
}
|
|
700
738
|
|
|
701
739
|
function _main() {
|
|
740
|
+
const args = parseArgs(process.argv);
|
|
702
741
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
process.exit(0);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Validate required args
|
|
711
|
-
if (!args.sidecarPath) {
|
|
712
|
-
process.stderr.write("[rpc-wrapper] ERROR: --sidecar-path is required\n");
|
|
713
|
-
process.exit(1);
|
|
714
|
-
}
|
|
715
|
-
if (!args.exitSummaryPath) {
|
|
716
|
-
process.stderr.write("[rpc-wrapper] ERROR: --exit-summary-path is required\n");
|
|
717
|
-
process.exit(1);
|
|
718
|
-
}
|
|
719
|
-
if (!args.promptFile) {
|
|
720
|
-
process.stderr.write("[rpc-wrapper] ERROR: --prompt-file is required\n");
|
|
721
|
-
process.exit(1);
|
|
722
|
-
}
|
|
742
|
+
if (args.help) {
|
|
743
|
+
printUsage();
|
|
744
|
+
process.exit(0);
|
|
745
|
+
}
|
|
723
746
|
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
747
|
+
// Validate required args
|
|
748
|
+
if (!args.sidecarPath) {
|
|
749
|
+
process.stderr.write("[rpc-wrapper] ERROR: --sidecar-path is required\n");
|
|
750
|
+
process.exit(1);
|
|
751
|
+
}
|
|
752
|
+
if (!args.exitSummaryPath) {
|
|
753
|
+
process.stderr.write("[rpc-wrapper] ERROR: --exit-summary-path is required\n");
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
if (!args.promptFile) {
|
|
757
|
+
process.stderr.write("[rpc-wrapper] ERROR: --prompt-file is required\n");
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
732
760
|
|
|
733
|
-
// Read
|
|
734
|
-
let
|
|
735
|
-
if (args.systemPromptFile) {
|
|
761
|
+
// Read prompt content
|
|
762
|
+
let promptContent;
|
|
736
763
|
try {
|
|
737
|
-
|
|
764
|
+
promptContent = readFileSync(resolve(args.promptFile), "utf-8");
|
|
738
765
|
} catch (err) {
|
|
739
|
-
process.stderr.write(`[rpc-wrapper]
|
|
766
|
+
process.stderr.write(`[rpc-wrapper] ERROR: Cannot read prompt file: ${err.message}\n`);
|
|
767
|
+
process.exit(1);
|
|
740
768
|
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Ensure output directories exist
|
|
744
|
-
mkdirSync(dirname(resolve(args.sidecarPath)), { recursive: true });
|
|
745
|
-
mkdirSync(dirname(resolve(args.exitSummaryPath)), { recursive: true });
|
|
746
769
|
|
|
747
|
-
//
|
|
770
|
+
// Read system prompt content (optional)
|
|
771
|
+
let systemPromptContent = null;
|
|
772
|
+
if (args.systemPromptFile) {
|
|
773
|
+
try {
|
|
774
|
+
systemPromptContent = readFileSync(resolve(args.systemPromptFile), "utf-8");
|
|
775
|
+
} catch (err) {
|
|
776
|
+
process.stderr.write(`[rpc-wrapper] WARNING: Cannot read system prompt file: ${err.message}\n`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
748
779
|
|
|
749
|
-
|
|
750
|
-
|
|
780
|
+
// Ensure output directories exist
|
|
781
|
+
mkdirSync(dirname(resolve(args.sidecarPath)), { recursive: true });
|
|
782
|
+
mkdirSync(dirname(resolve(args.exitSummaryPath)), { recursive: true });
|
|
751
783
|
|
|
752
|
-
// ──
|
|
784
|
+
// ── Session State ────────────────────────────────────────────────────
|
|
753
785
|
|
|
754
|
-
const
|
|
786
|
+
const startTime = Date.now();
|
|
787
|
+
const state = createSessionState();
|
|
755
788
|
|
|
756
|
-
|
|
757
|
-
piArgs.push("--model", args.model);
|
|
758
|
-
}
|
|
759
|
-
if (systemPromptContent) {
|
|
760
|
-
piArgs.push("--system-prompt", systemPromptContent);
|
|
761
|
-
}
|
|
762
|
-
if (args.tools.length > 0) {
|
|
763
|
-
piArgs.push("--tools", args.tools.join(","));
|
|
764
|
-
}
|
|
765
|
-
for (const ext of args.extensions) {
|
|
766
|
-
piArgs.push("-e", ext);
|
|
767
|
-
}
|
|
768
|
-
piArgs.push(...args.passthrough);
|
|
769
|
-
|
|
770
|
-
// ── Spawn pi process ─────────────────────────────────────────────────
|
|
771
|
-
|
|
772
|
-
// ── System prompt: file-based passthrough to avoid command line limits ────
|
|
773
|
-
// Windows CreateProcess has a ~32K command line limit. Orchestrated worker
|
|
774
|
-
// system prompts routinely exceed this (PROMPT.md + context docs + steps).
|
|
775
|
-
// When the system prompt is large, write it to a temp file and use shell
|
|
776
|
-
// expansion `$(cat file)` to pass it. This works in MSYS2/Git Bash shells
|
|
777
|
-
// used by lane sessions without hitting the Win32 limit.
|
|
778
|
-
//
|
|
779
|
-
// For small system prompts (< 8K), pass inline for simplicity.
|
|
780
|
-
const SYSTEM_PROMPT_FILE_THRESHOLD = 8192;
|
|
781
|
-
let systemPromptTempFile = null;
|
|
782
|
-
|
|
783
|
-
if (systemPromptContent && systemPromptContent.length >= SYSTEM_PROMPT_FILE_THRESHOLD) {
|
|
784
|
-
// Remove --system-prompt from piArgs (was added above) and use file instead
|
|
785
|
-
const sysIdx = piArgs.indexOf("--system-prompt");
|
|
786
|
-
if (sysIdx >= 0) piArgs.splice(sysIdx, 2);
|
|
787
|
-
// Write to temp file and use --append-system-prompt with @file syntax.
|
|
788
|
-
// Pi's --append-system-prompt accepts @filepath to read from a file.
|
|
789
|
-
// We use --system-prompt "" (empty base) + --append-system-prompt @file
|
|
790
|
-
// to effectively set the system prompt from a file.
|
|
791
|
-
systemPromptTempFile = join(tmpdir(), `pi-rpc-sysprompt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`);
|
|
792
|
-
writeFileSync(systemPromptTempFile, systemPromptContent, "utf-8");
|
|
793
|
-
piArgs.push("--system-prompt", "");
|
|
794
|
-
piArgs.push("--append-system-prompt", `@${systemPromptTempFile}`);
|
|
795
|
-
process.stderr.write(`[rpc-wrapper] system prompt written to file (${systemPromptContent.length} chars): ${systemPromptTempFile}\n`);
|
|
796
|
-
}
|
|
789
|
+
// ── Build pi spawn args ──────────────────────────────────────────────
|
|
797
790
|
|
|
798
|
-
const
|
|
799
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
800
|
-
env: { ...process.env },
|
|
801
|
-
shell: true,
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
// ── TP-097: Write PID file for orphan cleanup ──────────────────
|
|
805
|
-
// Write both the wrapper PID and the pi child PID alongside the sidecar file.
|
|
806
|
-
// The task-runner reads this on session end to kill orphan processes.
|
|
807
|
-
// Format: JSON with wrapperPid and childPid fields.
|
|
808
|
-
const pidFilePath = args.sidecarPath + ".pid";
|
|
809
|
-
try {
|
|
810
|
-
const pidData = {
|
|
811
|
-
wrapperPid: process.pid,
|
|
812
|
-
childPid: proc.pid ?? null,
|
|
813
|
-
startedAt: Date.now(),
|
|
814
|
-
};
|
|
815
|
-
writeFileSync(pidFilePath, JSON.stringify(pidData) + "\n", "utf-8");
|
|
816
|
-
process.stderr.write(`[rpc-wrapper] PID file written: ${pidFilePath} (wrapper=${process.pid}, child=${proc.pid})\n`);
|
|
817
|
-
} catch (err) {
|
|
818
|
-
process.stderr.write(`[rpc-wrapper] WARNING: failed to write PID file: ${err.message}\n`);
|
|
819
|
-
}
|
|
791
|
+
const piArgs = ["--mode", "rpc", "--no-session"];
|
|
820
792
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (
|
|
825
|
-
|
|
793
|
+
if (args.model) {
|
|
794
|
+
piArgs.push("--model", args.model);
|
|
795
|
+
}
|
|
796
|
+
if (systemPromptContent) {
|
|
797
|
+
piArgs.push("--system-prompt", systemPromptContent);
|
|
798
|
+
}
|
|
799
|
+
if (args.tools.length > 0) {
|
|
800
|
+
piArgs.push("--tools", args.tools.join(","));
|
|
801
|
+
}
|
|
802
|
+
for (const ext of args.extensions) {
|
|
803
|
+
piArgs.push("-e", ext);
|
|
804
|
+
}
|
|
805
|
+
piArgs.push(...args.passthrough);
|
|
806
|
+
|
|
807
|
+
// ── Spawn pi process ─────────────────────────────────────────────────
|
|
808
|
+
|
|
809
|
+
// ── System prompt: file-based passthrough to avoid command line limits ────
|
|
810
|
+
// Windows CreateProcess has a ~32K command line limit. Orchestrated worker
|
|
811
|
+
// system prompts routinely exceed this (PROMPT.md + context docs + steps).
|
|
812
|
+
// When the system prompt is large, write it to a temp file and use shell
|
|
813
|
+
// expansion `$(cat file)` to pass it. This works in MSYS2/Git Bash shells
|
|
814
|
+
// used by lane sessions without hitting the Win32 limit.
|
|
815
|
+
//
|
|
816
|
+
// For small system prompts (< 8K), pass inline for simplicity.
|
|
817
|
+
const SYSTEM_PROMPT_FILE_THRESHOLD = 8192;
|
|
818
|
+
let systemPromptTempFile = null;
|
|
819
|
+
|
|
820
|
+
if (systemPromptContent && systemPromptContent.length >= SYSTEM_PROMPT_FILE_THRESHOLD) {
|
|
821
|
+
// Remove --system-prompt from piArgs (was added above) and use file instead
|
|
822
|
+
const sysIdx = piArgs.indexOf("--system-prompt");
|
|
823
|
+
if (sysIdx >= 0) piArgs.splice(sysIdx, 2);
|
|
824
|
+
// Write to temp file and use --append-system-prompt with @file syntax.
|
|
825
|
+
// Pi's --append-system-prompt accepts @filepath to read from a file.
|
|
826
|
+
// We use --system-prompt "" (empty base) + --append-system-prompt @file
|
|
827
|
+
// to effectively set the system prompt from a file.
|
|
828
|
+
systemPromptTempFile = join(
|
|
829
|
+
tmpdir(),
|
|
830
|
+
`pi-rpc-sysprompt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`,
|
|
831
|
+
);
|
|
832
|
+
writeFileSync(systemPromptTempFile, systemPromptContent, "utf-8");
|
|
833
|
+
piArgs.push("--system-prompt", "");
|
|
834
|
+
piArgs.push("--append-system-prompt", `@${systemPromptTempFile}`);
|
|
835
|
+
process.stderr.write(
|
|
836
|
+
`[rpc-wrapper] system prompt written to file (${systemPromptContent.length} chars): ${systemPromptTempFile}\n`,
|
|
837
|
+
);
|
|
826
838
|
}
|
|
827
|
-
}
|
|
828
|
-
process.on("exit", cleanupPidFile);
|
|
829
839
|
|
|
830
|
-
|
|
840
|
+
const proc = spawn("pi", piArgs, {
|
|
841
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
842
|
+
env: { ...process.env },
|
|
843
|
+
shell: true,
|
|
844
|
+
});
|
|
831
845
|
|
|
832
|
-
|
|
833
|
-
|
|
846
|
+
// ── TP-097: Write PID file for orphan cleanup ──────────────────
|
|
847
|
+
// Write both the wrapper PID and the pi child PID alongside the sidecar file.
|
|
848
|
+
// The task-runner reads this on session end to kill orphan processes.
|
|
849
|
+
// Format: JSON with wrapperPid and childPid fields.
|
|
850
|
+
const pidFilePath = args.sidecarPath + ".pid";
|
|
851
|
+
try {
|
|
852
|
+
const pidData = {
|
|
853
|
+
wrapperPid: process.pid,
|
|
854
|
+
childPid: proc.pid ?? null,
|
|
855
|
+
startedAt: Date.now(),
|
|
856
|
+
};
|
|
857
|
+
writeFileSync(pidFilePath, JSON.stringify(pidData) + "\n", "utf-8");
|
|
858
|
+
process.stderr.write(
|
|
859
|
+
`[rpc-wrapper] PID file written: ${pidFilePath} (wrapper=${process.pid}, child=${proc.pid})\n`,
|
|
860
|
+
);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
process.stderr.write(`[rpc-wrapper] WARNING: failed to write PID file: ${err.message}\n`);
|
|
863
|
+
}
|
|
834
864
|
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
865
|
+
// Clean up PID file on process exit (best-effort)
|
|
866
|
+
function cleanupPidFile() {
|
|
867
|
+
try {
|
|
868
|
+
unlinkSync(pidFilePath);
|
|
869
|
+
} catch {
|
|
870
|
+
/* ignore */
|
|
871
|
+
}
|
|
872
|
+
if (systemPromptTempFile) {
|
|
873
|
+
try {
|
|
874
|
+
unlinkSync(systemPromptTempFile);
|
|
875
|
+
} catch {
|
|
876
|
+
/* ignore */
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
process.on("exit", cleanupPidFile);
|
|
843
881
|
|
|
844
|
-
// ──
|
|
882
|
+
// ── Send prompt via JSONL stdin ──────────────────────────────────────
|
|
845
883
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (proc.stdin && !proc.stdin.destroyed) {
|
|
857
|
-
proc.stdin.end();
|
|
858
|
-
}
|
|
859
|
-
} catch {
|
|
860
|
-
// stdin may already be closed — ignore
|
|
884
|
+
const promptCmd = { type: "prompt", message: promptContent };
|
|
885
|
+
proc.stdin.write(JSON.stringify(promptCmd) + "\n");
|
|
886
|
+
|
|
887
|
+
// ── Agent Mailbox Steering Setup (TP-089) ────────────────────────────
|
|
888
|
+
// When mailbox-dir is provided, set steering mode to "all" so queued
|
|
889
|
+
// steering messages are delivered together at the next turn boundary.
|
|
890
|
+
// Must be sent after prompt but before any agent processing begins.
|
|
891
|
+
if (args.mailboxDir) {
|
|
892
|
+
proc.stdin.write(JSON.stringify({ type: "set_steering_mode", mode: "all" }) + "\n");
|
|
893
|
+
process.stderr.write(`[rpc-wrapper] mailbox enabled: ${args.mailboxDir}\n`);
|
|
861
894
|
}
|
|
862
|
-
}
|
|
863
895
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
896
|
+
// ── Stdin Lifecycle ──────────────────────────────────────────────────
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Close the child process stdin at a deterministic terminal point.
|
|
900
|
+
* RPC mode waits for more commands while stdin is open — without closing it,
|
|
901
|
+
* the pi process can hang indefinitely after `agent_end` or a terminal error.
|
|
902
|
+
*
|
|
903
|
+
* Called from: agent_end handler, terminal response error handler.
|
|
904
|
+
* Safe to call multiple times (checks destroyed flag).
|
|
905
|
+
*/
|
|
906
|
+
function closeStdin() {
|
|
907
|
+
try {
|
|
908
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
909
|
+
proc.stdin.end();
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
// stdin may already be closed — ignore
|
|
874
913
|
}
|
|
875
|
-
} catch {
|
|
876
|
-
// stdin may be closed — ignore
|
|
877
914
|
}
|
|
878
|
-
}
|
|
879
915
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
"auto_retry_start",
|
|
895
|
-
"auto_retry_end",
|
|
896
|
-
"auto_compaction_start",
|
|
897
|
-
"response",
|
|
898
|
-
]);
|
|
899
|
-
|
|
900
|
-
function handleEvent(event) {
|
|
901
|
-
if (!event || !event.type) return;
|
|
902
|
-
|
|
903
|
-
// Write only telemetry-relevant events to sidecar (redacted)
|
|
904
|
-
if (SIDECAR_EVENT_TYPES.has(event.type)) {
|
|
905
|
-
writeSidecarEvent(args.sidecarPath, event);
|
|
916
|
+
/**
|
|
917
|
+
* Query pi for authoritative session stats including contextUsage.
|
|
918
|
+
* Available in pi ≥ 0.63.0 (RPC get_session_stats exposes contextUsage).
|
|
919
|
+
* Safe to call on older versions — the command is ignored or returns
|
|
920
|
+
* without the field, and state.contextUsage stays null.
|
|
921
|
+
*/
|
|
922
|
+
function querySessionStats() {
|
|
923
|
+
try {
|
|
924
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
925
|
+
proc.stdin.write(JSON.stringify({ type: "get_session_stats" }) + "\n");
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
// stdin may be closed — ignore
|
|
929
|
+
}
|
|
906
930
|
}
|
|
907
931
|
|
|
908
|
-
//
|
|
909
|
-
|
|
932
|
+
// ── Route RPC events ─────────────────────────────────────────────────
|
|
933
|
+
|
|
934
|
+
// Event types worth persisting to the sidecar JSONL.
|
|
935
|
+
// Streaming deltas (content_block_delta, content_block_start/stop, message_start,
|
|
936
|
+
// input_json_delta, etc.) are omitted — they're high-volume, large, and not used
|
|
937
|
+
// by the dashboard or telemetry consumers. A single merge agent can produce 42MB+
|
|
938
|
+
// of sidecar data from streaming deltas alone.
|
|
939
|
+
const SIDECAR_EVENT_TYPES = new Set([
|
|
940
|
+
"agent_start",
|
|
941
|
+
"agent_end",
|
|
942
|
+
"message_end",
|
|
943
|
+
"tool_execution_start",
|
|
944
|
+
"tool_execution_end",
|
|
945
|
+
"tool_execution_update",
|
|
946
|
+
"auto_retry_start",
|
|
947
|
+
"auto_retry_end",
|
|
948
|
+
"auto_compaction_start",
|
|
949
|
+
"response",
|
|
950
|
+
]);
|
|
951
|
+
|
|
952
|
+
function handleEvent(event) {
|
|
953
|
+
if (!event || !event.type) return;
|
|
954
|
+
|
|
955
|
+
// Write only telemetry-relevant events to sidecar (redacted)
|
|
956
|
+
if (SIDECAR_EVENT_TYPES.has(event.type)) {
|
|
957
|
+
writeSidecarEvent(args.sidecarPath, event);
|
|
958
|
+
}
|
|
910
959
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
960
|
+
// Delegate state mutation to the extracted (testable) accumulator
|
|
961
|
+
applyEvent(state, event);
|
|
962
|
+
|
|
963
|
+
// Side effects that depend on the event type (IO, stdin lifecycle, display)
|
|
964
|
+
switch (event.type) {
|
|
965
|
+
case "message_end":
|
|
966
|
+
displayProgress(state);
|
|
967
|
+
// Query pi for authoritative context usage (pi ≥ 0.63.0).
|
|
968
|
+
// Falls back gracefully: older pi versions ignore the command
|
|
969
|
+
// or return a response without contextUsage — state.contextUsage stays null.
|
|
970
|
+
querySessionStats();
|
|
971
|
+
// Check mailbox for pending steering messages (TP-089).
|
|
972
|
+
// Only active when --mailbox-dir is provided (backward compatible).
|
|
973
|
+
if (args.mailboxDir) {
|
|
974
|
+
try {
|
|
975
|
+
checkMailboxAndSteer(args.mailboxDir, proc, args.steeringPendingPath || null);
|
|
976
|
+
} catch (err) {
|
|
977
|
+
// Never crash on mailbox I/O errors
|
|
978
|
+
process.stderr.write(`\n[STEERING] ERROR: ${err.message}\n`);
|
|
979
|
+
}
|
|
927
980
|
}
|
|
928
|
-
|
|
929
|
-
break;
|
|
930
|
-
|
|
931
|
-
case "tool_execution_start":
|
|
932
|
-
displayProgress(state);
|
|
933
|
-
break;
|
|
981
|
+
break;
|
|
934
982
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
// without this, the process can hang indefinitely.
|
|
939
|
-
closeStdin();
|
|
940
|
-
break;
|
|
983
|
+
case "tool_execution_start":
|
|
984
|
+
displayProgress(state);
|
|
985
|
+
break;
|
|
941
986
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
987
|
+
case "agent_end":
|
|
988
|
+
// Close stdin so pi process can exit cleanly.
|
|
989
|
+
// RPC mode waits for more commands while stdin is open;
|
|
990
|
+
// without this, the process can hang indefinitely.
|
|
945
991
|
closeStdin();
|
|
946
|
-
|
|
947
|
-
break;
|
|
992
|
+
break;
|
|
948
993
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
994
|
+
case "response":
|
|
995
|
+
// Terminal error response — close stdin to let pi exit
|
|
996
|
+
if (event.success === false && event.error) {
|
|
997
|
+
closeStdin();
|
|
998
|
+
}
|
|
999
|
+
break;
|
|
953
1000
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const event = JSON.parse(line);
|
|
958
|
-
handleEvent(event);
|
|
959
|
-
} catch {
|
|
960
|
-
// Malformed JSON line — log to stderr but don't crash
|
|
961
|
-
process.stderr.write(`\n[rpc-wrapper] malformed JSONL: ${line.slice(0, 200)}\n`);
|
|
962
|
-
}
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
// Forward stderr from pi to our stderr
|
|
966
|
-
// Capture pi stderr for diagnostics — last 2KB preserved in exit summary.
|
|
967
|
-
// This is critical for diagnosing startup crashes (pi exits code 1 with 0 tokens).
|
|
968
|
-
let piStderrBuffer = "";
|
|
969
|
-
const PI_STDERR_MAX = 2048;
|
|
970
|
-
proc.stderr?.setEncoding("utf-8");
|
|
971
|
-
proc.stderr?.on("data", (chunk) => {
|
|
972
|
-
process.stderr.write(chunk);
|
|
973
|
-
piStderrBuffer += chunk;
|
|
974
|
-
if (piStderrBuffer.length > PI_STDERR_MAX * 2) {
|
|
975
|
-
piStderrBuffer = piStderrBuffer.slice(-PI_STDERR_MAX);
|
|
1001
|
+
default:
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
976
1004
|
}
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
// ── Single-Write Exit Summary Finalization ───────────────────────────
|
|
980
1005
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
process.stderr.write(`\n[rpc-wrapper] exit summary written to ${args.exitSummaryPath}\n`);
|
|
992
|
-
} catch (err) {
|
|
993
|
-
process.stderr.write(`\n[rpc-wrapper] FATAL: failed to write exit summary: ${err.message}\n`);
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
// ── Process Lifecycle Handlers ───────────────────────────────────────
|
|
998
|
-
|
|
999
|
-
// Primary handler: process close event (most authoritative source of exit info)
|
|
1000
|
-
proc.on("close", (code, signal) => {
|
|
1001
|
-
// Newline after progress display
|
|
1002
|
-
process.stderr.write("\n");
|
|
1003
|
-
|
|
1004
|
-
if (!state.agentEnded && code !== 0) {
|
|
1005
|
-
// Process crashed without agent_end — capture what we have
|
|
1006
|
-
const stderrTail = piStderrBuffer.trim().slice(-PI_STDERR_MAX);
|
|
1007
|
-
const crashError = state.error || `pi process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}${stderrTail ? `\npi stderr: ${stderrTail}` : ""}`;
|
|
1008
|
-
writeExitSummary(state, code, signal, crashError, startTime);
|
|
1009
|
-
} else {
|
|
1010
|
-
writeExitSummary(state, code, signal, null, startTime);
|
|
1011
|
-
}
|
|
1012
|
-
});
|
|
1006
|
+
// Read RPC events from stdout using JSONL line-buffering
|
|
1007
|
+
attachJsonlReader(proc.stdout, (line) => {
|
|
1008
|
+
try {
|
|
1009
|
+
const event = JSON.parse(line);
|
|
1010
|
+
handleEvent(event);
|
|
1011
|
+
} catch {
|
|
1012
|
+
// Malformed JSON line — log to stderr but don't crash
|
|
1013
|
+
process.stderr.write(`\n[rpc-wrapper] malformed JSONL: ${line.slice(0, 200)}\n`);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1013
1016
|
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1017
|
+
// Forward stderr from pi to our stderr
|
|
1018
|
+
// Capture pi stderr for diagnostics — last 2KB preserved in exit summary.
|
|
1019
|
+
// This is critical for diagnosing startup crashes (pi exits code 1 with 0 tokens).
|
|
1020
|
+
let piStderrBuffer = "";
|
|
1021
|
+
const PI_STDERR_MAX = 2048;
|
|
1022
|
+
proc.stderr?.setEncoding("utf-8");
|
|
1023
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1024
|
+
process.stderr.write(chunk);
|
|
1025
|
+
piStderrBuffer += chunk;
|
|
1026
|
+
if (piStderrBuffer.length > PI_STDERR_MAX * 2) {
|
|
1027
|
+
piStderrBuffer = piStderrBuffer.slice(-PI_STDERR_MAX);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1018
1030
|
|
|
1019
|
-
// ──
|
|
1031
|
+
// ── Single-Write Exit Summary Finalization ───────────────────────────
|
|
1020
1032
|
|
|
1021
|
-
/**
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1033
|
+
/**
|
|
1034
|
+
* Single-write guard: ensures exit summary is written exactly once
|
|
1035
|
+
* across all termination paths (close, error, signal handlers).
|
|
1036
|
+
*
|
|
1037
|
+
* Uses the extracted createSingleWriteGuard + buildExitSummary for testability.
|
|
1038
|
+
* The first handler to call writeExitSummary() wins; subsequent calls are no-ops.
|
|
1039
|
+
*/
|
|
1040
|
+
const writeExitSummary = createSingleWriteGuard((summary) => {
|
|
1041
|
+
try {
|
|
1042
|
+
writeFileSync(resolve(args.exitSummaryPath), JSON.stringify(summary, null, 2) + "\n", "utf-8");
|
|
1043
|
+
process.stderr.write(`\n[rpc-wrapper] exit summary written to ${args.exitSummaryPath}\n`);
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
process.stderr.write(`\n[rpc-wrapper] FATAL: failed to write exit summary: ${err.message}\n`);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1029
1048
|
|
|
1030
|
-
|
|
1031
|
-
if (signalForwarded) return;
|
|
1032
|
-
signalForwarded = true;
|
|
1049
|
+
// ── Process Lifecycle Handlers ───────────────────────────────────────
|
|
1033
1050
|
|
|
1034
|
-
process
|
|
1051
|
+
// Primary handler: process close event (most authoritative source of exit info)
|
|
1052
|
+
proc.on("close", (code, signal) => {
|
|
1053
|
+
// Newline after progress display
|
|
1054
|
+
process.stderr.write("\n");
|
|
1035
1055
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1056
|
+
if (!state.agentEnded && code !== 0) {
|
|
1057
|
+
// Process crashed without agent_end — capture what we have
|
|
1058
|
+
const stderrTail = piStderrBuffer.trim().slice(-PI_STDERR_MAX);
|
|
1059
|
+
const crashError =
|
|
1060
|
+
state.error ||
|
|
1061
|
+
`pi process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}${stderrTail ? `\npi stderr: ${stderrTail}` : ""}`;
|
|
1062
|
+
writeExitSummary(state, code, signal, crashError, startTime);
|
|
1063
|
+
} else {
|
|
1064
|
+
writeExitSummary(state, code, signal, null, startTime);
|
|
1040
1065
|
}
|
|
1041
|
-
}
|
|
1042
|
-
// stdin may already be closed
|
|
1043
|
-
}
|
|
1066
|
+
});
|
|
1044
1067
|
|
|
1045
|
-
//
|
|
1046
|
-
|
|
1068
|
+
// Fallback handler: spawn error (e.g., pi binary not found)
|
|
1069
|
+
proc.on("error", (err) => {
|
|
1070
|
+
writeExitSummary(state, null, null, `spawn error: ${err.message}`, startTime);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
// ── Signal Forwarding ────────────────────────────────────────────────
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Forward SIGTERM/SIGINT to the pi process via RPC abort command.
|
|
1077
|
+
* This allows graceful shutdown of the agent before the process exits.
|
|
1078
|
+
*
|
|
1079
|
+
* On Windows, SIGTERM/SIGINT behavior differs — we handle both and
|
|
1080
|
+
* attempt graceful abort first, then hard kill after a timeout.
|
|
1081
|
+
*/
|
|
1082
|
+
let signalForwarded = false;
|
|
1083
|
+
|
|
1084
|
+
function forwardSignal(signal) {
|
|
1085
|
+
if (signalForwarded) return;
|
|
1086
|
+
signalForwarded = true;
|
|
1087
|
+
|
|
1088
|
+
process.stderr.write(`\n[rpc-wrapper] received ${signal}, sending abort to pi...\n`);
|
|
1089
|
+
|
|
1090
|
+
// Try graceful abort via RPC
|
|
1047
1091
|
try {
|
|
1048
|
-
proc.
|
|
1092
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
1093
|
+
proc.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
|
|
1094
|
+
}
|
|
1049
1095
|
} catch {
|
|
1050
|
-
//
|
|
1096
|
+
// stdin may already be closed
|
|
1051
1097
|
}
|
|
1052
|
-
}, 5000);
|
|
1053
1098
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1099
|
+
// Give pi 5 seconds to shut down gracefully, then hard kill
|
|
1100
|
+
const killTimer = setTimeout(() => {
|
|
1101
|
+
try {
|
|
1102
|
+
proc.kill("SIGTERM");
|
|
1103
|
+
} catch {
|
|
1104
|
+
// Process may already be dead
|
|
1105
|
+
}
|
|
1106
|
+
}, 5000);
|
|
1060
1107
|
|
|
1061
|
-
//
|
|
1108
|
+
// Don't let the timer keep the process alive
|
|
1109
|
+
if (killTimer.unref) killTimer.unref();
|
|
1110
|
+
}
|
|
1062
1111
|
|
|
1063
|
-
process.on("
|
|
1064
|
-
process.
|
|
1065
|
-
writeExitSummary(state, null, null, `wrapper uncaught exception: ${err.message}`, startTime);
|
|
1066
|
-
process.exit(1);
|
|
1067
|
-
});
|
|
1112
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
1113
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
1068
1114
|
|
|
1069
|
-
|
|
1070
|
-
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
1071
|
-
process.stderr.write(`\n[rpc-wrapper] unhandled rejection: ${msg}\n`);
|
|
1072
|
-
writeExitSummary(state, null, null, `wrapper unhandled rejection: ${msg}`, startTime);
|
|
1073
|
-
process.exit(1);
|
|
1074
|
-
});
|
|
1115
|
+
// ── Uncaught Exception / Unhandled Rejection Handler ─────────────────
|
|
1075
1116
|
|
|
1076
|
-
|
|
1117
|
+
process.on("uncaughtException", (err) => {
|
|
1118
|
+
process.stderr.write(`\n[rpc-wrapper] uncaught exception: ${err.message}\n`);
|
|
1119
|
+
writeExitSummary(state, null, null, `wrapper uncaught exception: ${err.message}`, startTime);
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
});
|
|
1077
1122
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
process.
|
|
1123
|
+
process.on("unhandledRejection", (reason) => {
|
|
1124
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
1125
|
+
process.stderr.write(`\n[rpc-wrapper] unhandled rejection: ${msg}\n`);
|
|
1126
|
+
writeExitSummary(state, null, null, `wrapper unhandled rejection: ${msg}`, startTime);
|
|
1127
|
+
process.exit(1);
|
|
1083
1128
|
});
|
|
1084
|
-
});
|
|
1085
1129
|
|
|
1130
|
+
// ── Exit Code Forwarding ─────────────────────────────────────────────
|
|
1131
|
+
|
|
1132
|
+
// Forward the pi process exit code as our own (normalized: null/negative/non-finite → 1)
|
|
1133
|
+
proc.on("close", (code) => {
|
|
1134
|
+
// Use setImmediate to let other close handlers run first
|
|
1135
|
+
setImmediate(() => {
|
|
1136
|
+
process.exitCode = typeof code === "number" && Number.isFinite(code) && code >= 0 ? code : 1;
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1086
1139
|
} // end _main()
|