@yusukeshib/pi-babysit 0.3.16 → 0.3.17
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 +7 -5
- package/index.ts +79 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,10 +157,12 @@ grace window (`PI_BABYSIT_REAP_AFTER`, default 120s) using the same parked-turn
|
|
|
157
157
|
rule, so a subagent waiting on a long build is never false-killed. Give bounded
|
|
158
158
|
recon/review tasks at least one cost, turn, tool-call, or token budget; omit
|
|
159
159
|
budgets only for intentionally open-ended work. Optional task budgets are
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
observed by the parent poller. They are wrap-up thresholds rather than exact
|
|
161
|
+
hard caps: an already in-flight model call or parallel tool batch can overshoot
|
|
162
|
+
before the next poll. On the first observed threshold the worker is steered to
|
|
163
|
+
stop using tools and return its best answer; if it remains active after
|
|
164
|
+
`PI_BABYSIT_BUDGET_GRACE`, termination is verified before the task is marked
|
|
165
|
+
budget-killed. Usage shown by check/wait is cumulative for the task.
|
|
164
166
|
|
|
165
167
|
## Environment overrides
|
|
166
168
|
|
|
@@ -172,7 +174,7 @@ marked budget-killed. Usage shown by check/wait is cumulative for the task.
|
|
|
172
174
|
| `PI_BABYSIT_VIEW_CMD` | bundled `format-stream.mjs` | live-attach pretty printer for subagent JSONL (`""` disables) |
|
|
173
175
|
| `PI_BABYSIT_QUICK_GRACE` | `2s` | interactive process grace before a still-running command is returned as background work; use `foreground: true` to wait explicitly |
|
|
174
176
|
| `PI_BABYSIT_REAP_AFTER` | `120s` | idle grace before a finished subagent self-exits (`off`/`none`/`0` disables) |
|
|
175
|
-
| `PI_BABYSIT_BUDGET_GRACE` | `
|
|
177
|
+
| `PI_BABYSIT_BUDGET_GRACE` | `90s` | grace after a subagent budget is exceeded before verified termination |
|
|
176
178
|
| `PI_BABYSIT_RPC_LOG_MODE` | `compact` | `compact` removes duplicate RPC lifecycle payloads; `standard` opts into legacy payloads |
|
|
177
179
|
| `PI_BABYSIT_RETENTION_DAYS` | unset | when set to a positive number, remove safe terminal roots older than this at session startup |
|
|
178
180
|
| `PI_BABYSIT_TAIL_MAX_BYTES` | `8000` | cap for explicit log tails/screens returned by `babysit_check` |
|
package/index.ts
CHANGED
|
@@ -791,6 +791,33 @@ export function parseRpcResponseBytes(
|
|
|
791
791
|
// it. Distinguishes: success, explicit failure (success:false → the error
|
|
792
792
|
// message, e.g. "No API key found for …"), subagent death, and timeout — this
|
|
793
793
|
// is what makes bad-model/config failures LOUD instead of silent.
|
|
794
|
+
export function rpcResponsePattern(command: string): string {
|
|
795
|
+
const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
796
|
+
// Match only a complete response record. A loose `"command":"…"` can occur
|
|
797
|
+
// in assistant/tool text, and matching before the terminating newline can
|
|
798
|
+
// race the writer while the JSON record is still partial.
|
|
799
|
+
return `(?m)^\\{(?:"id":"[^"\\n]*",)?"type":"response","command":"${escaped}"[^\\n]*\\}\\r?\\n`;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function readLogBytesFrom(file: string, since: number): Buffer {
|
|
803
|
+
const size = fs.statSync(file).size;
|
|
804
|
+
const offset = Math.min(Math.max(0, since), size);
|
|
805
|
+
const length = size - offset;
|
|
806
|
+
const bytes = Buffer.allocUnsafe(length);
|
|
807
|
+
const fd = fs.openSync(file, "r");
|
|
808
|
+
let read = 0;
|
|
809
|
+
try {
|
|
810
|
+
while (read < length) {
|
|
811
|
+
const count = fs.readSync(fd, bytes, read, length - read, offset + read);
|
|
812
|
+
if (count === 0) break;
|
|
813
|
+
read += count;
|
|
814
|
+
}
|
|
815
|
+
} finally {
|
|
816
|
+
fs.closeSync(fd);
|
|
817
|
+
}
|
|
818
|
+
return bytes.subarray(0, read);
|
|
819
|
+
}
|
|
820
|
+
|
|
794
821
|
async function rpcResponse(
|
|
795
822
|
id: string,
|
|
796
823
|
since: number,
|
|
@@ -802,7 +829,16 @@ async function rpcResponse(
|
|
|
802
829
|
| { ok: false; error: string }
|
|
803
830
|
> {
|
|
804
831
|
const e = await bs(
|
|
805
|
-
[
|
|
832
|
+
[
|
|
833
|
+
"expect",
|
|
834
|
+
"-s",
|
|
835
|
+
id,
|
|
836
|
+
"--since",
|
|
837
|
+
String(since),
|
|
838
|
+
"--timeout",
|
|
839
|
+
timeout,
|
|
840
|
+
rpcResponsePattern(command),
|
|
841
|
+
],
|
|
806
842
|
{ signal },
|
|
807
843
|
);
|
|
808
844
|
if (e.code !== 0) {
|
|
@@ -810,8 +846,9 @@ async function rpcResponse(
|
|
|
810
846
|
if (st && st.state !== "running") {
|
|
811
847
|
let structuredError = "";
|
|
812
848
|
try {
|
|
813
|
-
|
|
814
|
-
|
|
849
|
+
// Long-lived follow-up workers can have very large historical logs. Read
|
|
850
|
+
// only the response window rather than synchronously loading all history.
|
|
851
|
+
structuredError = parseEvents(readLogBytesFrom(logPath(id), since).toString("utf8")).errorMsg ?? "";
|
|
815
852
|
} catch {
|
|
816
853
|
/* full log path below remains the diagnostic source */
|
|
817
854
|
}
|
|
@@ -832,22 +869,7 @@ async function rpcResponse(
|
|
|
832
869
|
};
|
|
833
870
|
}
|
|
834
871
|
try {
|
|
835
|
-
|
|
836
|
-
const size = fs.statSync(file).size;
|
|
837
|
-
const length = Math.max(0, size - since);
|
|
838
|
-
const bytes = Buffer.allocUnsafe(length);
|
|
839
|
-
const fd = fs.openSync(file, "r");
|
|
840
|
-
let read = 0;
|
|
841
|
-
try {
|
|
842
|
-
while (read < length) {
|
|
843
|
-
const count = fs.readSync(fd, bytes, read, length - read, since + read);
|
|
844
|
-
if (count === 0) break;
|
|
845
|
-
read += count;
|
|
846
|
-
}
|
|
847
|
-
} finally {
|
|
848
|
-
fs.closeSync(fd);
|
|
849
|
-
}
|
|
850
|
-
return parseRpcResponseBytes(bytes.subarray(0, read), since, command);
|
|
872
|
+
return parseRpcResponseBytes(readLogBytesFrom(logPath(id), since), since, command);
|
|
851
873
|
} catch (error) {
|
|
852
874
|
return { ok: false, error: `could not read ${command} response: ${String(error)}` };
|
|
853
875
|
}
|
|
@@ -887,7 +909,7 @@ const NOTIFY_BATCH_MAX_BYTES = byteLimitFromEnv("PI_BABYSIT_NOTIFY_BATCH_MAX_BYT
|
|
|
887
909
|
const ANSWER_MAX_BYTES = 24_000; // single subagent answers / structured error messages
|
|
888
910
|
const MAX_MULTI_WAIT_SESSIONS = 32;
|
|
889
911
|
const SUBAGENT_BUDGET_GRACE_MS =
|
|
890
|
-
parseDurMs(process.env.PI_BABYSIT_BUDGET_GRACE ?? "
|
|
912
|
+
parseDurMs(process.env.PI_BABYSIT_BUDGET_GRACE ?? "90s") ?? 90_000;
|
|
891
913
|
|
|
892
914
|
export function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
893
915
|
if (maxBytes <= 0) return "";
|
|
@@ -1561,6 +1583,22 @@ interface TaskProgressCache {
|
|
|
1561
1583
|
}
|
|
1562
1584
|
const taskProgressCache = new Map<string, TaskProgressCache>();
|
|
1563
1585
|
|
|
1586
|
+
export function pruneTerminalSessionCache<T>(
|
|
1587
|
+
cache: Map<string, T>,
|
|
1588
|
+
sessions: Array<{ id: string; state: string }>,
|
|
1589
|
+
): number {
|
|
1590
|
+
const running = new Set(
|
|
1591
|
+
sessions.filter((session) => session.state === "running").map((session) => session.id),
|
|
1592
|
+
);
|
|
1593
|
+
let removed = 0;
|
|
1594
|
+
for (const id of cache.keys()) {
|
|
1595
|
+
if (running.has(id)) continue;
|
|
1596
|
+
cache.delete(id);
|
|
1597
|
+
removed++;
|
|
1598
|
+
}
|
|
1599
|
+
return removed;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1564
1602
|
/** Parse only bytes appended since the previous observation of this task. */
|
|
1565
1603
|
function taskProgressOf(id: string): { progress: Progress; offset: number } {
|
|
1566
1604
|
const base = readMeta(id)?.promptOffset ?? 0;
|
|
@@ -2424,16 +2462,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2424
2462
|
for (const session of sessions) {
|
|
2425
2463
|
if (session.state !== "running") continue;
|
|
2426
2464
|
await withSessionRpcLock(session.id, async () => {
|
|
2427
|
-
const latestStatus = await statusOf(session.id);
|
|
2428
2465
|
const meta = readMeta(session.id);
|
|
2429
|
-
if (
|
|
2430
|
-
latestStatus?.state !== "running" ||
|
|
2431
|
-
meta?.kind !== "subagent" ||
|
|
2432
|
-
!meta.budget ||
|
|
2433
|
-
meta.budgetKilled
|
|
2434
|
-
) {
|
|
2435
|
-
return;
|
|
2436
|
-
}
|
|
2466
|
+
if (meta?.kind !== "subagent" || !meta.budget || meta.budgetKilled) return;
|
|
2437
2467
|
|
|
2438
2468
|
let progress: Progress;
|
|
2439
2469
|
try {
|
|
@@ -2453,6 +2483,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2453
2483
|
if (decision.action === "none") return;
|
|
2454
2484
|
const reason = decision.reason as string;
|
|
2455
2485
|
|
|
2486
|
+
// The poll already supplied a running-session snapshot. Revalidate only
|
|
2487
|
+
// when a limit requires action; checking every healthy worker made each
|
|
2488
|
+
// interval launch N extra `babysit list` subprocesses.
|
|
2489
|
+
const latestStatus = await statusOf(session.id);
|
|
2490
|
+
if (latestStatus?.state !== "running") return;
|
|
2491
|
+
|
|
2456
2492
|
if (decision.action === "steer") {
|
|
2457
2493
|
// Start the grace period only after Pi accepts the steering command.
|
|
2458
2494
|
const sent = await sendRpc(session.id, {
|
|
@@ -2780,6 +2816,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2780
2816
|
notifyEndedProcesses(ctx, snapshot),
|
|
2781
2817
|
refreshWidget(ctx, snapshot),
|
|
2782
2818
|
]);
|
|
2819
|
+
pruneTerminalSessionCache(taskProgressCache, snapshot);
|
|
2783
2820
|
pollNeeded = shouldKeepPolling(snapshot, readMeta);
|
|
2784
2821
|
})()
|
|
2785
2822
|
.catch(() => {
|
|
@@ -2913,15 +2950,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
2913
2950
|
}),
|
|
2914
2951
|
),
|
|
2915
2952
|
maxTurns: Type.Optional(
|
|
2916
|
-
Type.Integer({
|
|
2953
|
+
Type.Integer({
|
|
2954
|
+
minimum: 1,
|
|
2955
|
+
description:
|
|
2956
|
+
"Subagent only: observed turn threshold for steering it to wrap up. In-flight work can overshoot before the poller intervenes.",
|
|
2957
|
+
}),
|
|
2917
2958
|
),
|
|
2918
2959
|
maxToolCalls: Type.Optional(
|
|
2919
|
-
Type.Integer({
|
|
2960
|
+
Type.Integer({
|
|
2961
|
+
minimum: 1,
|
|
2962
|
+
description:
|
|
2963
|
+
"Subagent only: observed tool-call threshold for steering it to wrap up. A parallel in-flight tool batch can overshoot.",
|
|
2964
|
+
}),
|
|
2920
2965
|
),
|
|
2921
2966
|
maxUsageTokens: Type.Optional(
|
|
2922
2967
|
Type.Integer({
|
|
2923
2968
|
minimum: 1,
|
|
2924
|
-
description:
|
|
2969
|
+
description:
|
|
2970
|
+
"Subagent only: observed cumulative reported totalTokens threshold for steering it to wrap up.",
|
|
2925
2971
|
}),
|
|
2926
2972
|
),
|
|
2927
2973
|
agentScope: Type.Optional(
|
|
@@ -3453,7 +3499,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3453
3499
|
if (el) header += ` elapsed=${el}`;
|
|
3454
3500
|
}
|
|
3455
3501
|
if (st.exit_code != null) header += ` exit_code=${st.exit_code}`;
|
|
3456
|
-
if (meta?.command) header += `\ncommand: ${meta.command}`;
|
|
3502
|
+
if (meta?.command) header += `\ncommand: ${summarizeNotificationCommand(meta.command)}`;
|
|
3457
3503
|
header += `\nlog: ${logPath(params.id)}`;
|
|
3458
3504
|
if (st.note) header += ` ⚑ ${st.note}`;
|
|
3459
3505
|
parts.push(header);
|