@pify/shell-background 0.6.0 → 0.6.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/README.md +1 -1
- package/extensions/shell-background.ts +63 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ shell_kill { id: "bg-2" } # stop it and its whole process tree
|
|
|
34
34
|
|
|
35
35
|
## Delivery, and the headless caveat
|
|
36
36
|
|
|
37
|
-
When a backgrounded command finishes in an **interactive** session, its result is pushed into the conversation as the next turn — you do not have to poll. Under headless `pi -p` there is nothing to deliver into (the session tears down when the prompt resolves), so **auto-background is disabled there** and only explicit `background: true` applies; collect it within the same turn with `shell_status { id, wait: N }`, which blocks (up to `N` seconds, 0–300) until the command finishes rather than returning immediately. This is the same delivery rule the rest of the suite lives by. A
|
|
37
|
+
When a backgrounded command finishes in an **interactive** session, its result is pushed into the conversation as the next turn — you do not have to poll. Under headless `pi -p` there is nothing to deliver into (the session tears down when the prompt resolves), so **auto-background is disabled there** and only explicit `background: true` applies; collect it within the same turn with `shell_status { id, wait: N }`, which blocks (up to `N` seconds, 0–300) until the command finishes rather than returning immediately. This is the same delivery rule the rest of the suite lives by. A job's result is pushed only at the end of the current turn, not the instant the command exits, so a job you collect during the turn with `shell_status {id}` (with or without `wait`) is never also delivered as a follow-up — the model never spends a turn acknowledging a result it has just reported. A job nobody collects is delivered once when the turn ends, and a job that finishes while the agent is idle wakes it right away.
|
|
38
38
|
|
|
39
39
|
## How it works
|
|
40
40
|
|
|
@@ -242,6 +242,47 @@ export default function shellBackground(pi: ExtensionAPI) {
|
|
|
242
242
|
const waiters = new Map<string, number>();
|
|
243
243
|
const awaited = (job: Job): boolean => (waiters.get(job.id) ?? 0) > 0;
|
|
244
244
|
|
|
245
|
+
// A finished job's result must reach the model exactly once, and NOT while
|
|
246
|
+
// the model may still collect it itself. The old code sent the follow-up the
|
|
247
|
+
// instant the process exited, so a `shell_status {id}` that collected the
|
|
248
|
+
// same job later in that turn got the result a second time (issue #2): the
|
|
249
|
+
// message was already queued when the job ended, and marking it collected
|
|
250
|
+
// afterwards was too late. So a finished job is held in `pending` and
|
|
251
|
+
// delivered only at a turn boundary (agent_settled) — or right away while
|
|
252
|
+
// idle, so a job finishing between turns still wakes the agent — and the
|
|
253
|
+
// drain skips any job a `shell_status` read marked `delivered` meanwhile.
|
|
254
|
+
// `waiters` still covers an ACTIVE wait, which returns and collects the
|
|
255
|
+
// result itself and so must never also be sent.
|
|
256
|
+
const pending = new Set<Job>();
|
|
257
|
+
let turnActive = false;
|
|
258
|
+
|
|
259
|
+
function enqueueDelivery(job: Job): void {
|
|
260
|
+
if (job.delivered) return;
|
|
261
|
+
pending.add(job);
|
|
262
|
+
maybeDrainDeliveries();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Drain now only while idle; during a turn the model may still collect a job, so wait for agent_settled. */
|
|
266
|
+
function maybeDrainDeliveries(): void {
|
|
267
|
+
if (!turnActive) drainDeliveries();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function drainDeliveries(): void {
|
|
271
|
+
for (const job of [...pending]) {
|
|
272
|
+
// Collected (by a shell_status read) since it was queued — never send it.
|
|
273
|
+
if (job.delivered) {
|
|
274
|
+
pending.delete(job);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// An active shell_status wait will return and collect this job itself.
|
|
278
|
+
if (awaited(job)) continue;
|
|
279
|
+
pending.delete(job);
|
|
280
|
+
job.delivered = true;
|
|
281
|
+
registry?.persist(job);
|
|
282
|
+
deliver(job);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
245
286
|
/** Deliver a finished background job into the conversation, once. */
|
|
246
287
|
function scheduleDelivery(job: Job, exit: Promise<unknown>): void {
|
|
247
288
|
exit
|
|
@@ -254,10 +295,7 @@ export default function shellBackground(pi: ExtensionAPI) {
|
|
|
254
295
|
registry?.persist(job);
|
|
255
296
|
return;
|
|
256
297
|
}
|
|
257
|
-
|
|
258
|
-
job.delivered = true;
|
|
259
|
-
registry?.persist(job);
|
|
260
|
-
deliver(job);
|
|
298
|
+
enqueueDelivery(job);
|
|
261
299
|
})
|
|
262
300
|
.catch(() => {
|
|
263
301
|
// A /reload can make captured handles throw; delivery is a convenience,
|
|
@@ -544,19 +582,17 @@ export default function shellBackground(pi: ExtensionAPI) {
|
|
|
544
582
|
}
|
|
545
583
|
if (sig?.aborted) {
|
|
546
584
|
// The model never sees an aborted tool result. If the job finished
|
|
547
|
-
// while
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
registry.persist(job);
|
|
551
|
-
deliver(job);
|
|
552
|
-
}
|
|
585
|
+
// while the wait held delivery back, hand it to the turn-boundary drain
|
|
586
|
+
// (it was not collected here) rather than dropping it.
|
|
587
|
+
if (isFinished(job) && !job.delivered) enqueueDelivery(job);
|
|
553
588
|
return { content: [{ type: "text", text: `shell_status ${job.id}: wait aborted` }], details: { id: job.id }, isError: true };
|
|
554
589
|
}
|
|
555
|
-
// Reading a finished job marks it collected so
|
|
556
|
-
//
|
|
557
|
-
// delivered here would permanently cancel the promised
|
|
590
|
+
// Reading a finished job marks it collected so the turn-boundary drain
|
|
591
|
+
// never also sends it (issue #2). A still-running poll must never do this:
|
|
592
|
+
// setting delivered here would permanently cancel the promised delivery.
|
|
558
593
|
if (isFinished(job)) {
|
|
559
594
|
job.delivered = true;
|
|
595
|
+
pending.delete(job);
|
|
560
596
|
registry.persist(job);
|
|
561
597
|
}
|
|
562
598
|
return {
|
|
@@ -642,11 +678,7 @@ export default function shellBackground(pi: ExtensionAPI) {
|
|
|
642
678
|
}
|
|
643
679
|
}
|
|
644
680
|
if (job.status === "running") anyRunning = true;
|
|
645
|
-
else
|
|
646
|
-
job.delivered = true;
|
|
647
|
-
registry.persist(job);
|
|
648
|
-
deliver(job);
|
|
649
|
-
}
|
|
681
|
+
else enqueueDelivery(job);
|
|
650
682
|
}
|
|
651
683
|
renderWidget(ctx);
|
|
652
684
|
if (!anyRunning) clearInterval(timer);
|
|
@@ -654,6 +686,19 @@ export default function shellBackground(pi: ExtensionAPI) {
|
|
|
654
686
|
timer.unref?.();
|
|
655
687
|
}
|
|
656
688
|
|
|
689
|
+
// Turn boundaries drive delivery: a finished job is pushed only when the
|
|
690
|
+
// current turn ends, so a job the model collects during the turn is never
|
|
691
|
+
// also delivered as a follow-up (issue #2).
|
|
692
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
693
|
+
turnActive = true;
|
|
694
|
+
lastUiCtx = ctx;
|
|
695
|
+
});
|
|
696
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
697
|
+
turnActive = false;
|
|
698
|
+
lastUiCtx = ctx;
|
|
699
|
+
drainDeliveries();
|
|
700
|
+
});
|
|
701
|
+
|
|
657
702
|
pi.on("session_start", async (_event, ctx) => {
|
|
658
703
|
const warnings = loadSettings(ctx.cwd);
|
|
659
704
|
const key = sessionKey(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pify/shell-background",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Long-running bash goes async: background: true launches detached, and any command still running after 30s auto-backgrounds and delivers its result when it finishes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|