@yusukeshib/pi-babysit 0.3.9 → 0.3.12
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/README.md +26 -10
- package/format-stream.mjs +39 -12
- package/index.ts +190 -50
- package/package.json +2 -1
- package/rpc-stream-proxy.mjs +134 -0
package/README.md
CHANGED
|
@@ -62,8 +62,11 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
|
62
62
|
| `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: `ids` + `mode: "any"\|"all"` |
|
|
63
63
|
| `babysit_kill` | Terminate a session, verify terminal state, then suppress the exit notification |
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
The built-in `bash` tool is removed from the active tool set so the model does
|
|
66
|
+
not waste a failed tool turn before choosing `babysit_run`. A fallback
|
|
67
|
+
`tool_call` hook still blocks direct shell calls if another extension or preset
|
|
68
|
+
re-enables `bash`, including shell backgrounding (`… &`, `nohup`, `setsid`,
|
|
69
|
+
`disown`). Set `PI_BABYSIT_ALLOW_BASH=1` to retain direct `bash` explicitly.
|
|
67
70
|
|
|
68
71
|
## Commands (human)
|
|
69
72
|
|
|
@@ -91,11 +94,21 @@ babysit_check { id: "cargo-test", lines: 50 }
|
|
|
91
94
|
babysit_check { id: "cargo-test", pattern: "FAIL|ERROR", lines: 50 }
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
Tail and search results are capped at 200 lines and
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
Tail and search results are capped at 200 lines, and the complete returned tool
|
|
98
|
+
result (including lifecycle headers) is clipped to 8 KB. Pattern search returns
|
|
99
|
+
the latest matching lines with line numbers. Prefer a targeted pattern over a
|
|
100
|
+
broad tail, and do not read a potentially large log file in full.
|
|
101
|
+
|
|
102
|
+
Subagent logs also compact Pi's streaming `message_update` events before they
|
|
103
|
+
are recorded. Pi repeats the complete growing assistant message and partial
|
|
104
|
+
snapshot on every token; pi-babysit retains only the incremental delta while
|
|
105
|
+
leaving authoritative `message_end`, tool, lifecycle, response, and error
|
|
106
|
+
events untouched. This keeps long RPC sessions approximately linear in emitted
|
|
107
|
+
content without changing final answers, completion detection, or follow-up
|
|
108
|
+
behavior. Live `/babysit` and attach views render the retained deltas.
|
|
109
|
+
|
|
110
|
+
All shell commands, including `pwd` and Git, go through `babysit_run`. Bundle
|
|
111
|
+
closely related tiny observations when doing so safely reduces tool turns.
|
|
99
112
|
Set `PI_BABYSIT_ALLOW_BASH=1` only as an explicit emergency escape hatch.
|
|
100
113
|
|
|
101
114
|
## Unexpected worker loss
|
|
@@ -109,9 +122,12 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
109
122
|
|
|
110
123
|
## How completion detection works
|
|
111
124
|
|
|
112
|
-
- **Process**: a 2.5s poller watches for running→exited transitions and
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
- **Process**: a 2.5s poller watches for running→exited transitions and, once the
|
|
126
|
+
parent agent is idle, injects one
|
|
127
|
+
`pi.sendMessage(…, { triggerTurn: true, deliverAs: "steer" })` containing every
|
|
128
|
+
deliverable exit observed in that poll (deduped via `meta/<id>.json`). Waiting
|
|
129
|
+
for idleness prevents an immediately-following `babysit_wait` from racing the
|
|
130
|
+
poller and receiving a duplicate completion.
|
|
115
131
|
`babysit_kill` and an exit already reported by `babysit_wait` suppress the
|
|
116
132
|
notification.
|
|
117
133
|
- **Subagent**: `babysit_wait` blocks on `babysit expect '"type":"agent_end"'`.
|
package/format-stream.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* format-stream.mjs — human-readable view for a `pi --mode
|
|
3
|
+
* format-stream.mjs — human-readable view for a `pi --mode rpc` event stream.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* pane sees turns, thinking, tool calls and answers instead of a JSON firehose.
|
|
5
|
+
* This filter sits on the interactive attach path via `babysit run --view-cmd`.
|
|
6
|
+
* It accepts both Pi's raw cumulative updates and pi-babysit's compact deltas,
|
|
7
|
+
* then prints a colored, incrementally streamed transcript so a human sees
|
|
8
|
+
* turns, thinking, tool calls and answers instead of a JSON firehose.
|
|
10
9
|
*/
|
|
11
10
|
import * as readline from "node:readline";
|
|
12
11
|
|
|
@@ -52,8 +51,8 @@ function summarizeTool(name, a = {}) {
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
// Join the text of a given block kind ("text" or "thinking") in a message's
|
|
55
|
-
// content array.
|
|
56
|
-
//
|
|
54
|
+
// content array. Unfiltered RPC streams repeat the whole growing message;
|
|
55
|
+
// pi-babysit's compact stream instead retains only assistantMessageEvent.delta.
|
|
57
56
|
function blockText(content, kind) {
|
|
58
57
|
if (!Array.isArray(content)) return "";
|
|
59
58
|
let s = "";
|
|
@@ -75,15 +74,20 @@ const openBlock = () => {
|
|
|
75
74
|
out("\n");
|
|
76
75
|
};
|
|
77
76
|
|
|
78
|
-
function
|
|
79
|
-
if (!msg || msg.role !== "assistant") return;
|
|
80
|
-
const id = msg.id ?? `t${turn}`;
|
|
77
|
+
function messageState(id = `t${turn}`, fresh = false) {
|
|
81
78
|
if (fresh) emitted.delete(id);
|
|
82
79
|
let st = emitted.get(id);
|
|
83
80
|
if (!st) {
|
|
84
81
|
st = { think: 0, text: 0, hThink: false, hText: false };
|
|
85
82
|
emitted.set(id, st);
|
|
86
83
|
}
|
|
84
|
+
return st;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function streamMessage(msg, fresh = false) {
|
|
88
|
+
if (!msg || msg.role !== "assistant") return;
|
|
89
|
+
const id = msg.id ?? `t${turn}`;
|
|
90
|
+
const st = messageState(id, fresh);
|
|
87
91
|
const think = blockText(msg.content, "thinking");
|
|
88
92
|
if (think.length > st.think) {
|
|
89
93
|
if (!st.hThink) {
|
|
@@ -106,6 +110,28 @@ function streamMessage(msg, fresh = false) {
|
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
function streamDelta(event) {
|
|
114
|
+
if (!event || typeof event.delta !== "string" || !event.delta) return;
|
|
115
|
+
const st = messageState();
|
|
116
|
+
if (event.type === "thinking_delta") {
|
|
117
|
+
if (!st.hThink) {
|
|
118
|
+
openBlock();
|
|
119
|
+
out(`${C.gray}[think] `);
|
|
120
|
+
st.hThink = true;
|
|
121
|
+
}
|
|
122
|
+
out(C.gray + event.delta + C.reset);
|
|
123
|
+
st.think += event.delta.length;
|
|
124
|
+
} else if (event.type === "text_delta") {
|
|
125
|
+
if (!st.hText) {
|
|
126
|
+
openBlock();
|
|
127
|
+
out(`${C.green}[text]${C.reset} `);
|
|
128
|
+
st.hText = true;
|
|
129
|
+
}
|
|
130
|
+
out(event.delta);
|
|
131
|
+
st.text += event.delta.length;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
function usageLine(msg) {
|
|
110
136
|
const u = msg?.usage;
|
|
111
137
|
if (!u) return;
|
|
@@ -138,7 +164,8 @@ rl.on("line", (line) => {
|
|
|
138
164
|
streamMessage(ev.message, true);
|
|
139
165
|
break;
|
|
140
166
|
case "message_update":
|
|
141
|
-
streamMessage(ev.message);
|
|
167
|
+
if (ev.message) streamMessage(ev.message);
|
|
168
|
+
else streamDelta(ev.assistantMessageEvent);
|
|
142
169
|
break;
|
|
143
170
|
case "message_end":
|
|
144
171
|
streamMessage(ev.message);
|
package/index.ts
CHANGED
|
@@ -36,10 +36,15 @@ import * as fs from "node:fs";
|
|
|
36
36
|
import * as os from "node:os";
|
|
37
37
|
import * as path from "node:path";
|
|
38
38
|
import { fileURLToPath } from "node:url";
|
|
39
|
-
import type {
|
|
39
|
+
import type {
|
|
40
|
+
ExtensionAPI,
|
|
41
|
+
ExtensionContext,
|
|
42
|
+
Theme,
|
|
43
|
+
ToolDefinition,
|
|
44
|
+
} from "@earendil-works/pi-coding-agent";
|
|
40
45
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
41
|
-
import { Box,
|
|
42
|
-
import { Type } from "typebox";
|
|
46
|
+
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
47
|
+
import { Type, type TSchema } from "typebox";
|
|
43
48
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
44
49
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents";
|
|
45
50
|
|
|
@@ -130,10 +135,10 @@ export function planSubagentSpawn(
|
|
|
130
135
|
// apart from any other turn end (see isParkedToolResult / self-reap.ts).
|
|
131
136
|
export const NOTIFY_MARKER = "[notify-on-exit]";
|
|
132
137
|
|
|
133
|
-
// Human-readable view for the subagent JSONL stream when a human
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// disable
|
|
138
|
+
// Human-readable view for the compact subagent JSONL stream when a human
|
|
139
|
+
// attaches. The RPC proxy removes cumulative streaming snapshots before both
|
|
140
|
+
// recording and display; authoritative events remain intact for parsers. Set
|
|
141
|
+
// PI_BABYSIT_VIEW_CMD="" to disable formatting, or provide a custom command.
|
|
137
142
|
const EXT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
138
143
|
const shq = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
139
144
|
const VIEW_CMD =
|
|
@@ -365,6 +370,10 @@ interface Meta {
|
|
|
365
370
|
// Temporary reservation while kill is in flight. Unlike `notified`, this
|
|
366
371
|
// must be cleared on failure so a real completion remains deliverable.
|
|
367
372
|
notificationPaused?: boolean;
|
|
373
|
+
// Concurrent explicit waits share a reference-counted notification claim.
|
|
374
|
+
// A timeout must not re-enable the poller while another wait still owns it.
|
|
375
|
+
waitReservations?: number;
|
|
376
|
+
waitCompletionClaimed?: boolean;
|
|
368
377
|
completionObservedAt?: number;
|
|
369
378
|
startedAt?: number;
|
|
370
379
|
// subagent
|
|
@@ -405,6 +414,10 @@ export function shouldDeliverProcessCompletion(
|
|
|
405
414
|
return meta?.kind === "process" && !meta.notified && !meta.notificationPaused;
|
|
406
415
|
}
|
|
407
416
|
|
|
417
|
+
export function shouldDeferCompletionNotification(agentIsIdle: boolean): boolean {
|
|
418
|
+
return !agentIsIdle;
|
|
419
|
+
}
|
|
420
|
+
|
|
408
421
|
const kindOf = (id: string): "process" | "subagent" => readMeta(id)?.kind ?? "process";
|
|
409
422
|
|
|
410
423
|
// Compact elapsed formatting: "42s", "3m12s", "1h04m".
|
|
@@ -528,14 +541,26 @@ const NOTIFY_COMMAND_MAX_BYTES = byteLimitFromEnv("PI_BABYSIT_NOTIFY_COMMAND_MAX
|
|
|
528
541
|
const NOTIFY_BATCH_MAX_BYTES = byteLimitFromEnv("PI_BABYSIT_NOTIFY_BATCH_MAX_BYTES", 8_000);
|
|
529
542
|
const ANSWER_MAX_BYTES = 24_000; // subagent answers / error messages
|
|
530
543
|
|
|
531
|
-
function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
544
|
+
export function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
545
|
+
if (maxBytes <= 0) return "";
|
|
532
546
|
const buf = Buffer.from(s, "utf8");
|
|
533
547
|
if (buf.length <= maxBytes) return s;
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
548
|
+
|
|
549
|
+
// The marker counts toward the limit. Recompute a few times because the
|
|
550
|
+
// omitted-byte count can change the marker's digit width.
|
|
551
|
+
let available = maxBytes;
|
|
552
|
+
let marker = "";
|
|
553
|
+
for (let i = 0; i < 3; i++) {
|
|
554
|
+
marker = `\n… [${buf.length - available} bytes elided] …\n`;
|
|
555
|
+
available = Math.max(0, maxBytes - Buffer.byteLength(marker, "utf8"));
|
|
556
|
+
}
|
|
557
|
+
if (Buffer.byteLength(marker, "utf8") > maxBytes) return truncateUtf8End(marker, maxBytes);
|
|
558
|
+
const headBytes = Math.floor(available / 2);
|
|
559
|
+
const tailBytes = available - headBytes;
|
|
560
|
+
// Strip replacement chars from a mid-codepoint cut at either boundary.
|
|
561
|
+
const head = buf.subarray(0, headBytes).toString("utf8").replace(/\uFFFD+$/, "");
|
|
562
|
+
const tail = buf.subarray(buf.length - tailBytes).toString("utf8").replace(/^\uFFFD+/, "");
|
|
563
|
+
return `${head}${marker}${tail}`;
|
|
539
564
|
}
|
|
540
565
|
|
|
541
566
|
async function searchLog(
|
|
@@ -634,7 +659,10 @@ export function summarizeNotificationCommand(command: string | undefined): strin
|
|
|
634
659
|
.trim()
|
|
635
660
|
.replace(/\r/g, "\\r")
|
|
636
661
|
.replace(/\n/g, "\\n")
|
|
637
|
-
.replace(/\t/g, "\\t")
|
|
662
|
+
.replace(/\t/g, "\\t")
|
|
663
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, (char) =>
|
|
664
|
+
`\\x${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
|
|
665
|
+
) || "?";
|
|
638
666
|
const bytes = Buffer.from(preview, "utf8");
|
|
639
667
|
if (bytes.length <= NOTIFY_COMMAND_MAX_BYTES) return preview;
|
|
640
668
|
if (NOTIFY_COMMAND_MAX_BYTES === 0) return "";
|
|
@@ -674,6 +702,7 @@ interface ProcessCompletionMessage {
|
|
|
674
702
|
status: CompletionStatus;
|
|
675
703
|
runtime?: string;
|
|
676
704
|
logPath?: string;
|
|
705
|
+
command?: string;
|
|
677
706
|
count: number;
|
|
678
707
|
totalCount: number;
|
|
679
708
|
remainingCount: number;
|
|
@@ -683,6 +712,7 @@ interface ProcessCompletionMessage {
|
|
|
683
712
|
success: boolean;
|
|
684
713
|
status: CompletionStatus;
|
|
685
714
|
runtime: string;
|
|
715
|
+
command: string | undefined;
|
|
686
716
|
logPath: string;
|
|
687
717
|
}>;
|
|
688
718
|
};
|
|
@@ -791,6 +821,7 @@ export function buildProcessCompletionMessage(
|
|
|
791
821
|
success: notice.success,
|
|
792
822
|
status: notice.status,
|
|
793
823
|
runtime: notice.runtime,
|
|
824
|
+
command: notice.command,
|
|
794
825
|
logPath: notice.logPath,
|
|
795
826
|
}));
|
|
796
827
|
const single = totalCount === 1 ? batch[0] : undefined;
|
|
@@ -805,6 +836,7 @@ export function buildProcessCompletionMessage(
|
|
|
805
836
|
status,
|
|
806
837
|
runtime: single?.runtime,
|
|
807
838
|
logPath: single?.logPath,
|
|
839
|
+
command: single?.command,
|
|
808
840
|
count: batch.length,
|
|
809
841
|
totalCount,
|
|
810
842
|
remainingCount: totalCount - batch.length,
|
|
@@ -871,7 +903,7 @@ interface ToolCall {
|
|
|
871
903
|
name: string;
|
|
872
904
|
summary: string;
|
|
873
905
|
}
|
|
874
|
-
interface Progress {
|
|
906
|
+
export interface Progress {
|
|
875
907
|
turns: number;
|
|
876
908
|
toolCalls: ToolCall[];
|
|
877
909
|
finalText: string;
|
|
@@ -914,7 +946,7 @@ function summarizeToolCall(name: string, args: Record<string, unknown>): string
|
|
|
914
946
|
}
|
|
915
947
|
}
|
|
916
948
|
|
|
917
|
-
function parseEvents(logText: string): Progress {
|
|
949
|
+
export function parseEvents(logText: string): Progress {
|
|
918
950
|
const p: Progress = {
|
|
919
951
|
turns: 0,
|
|
920
952
|
toolCalls: [],
|
|
@@ -1122,13 +1154,21 @@ async function spawnSubagent(
|
|
|
1122
1154
|
"--timeout",
|
|
1123
1155
|
opts.timeout,
|
|
1124
1156
|
];
|
|
1125
|
-
// Pretty-print the JSONL stream for humans who `attach
|
|
1126
|
-
//
|
|
1157
|
+
// Pretty-print the compact JSONL stream for humans who `attach`. The RPC
|
|
1158
|
+
// proxy removes only cumulative streaming snapshots; authoritative final,
|
|
1159
|
+
// tool, lifecycle, response, and error events remain intact for parseEvents.
|
|
1127
1160
|
if (VIEW_CMD.trim()) bsArgs.push("--view-cmd", VIEW_CMD);
|
|
1128
1161
|
if (opts.idleTimeout && opts.idleTimeout !== "none") {
|
|
1129
1162
|
bsArgs.push("--idle-timeout", opts.idleTimeout);
|
|
1130
1163
|
}
|
|
1131
|
-
bsArgs.push(
|
|
1164
|
+
bsArgs.push(
|
|
1165
|
+
"--",
|
|
1166
|
+
process.execPath,
|
|
1167
|
+
path.join(EXT_DIR, "rpc-stream-proxy.mjs"),
|
|
1168
|
+
"--",
|
|
1169
|
+
PI_BIN,
|
|
1170
|
+
...piArgs,
|
|
1171
|
+
);
|
|
1132
1172
|
|
|
1133
1173
|
const r = await bs(bsArgs, {
|
|
1134
1174
|
cwd: opts.cwd,
|
|
@@ -1427,19 +1467,47 @@ function suppressNotify(id: string, reason: "observed" | "kill" = "observed"): v
|
|
|
1427
1467
|
}
|
|
1428
1468
|
}
|
|
1429
1469
|
|
|
1430
|
-
export
|
|
1470
|
+
export interface WaitReservationState {
|
|
1431
1471
|
notified?: boolean;
|
|
1432
1472
|
killNotificationSuppressed?: boolean;
|
|
1433
|
-
|
|
1434
|
-
|
|
1473
|
+
waitReservations?: number;
|
|
1474
|
+
waitCompletionClaimed?: boolean;
|
|
1435
1475
|
}
|
|
1436
1476
|
|
|
1437
|
-
function
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
meta.
|
|
1441
|
-
|
|
1477
|
+
export function canRestoreNotificationAfterWait(meta: WaitReservationState): boolean {
|
|
1478
|
+
return (
|
|
1479
|
+
meta.notified === true &&
|
|
1480
|
+
meta.killNotificationSuppressed !== true &&
|
|
1481
|
+
meta.waitCompletionClaimed !== true &&
|
|
1482
|
+
(meta.waitReservations ?? 0) === 0
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
export function transitionWaitReservation(
|
|
1487
|
+
state: WaitReservationState,
|
|
1488
|
+
action: "reserve" | "abandon" | "claim",
|
|
1489
|
+
): WaitReservationState {
|
|
1490
|
+
const next = { ...state };
|
|
1491
|
+
if (action === "reserve") {
|
|
1492
|
+
next.waitReservations = (next.waitReservations ?? 0) + 1;
|
|
1493
|
+
next.notified = true;
|
|
1494
|
+
return next;
|
|
1442
1495
|
}
|
|
1496
|
+
|
|
1497
|
+
next.waitReservations = Math.max(0, (next.waitReservations ?? 0) - 1);
|
|
1498
|
+
if (action === "claim") next.waitCompletionClaimed = true;
|
|
1499
|
+
if (action === "claim" || next.waitCompletionClaimed) {
|
|
1500
|
+
next.notified = true;
|
|
1501
|
+
} else if (canRestoreNotificationAfterWait(next)) {
|
|
1502
|
+
next.notified = false;
|
|
1503
|
+
}
|
|
1504
|
+
return next;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function updateWaitReservation(id: string, action: "reserve" | "abandon" | "claim"): void {
|
|
1508
|
+
const meta = readMeta(id);
|
|
1509
|
+
if (!meta || meta.kind !== "process") return;
|
|
1510
|
+
writeMeta(id, { ...meta, ...transitionWaitReservation(meta, action) });
|
|
1443
1511
|
}
|
|
1444
1512
|
|
|
1445
1513
|
function pauseNotify(id: string): void {
|
|
@@ -1493,19 +1561,20 @@ async function waitForExit(
|
|
|
1493
1561
|
}
|
|
1494
1562
|
// fall through: session exited before the pattern appeared
|
|
1495
1563
|
} else {
|
|
1496
|
-
// An explicit wait owns completion delivery.
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1564
|
+
// An explicit wait owns completion delivery. Reference-count the claim so
|
|
1565
|
+
// one concurrent wait timing out cannot re-enable notifications underneath
|
|
1566
|
+
// another wait that is still pending.
|
|
1567
|
+
updateWaitReservation(id, "reserve");
|
|
1499
1568
|
const w = await bs(["wait", "-s", id, "--timeout", t], { signal });
|
|
1500
1569
|
if (signal?.aborted || w.code === 130) {
|
|
1501
|
-
|
|
1570
|
+
updateWaitReservation(id, "abandon");
|
|
1502
1571
|
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
1503
1572
|
}
|
|
1504
1573
|
if (w.code === 124) {
|
|
1505
1574
|
// 124 is ambiguous (timeout vs child exiting 124) — disambiguate.
|
|
1506
1575
|
const st0 = await statusOf(id);
|
|
1507
1576
|
if (st0?.state === "running") {
|
|
1508
|
-
|
|
1577
|
+
updateWaitReservation(id, "abandon");
|
|
1509
1578
|
return {
|
|
1510
1579
|
id,
|
|
1511
1580
|
kind: "timeout",
|
|
@@ -1519,9 +1588,15 @@ async function waitForExit(
|
|
|
1519
1588
|
|
|
1520
1589
|
const st = await statusOf(id);
|
|
1521
1590
|
if (!st) {
|
|
1591
|
+
// A non-pattern backend wait returned before this lookup, so preserve its
|
|
1592
|
+
// completion claim when status persistence is transiently unavailable.
|
|
1593
|
+
// Abandoning here can re-enable the poller and duplicate a completion the
|
|
1594
|
+
// explicit wait already consumed. This matches the old suppress-first rule.
|
|
1595
|
+
if (!expectPattern) updateWaitReservation(id, "claim");
|
|
1522
1596
|
return { id, kind: "exited", ok: false, text: `No such session: ${id}` };
|
|
1523
1597
|
}
|
|
1524
|
-
suppressNotify(id);
|
|
1598
|
+
if (expectPattern) suppressNotify(id);
|
|
1599
|
+
else updateWaitReservation(id, "claim"); // the agent sees the exit here; don't notify again
|
|
1525
1600
|
const meta = readMeta(id);
|
|
1526
1601
|
const workerDead = st.state === "dead" && st.exit_code == null;
|
|
1527
1602
|
const ok = st.exit_code === 0;
|
|
@@ -1573,12 +1648,41 @@ export function isAllowedDirectBash(_command: string): boolean {
|
|
|
1573
1648
|
return process.env.PI_BABYSIT_ALLOW_BASH === "1";
|
|
1574
1649
|
}
|
|
1575
1650
|
|
|
1651
|
+
export function activeToolsWithoutDirectBash(activeTools: string[], allowDirectBash: boolean): string[] {
|
|
1652
|
+
return allowDirectBash ? activeTools : activeTools.filter((name) => name !== "bash");
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1576
1655
|
// ---------------------------------------------------------------------------
|
|
1577
1656
|
// extension
|
|
1578
1657
|
// ---------------------------------------------------------------------------
|
|
1579
1658
|
|
|
1580
1659
|
export default function (pi: ExtensionAPI) {
|
|
1581
1660
|
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
1661
|
+
const declaredToolErrors = new Set<string>();
|
|
1662
|
+
|
|
1663
|
+
// Pi only persists custom-tool failures when they are thrown or patched by a
|
|
1664
|
+
// tool_result hook; an `isError` property returned from execute() is ignored.
|
|
1665
|
+
// Keep the structured result (status, log path, diagnostics) while promoting
|
|
1666
|
+
// its declared error bit at the supported hook boundary.
|
|
1667
|
+
const registerTool = <TParams extends TSchema, TDetails, TState>(
|
|
1668
|
+
tool: ToolDefinition<TParams, TDetails, TState>,
|
|
1669
|
+
): void => {
|
|
1670
|
+
const execute = tool.execute;
|
|
1671
|
+
pi.registerTool({
|
|
1672
|
+
...tool,
|
|
1673
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1674
|
+
const result = await execute(toolCallId, params, signal, onUpdate, ctx);
|
|
1675
|
+
if ((result as typeof result & { isError?: boolean }).isError === true) {
|
|
1676
|
+
declaredToolErrors.add(toolCallId);
|
|
1677
|
+
}
|
|
1678
|
+
return result;
|
|
1679
|
+
},
|
|
1680
|
+
});
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
pi.on("tool_result", (event) => {
|
|
1684
|
+
if (declaredToolErrors.delete(event.toolCallId)) return { isError: true };
|
|
1685
|
+
});
|
|
1582
1686
|
|
|
1583
1687
|
// Exit notifications for kind=process sessions: the poller detects
|
|
1584
1688
|
// running→exited transitions and injects ONE message (triggerTurn) for all
|
|
@@ -1586,7 +1690,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1586
1690
|
// that ended its turn after babysit_run without spending one turn per exit.
|
|
1587
1691
|
// Kills via babysit_kill and exits already reported by babysit_wait are
|
|
1588
1692
|
// suppressed via meta.notified.
|
|
1589
|
-
async function notifyEndedProcesses(): Promise<void> {
|
|
1693
|
+
async function notifyEndedProcesses(ctx: ExtensionContext): Promise<void> {
|
|
1694
|
+
// Never steer a completion into an active agent turn. In particular, this
|
|
1695
|
+
// lets an immediately-following babysit_wait reserve the completion first,
|
|
1696
|
+
// instead of racing the poller and receiving both wait + auto notification.
|
|
1697
|
+
if (shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
1590
1698
|
const { sessions } = await listSessions();
|
|
1591
1699
|
const ready: Array<{ session: BsSession; meta: Meta }> = [];
|
|
1592
1700
|
for (const session of sessions) {
|
|
@@ -1645,6 +1753,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1645
1753
|
metadataById.set(notice.id, current);
|
|
1646
1754
|
return [{ ...notice, command: current.command }];
|
|
1647
1755
|
});
|
|
1756
|
+
// Output collection above yields to the event loop. Re-check idleness so a
|
|
1757
|
+
// newly-started agent turn cannot receive a duplicate completion mid-turn.
|
|
1758
|
+
if (shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
1648
1759
|
deliverProcessCompletionMessage(
|
|
1649
1760
|
notices,
|
|
1650
1761
|
(message, options) => pi.sendMessage(message, options),
|
|
@@ -1738,17 +1849,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
1738
1849
|
status?: DisplayStatus;
|
|
1739
1850
|
success?: boolean;
|
|
1740
1851
|
exitCode?: number | null;
|
|
1852
|
+
command?: string;
|
|
1853
|
+
count?: number;
|
|
1741
1854
|
};
|
|
1742
1855
|
const status =
|
|
1743
1856
|
d.status ?? (d.success ? "success" : d.exitCode == null ? "terminated" : "failed");
|
|
1857
|
+
const payload = d.command
|
|
1858
|
+
? ` ${summarizeNotificationCommand(d.command)}`
|
|
1859
|
+
: d.count && d.count > 1
|
|
1860
|
+
? ` ×${d.count}`
|
|
1861
|
+
: "";
|
|
1862
|
+
const header = theme.fg("warning", theme.bold(`babysit_run COMMAND${payload}`));
|
|
1744
1863
|
const box = new Box(1, 1, (t) => theme.bg("toolSuccessBg", t));
|
|
1745
|
-
box.addChild(new Text(
|
|
1864
|
+
box.addChild(new Text(header, 0, 0));
|
|
1865
|
+
box.addChild(new Text(renderStatus(status, theme), 0, 0));
|
|
1746
1866
|
box.addChild(new Text(theme.fg("toolOutput", content), 0, 0));
|
|
1747
1867
|
return box;
|
|
1748
1868
|
});
|
|
1749
1869
|
|
|
1750
1870
|
let polling = false;
|
|
1751
1871
|
pi.on("session_start", async (_event, ctx) => {
|
|
1872
|
+
// Do not expose the built-in bash tool only to reject it after the model has
|
|
1873
|
+
// already paid for a failed tool turn. The tool_call hook remains a fallback
|
|
1874
|
+
// if another extension/preset re-enables bash later in the session.
|
|
1875
|
+
const activeTools = pi.getActiveTools();
|
|
1876
|
+
const supervisedTools = activeToolsWithoutDirectBash(
|
|
1877
|
+
activeTools,
|
|
1878
|
+
isAllowedDirectBash(""),
|
|
1879
|
+
);
|
|
1880
|
+
if (supervisedTools.length !== activeTools.length) pi.setActiveTools(supervisedTools);
|
|
1881
|
+
|
|
1752
1882
|
// Session-local registry: scope the babysit root to this pi session so
|
|
1753
1883
|
// other sessions' processes/subagents are invisible here. Resuming a
|
|
1754
1884
|
// session keeps the same id, so its sessions come back with it.
|
|
@@ -1768,7 +1898,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1768
1898
|
// calls never stack up.
|
|
1769
1899
|
if (polling) return;
|
|
1770
1900
|
polling = true;
|
|
1771
|
-
Promise.all([notifyEndedProcesses(), refreshWidget(ctx)])
|
|
1901
|
+
Promise.all([notifyEndedProcesses(ctx), refreshWidget(ctx)])
|
|
1772
1902
|
.catch(() => {
|
|
1773
1903
|
/* ignore poll errors */
|
|
1774
1904
|
})
|
|
@@ -1805,7 +1935,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1805
1935
|
});
|
|
1806
1936
|
|
|
1807
1937
|
// ----- babysit_run --------------------------------------------------------
|
|
1808
|
-
|
|
1938
|
+
registerTool({
|
|
1809
1939
|
name: "babysit_run",
|
|
1810
1940
|
label: "Babysit: run",
|
|
1811
1941
|
description:
|
|
@@ -1826,7 +1956,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1826
1956
|
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
1827
1957
|
promptGuidelines: [
|
|
1828
1958
|
"Use babysit_run as the default for shell commands, not only long-running work. Small output is returned directly; large stdout/stderr stays out of model context in the returned log path. Give meaningful commands a clear stable `name`.",
|
|
1829
|
-
"
|
|
1959
|
+
"Bundle closely related tiny observations into one babysit_run command when that reduces tool turns without obscuring lifecycle or failure handling.",
|
|
1960
|
+
"Inspect a babysit log with babysit_check { id, lines, pattern? }; never read or cat a potentially large log file in full. Prefer a targeted `pattern` search over returning a broad tail.",
|
|
1830
1961
|
"After babysit_run { command } starts a process, end your response immediately so the automatic process-end notification can resume you; NEVER poll with babysit_check or sleep. Set continueAfterStart: true only when you have immediate, specific, non-polling work to do next. Call babysit_wait when you must consume the result inside the current turn (optionally with `expect` to wait for a readiness line like 'listening on').",
|
|
1831
1962
|
"If a babysit worker is killed externally, babysit_run reports it as worker-dead rather than hanging. Set retryOnWorkerDeath: true only for safe, idempotent commands; it retries at most once and may otherwise duplicate side effects.",
|
|
1832
1963
|
"babysit_run gives full PTY control: drive interactive programs (installers, wizards, REPLs) with babysit_send (text or named keys) and read the rendered screen with babysit_check { screen: true }.",
|
|
@@ -2129,10 +2260,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
2129
2260
|
},
|
|
2130
2261
|
};
|
|
2131
2262
|
},
|
|
2132
|
-
//
|
|
2133
|
-
//
|
|
2134
|
-
renderCall() {
|
|
2135
|
-
|
|
2263
|
+
// Make the execution kind and payload identifiable before the result arrives.
|
|
2264
|
+
// Use semantic theme colors rather than the fallback gray tool-call text.
|
|
2265
|
+
renderCall(args, theme) {
|
|
2266
|
+
const isSubagent = args.profile === "subagent";
|
|
2267
|
+
const kind = isSubagent ? "AGENT" : "COMMAND";
|
|
2268
|
+
const payload = isSubagent ? args.task : args.command;
|
|
2269
|
+
const agent = isSubagent && args.agent ? ` [${args.agent}]` : "";
|
|
2270
|
+
const title = theme.bold(`babysit_run ${kind}`);
|
|
2271
|
+
const detail =
|
|
2272
|
+
typeof payload === "string" && payload.length > 0
|
|
2273
|
+
? ` ${summarizeNotificationCommand(payload)}`
|
|
2274
|
+
: "";
|
|
2275
|
+
return new Text(theme.fg("warning", `${title}${agent}${detail}`), 0, 0);
|
|
2136
2276
|
},
|
|
2137
2277
|
renderResult(result, { isPartial }, theme, context) {
|
|
2138
2278
|
const details = (result.details ?? {}) as {
|
|
@@ -2160,13 +2300,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
2160
2300
|
: details.kind === "subagent" || content.includes(NOTIFY_MARKER)
|
|
2161
2301
|
? "started"
|
|
2162
2302
|
: "success");
|
|
2163
|
-
const label = renderStatus(status, theme
|
|
2303
|
+
const label = renderStatus(status, theme);
|
|
2164
2304
|
return new Text(content ? `${label}\n${theme.fg("toolOutput", content)}` : label, 0, 0);
|
|
2165
2305
|
},
|
|
2166
2306
|
});
|
|
2167
2307
|
|
|
2168
2308
|
// ----- babysit_check ------------------------------------------------------
|
|
2169
|
-
|
|
2309
|
+
registerTool({
|
|
2170
2310
|
name: "babysit_check",
|
|
2171
2311
|
label: "Babysit: check",
|
|
2172
2312
|
description:
|
|
@@ -2224,7 +2364,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2224
2364
|
const preview = what.length > 60 ? `${what.slice(0, 57)}…` : what;
|
|
2225
2365
|
return `${s.id} [${kind}] ${s.state}${ec}${depth}${flag}${preview ? ` — ${preview}` : ""}`;
|
|
2226
2366
|
});
|
|
2227
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { sessions } };
|
|
2367
|
+
return { content: [{ type: "text", text: clip(lines.join("\n")) }], details: { sessions } };
|
|
2228
2368
|
}
|
|
2229
2369
|
|
|
2230
2370
|
const st = await statusOf(params.id);
|
|
@@ -2266,7 +2406,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2266
2406
|
? `--- latest matches /${params.pattern}/ ---\n${result.text}`
|
|
2267
2407
|
: `(no output matching /${params.pattern}/)`;
|
|
2268
2408
|
return {
|
|
2269
|
-
content: [{ type: "text", text: `${header}\n${body}` }],
|
|
2409
|
+
content: [{ type: "text", text: clip(`${header}\n${body}`) }],
|
|
2270
2410
|
details: { status: st, kind, logPath: logPath(params.id), pattern: params.pattern },
|
|
2271
2411
|
};
|
|
2272
2412
|
}
|
|
@@ -2294,7 +2434,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2294
2434
|
parts.push(tail ? `--- recent output ---\n${tail}` : "(no output yet)");
|
|
2295
2435
|
}
|
|
2296
2436
|
return {
|
|
2297
|
-
content: [{ type: "text", text: parts.join("\n") }],
|
|
2437
|
+
content: [{ type: "text", text: clip(parts.join("\n")) }],
|
|
2298
2438
|
details: { status: st, kind: "process", logPath: logPath(params.id) },
|
|
2299
2439
|
};
|
|
2300
2440
|
}
|
|
@@ -2356,7 +2496,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2356
2496
|
});
|
|
2357
2497
|
|
|
2358
2498
|
// ----- babysit_send -------------------------------------------------------
|
|
2359
|
-
|
|
2499
|
+
registerTool({
|
|
2360
2500
|
name: "babysit_send",
|
|
2361
2501
|
label: "Babysit: send",
|
|
2362
2502
|
description:
|
|
@@ -2509,7 +2649,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2509
2649
|
});
|
|
2510
2650
|
|
|
2511
2651
|
// ----- babysit_wait -------------------------------------------------------
|
|
2512
|
-
|
|
2652
|
+
registerTool({
|
|
2513
2653
|
name: "babysit_wait",
|
|
2514
2654
|
label: "Babysit: wait",
|
|
2515
2655
|
description:
|
|
@@ -2620,7 +2760,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2620
2760
|
});
|
|
2621
2761
|
|
|
2622
2762
|
// ----- babysit_kill -------------------------------------------------------
|
|
2623
|
-
|
|
2763
|
+
registerTool({
|
|
2624
2764
|
name: "babysit_kill",
|
|
2625
2765
|
label: "Babysit: kill",
|
|
2626
2766
|
description: "Terminate a babysit session (process or subagent).",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yusukeshib/pi-babysit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"agents.ts",
|
|
24
24
|
"self-reap.ts",
|
|
25
25
|
"format-stream.mjs",
|
|
26
|
+
"rpc-stream-proxy.mjs",
|
|
26
27
|
"search-log.mjs",
|
|
27
28
|
"README.md",
|
|
28
29
|
"LICENSE"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Transparent stdin/stdout proxy for `pi --mode rpc`.
|
|
4
|
+
*
|
|
5
|
+
* Pi's `message_update` events repeat the entire growing assistant message and
|
|
6
|
+
* `assistantMessageEvent.partial` on every token. Those cumulative snapshots
|
|
7
|
+
* are useful only while streaming; `message_end` is the authoritative final
|
|
8
|
+
* message. Persisting every snapshot makes babysit logs grow quadratically.
|
|
9
|
+
*
|
|
10
|
+
* This proxy forwards stdin byte-for-byte and compacts only `message_update`
|
|
11
|
+
* lines on stdout. All final, tool, lifecycle, response, and error events pass
|
|
12
|
+
* through byte-for-byte.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { StringDecoder } from "node:string_decoder";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
export function compactRpcLine(line) {
|
|
19
|
+
if (!line.trimStart().startsWith("{")) return line;
|
|
20
|
+
|
|
21
|
+
let event;
|
|
22
|
+
try {
|
|
23
|
+
event = JSON.parse(line);
|
|
24
|
+
} catch {
|
|
25
|
+
return line;
|
|
26
|
+
}
|
|
27
|
+
if (event?.type !== "message_update") return line;
|
|
28
|
+
|
|
29
|
+
const { message: _cumulativeMessage, assistantMessageEvent, ...rest } = event;
|
|
30
|
+
if (!assistantMessageEvent || typeof assistantMessageEvent !== "object") {
|
|
31
|
+
return JSON.stringify(rest);
|
|
32
|
+
}
|
|
33
|
+
const { partial: _cumulativePartial, ...deltaEvent } = assistantMessageEvent;
|
|
34
|
+
return JSON.stringify({ ...rest, assistantMessageEvent: deltaEvent });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transformRpcOutput(input, output) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const decoder = new StringDecoder("utf8");
|
|
40
|
+
let buffered = "";
|
|
41
|
+
|
|
42
|
+
const write = (value) => {
|
|
43
|
+
if (!output.write(value)) input.pause();
|
|
44
|
+
};
|
|
45
|
+
output.on("drain", () => input.resume());
|
|
46
|
+
input.on("error", reject);
|
|
47
|
+
input.on("data", (chunk) => {
|
|
48
|
+
buffered += decoder.write(chunk);
|
|
49
|
+
for (;;) {
|
|
50
|
+
const newline = buffered.indexOf("\n");
|
|
51
|
+
if (newline < 0) break;
|
|
52
|
+
const line = buffered.slice(0, newline);
|
|
53
|
+
buffered = buffered.slice(newline + 1);
|
|
54
|
+
write(`${compactRpcLine(line)}\n`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
input.on("end", () => {
|
|
58
|
+
buffered += decoder.end();
|
|
59
|
+
if (buffered) write(compactRpcLine(buffered));
|
|
60
|
+
if (output.writableNeedDrain) output.once("drain", resolve);
|
|
61
|
+
else resolve();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function waitForDrain(stream) {
|
|
67
|
+
return stream.writableNeedDrain
|
|
68
|
+
? new Promise((resolve) => stream.once("drain", resolve))
|
|
69
|
+
: Promise.resolve();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function runRpcProxy(argv = process.argv.slice(2)) {
|
|
73
|
+
const separator = argv.indexOf("--");
|
|
74
|
+
const command = separator >= 0 ? argv[separator + 1] : argv[0];
|
|
75
|
+
const args = separator >= 0 ? argv.slice(separator + 2) : argv.slice(1);
|
|
76
|
+
if (!command) {
|
|
77
|
+
console.error("Usage: rpc-stream-proxy.mjs -- <command> [args...]");
|
|
78
|
+
process.exitCode = 2;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const child = spawn(command, args, {
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
env: process.env,
|
|
85
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let transportFailed = false;
|
|
89
|
+
process.stdin.pipe(child.stdin);
|
|
90
|
+
// The RPC worker can exit or self-reap while a command is still arriving.
|
|
91
|
+
// Ignore the resulting pipe-close race so the proxy preserves the worker's
|
|
92
|
+
// actual exit code/signal instead of crashing with an unhandled EPIPE.
|
|
93
|
+
child.stdin.on("error", (error) => {
|
|
94
|
+
if (error?.code === "EPIPE" || error?.code === "ERR_STREAM_DESTROYED") return;
|
|
95
|
+
transportFailed = true;
|
|
96
|
+
console.error(`rpc stream proxy stdin failed: ${error.message}`);
|
|
97
|
+
});
|
|
98
|
+
const outputFlushed = transformRpcOutput(child.stdout, process.stdout);
|
|
99
|
+
child.stderr.pipe(process.stderr);
|
|
100
|
+
|
|
101
|
+
const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
102
|
+
const signalHandlers = new Map(
|
|
103
|
+
forwardedSignals.map((signal) => [signal, () => child.kill(signal)]),
|
|
104
|
+
);
|
|
105
|
+
for (const [signal, handler] of signalHandlers) process.on(signal, handler);
|
|
106
|
+
let spawnFailed = false;
|
|
107
|
+
child.on("error", (error) => {
|
|
108
|
+
spawnFailed = true;
|
|
109
|
+
console.error(`rpc stream proxy failed to start ${command}: ${error.message}`);
|
|
110
|
+
});
|
|
111
|
+
child.on("close", async (code, signal) => {
|
|
112
|
+
try {
|
|
113
|
+
await outputFlushed;
|
|
114
|
+
await Promise.all([waitForDrain(process.stdout), waitForDrain(process.stderr)]);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`rpc stream proxy output failed: ${error.message}`);
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (transportFailed) {
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (signal) {
|
|
125
|
+
for (const [forwarded, handler] of signalHandlers) process.off(forwarded, handler);
|
|
126
|
+
process.kill(process.pid, signal);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
process.exitCode = spawnFailed ? 1 : (code ?? 1);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : undefined;
|
|
134
|
+
if (entry === import.meta.url) runRpcProxy();
|