@stacknyu/pi-flow 1.5.0 → 1.5.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/CHANGELOG.md +6 -0
- package/extension.ts +31 -29
- package/package.json +1 -1
- package/runner.mjs +43 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# pi-flow changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.1
|
|
4
|
+
|
|
5
|
+
- Support Pi Web 0.15.0 session-history results, including compaction, fresh-answer validation and nested subagent completion/abort checks; preserve native CLI workers.
|
|
6
|
+
- Isolate live queue widgets per session, honor Web cleanup refusals, retain retryable cleanup, and batch delete/archive requests within Web's 500-session limit.
|
|
7
|
+
- Explain cancelled or unsupported session navigation and add native/RPC/Web compatibility regressions.
|
|
8
|
+
|
|
3
9
|
## 1.5.0
|
|
4
10
|
|
|
5
11
|
- First standalone public npm package, `@stacknyu/pi-flow`, maintained in the Stack Pi Plugins monorepo. pi-check remains optional and is not installed as a dependency.
|
package/extension.ts
CHANGED
|
@@ -191,30 +191,6 @@ function liveLines(cwd: string): string[] {
|
|
|
191
191
|
return [...lines, ...log];
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
const g = globalThis as { __flowWatch?: ReturnType<typeof setInterval> };
|
|
195
|
-
|
|
196
|
-
function stopWatch(ctx?: any) {
|
|
197
|
-
if (g.__flowWatch) clearInterval(g.__flowWatch);
|
|
198
|
-
g.__flowWatch = undefined;
|
|
199
|
-
try {
|
|
200
|
-
ctx?.ui.setWidget("queue", undefined);
|
|
201
|
-
} catch {}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function startWatch(ctx: any) {
|
|
205
|
-
stopWatch();
|
|
206
|
-
const cwd = ctx.cwd;
|
|
207
|
-
const tick = () => {
|
|
208
|
-
try {
|
|
209
|
-
ctx.ui.setWidget("queue", liveLines(cwd));
|
|
210
|
-
} catch {
|
|
211
|
-
stopWatch();
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
tick();
|
|
215
|
-
g.__flowWatch = setInterval(tick, 4000);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
194
|
/** Start the background runner. Returns an error text, or "" when it started (or already ran to the end). */
|
|
219
195
|
async function startRunner(cwd: string, resume = false): Promise<string> {
|
|
220
196
|
await ensureDir(cwd);
|
|
@@ -433,7 +409,6 @@ async function handoff(cwd: string, slug: string, ctx: any): Promise<{ ok: boole
|
|
|
433
409
|
const web = await piWeb(cwd);
|
|
434
410
|
if (web) await web.ui({ action: "set", ids: [sid], pinned: true }).catch(() => {});
|
|
435
411
|
}
|
|
436
|
-
if (ctx?.ui) startWatch(ctx);
|
|
437
412
|
return {
|
|
438
413
|
ok: true,
|
|
439
414
|
text: `Queued ${items.length} items for task ${slug} (up to ${parallel} in parallel, adaptive). Checks: ${checks.join(" · ")}.${warnings.length ? `\nWarnings: ${warnings.join(" ")}` : ""}\nThe live panel below shows progress; /task status for a summary.`,
|
|
@@ -472,6 +447,7 @@ async function cleanup(cwd: string, all: boolean, currentSession: string): Promi
|
|
|
472
447
|
for (const t of tasks) {
|
|
473
448
|
const ids = [...new Set([...(t.planningSessions ?? []), ...(fs[t.slug]?.planningSessions ?? [])])];
|
|
474
449
|
const files: Record<string, string> = fs[t.slug]?.planningFiles ?? {};
|
|
450
|
+
let pending = false;
|
|
475
451
|
for (const id of ids) {
|
|
476
452
|
if (!id) continue;
|
|
477
453
|
if (id === currentSession) {
|
|
@@ -481,12 +457,14 @@ async function cleanup(cwd: string, all: boolean, currentSession: string): Promi
|
|
|
481
457
|
}
|
|
482
458
|
let gone = false;
|
|
483
459
|
if (web) gone = await web.del([id]).then((r: any) => (r.deleted ?? []).includes(id) || (r.missing ?? []).includes(id), () => false);
|
|
484
|
-
if (!
|
|
460
|
+
if (!web && files[id]) {
|
|
485
461
|
rmSync(files[id], { force: true });
|
|
486
462
|
gone = true;
|
|
487
463
|
}
|
|
488
464
|
if (gone) planned++;
|
|
465
|
+
else { pending = true; notes.push(`kept planning chat ${id}: deletion was not confirmed`); }
|
|
489
466
|
}
|
|
467
|
+
if (!pending) setFlowStatus(cwd, t.slug, { status: taskStatus(cwd, t), cleanedAt: new Date().toISOString() });
|
|
490
468
|
}
|
|
491
469
|
if (planned) notes.push(`deleted ${planned} planning chat(s)`);
|
|
492
470
|
if (keptCurrent) notes.push("kept this chat (you are in it); delete it from the sidebar or /resume list when you leave it");
|
|
@@ -507,7 +485,6 @@ async function cleanup(cwd: string, all: boolean, currentSession: string): Promi
|
|
|
507
485
|
notes.push(`removing ${wts.length} worktrees in the background (the next task reinstalls them)`);
|
|
508
486
|
}
|
|
509
487
|
}
|
|
510
|
-
for (const t of tasks) setFlowStatus(cwd, t.slug, { status: taskStatus(cwd, t), cleanedAt: new Date().toISOString() });
|
|
511
488
|
return notes.join("\n");
|
|
512
489
|
}
|
|
513
490
|
|
|
@@ -603,7 +580,30 @@ export { cleanup, handoff, planPrompt, control, startRunner };
|
|
|
603
580
|
// ---------- the extension ----------
|
|
604
581
|
|
|
605
582
|
export default function (pi: ExtensionAPI) {
|
|
606
|
-
|
|
583
|
+
let watch: ReturnType<typeof setInterval> | undefined;
|
|
584
|
+
function stopWatch(ctx?: any) {
|
|
585
|
+
clearInterval(watch);
|
|
586
|
+
watch = undefined;
|
|
587
|
+
try {
|
|
588
|
+
ctx?.ui.setWidget("queue", undefined);
|
|
589
|
+
} catch {}
|
|
590
|
+
}
|
|
591
|
+
function startWatch(ctx: any) {
|
|
592
|
+
stopWatch();
|
|
593
|
+
if (!ctx.hasUI) return;
|
|
594
|
+
const cwd = ctx.cwd;
|
|
595
|
+
const tick = () => {
|
|
596
|
+
try {
|
|
597
|
+
ctx.ui.setWidget("queue", liveLines(cwd));
|
|
598
|
+
} catch {
|
|
599
|
+
stopWatch();
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
tick();
|
|
603
|
+
watch = setInterval(tick, 4000);
|
|
604
|
+
watch.unref?.();
|
|
605
|
+
}
|
|
606
|
+
pi.on("session_shutdown", async (_event, ctx) => stopWatch(ctx));
|
|
607
607
|
|
|
608
608
|
pi.registerTool({
|
|
609
609
|
name: "task_handoff",
|
|
@@ -616,6 +616,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
616
616
|
const blocked = guard(ctx);
|
|
617
617
|
if (blocked) return blocked;
|
|
618
618
|
const r = await handoff(ctx.cwd, String(params.slug).trim(), ctx);
|
|
619
|
+
if (r.ok) startWatch(ctx);
|
|
619
620
|
return { content: [{ type: "text", text: r.text }], details: { ok: r.ok } };
|
|
620
621
|
},
|
|
621
622
|
} as any);
|
|
@@ -847,7 +848,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
847
848
|
return;
|
|
848
849
|
}
|
|
849
850
|
stopWatch(ctx);
|
|
850
|
-
await ctx.switchSession(file);
|
|
851
|
+
const switched = await ctx.switchSession(file);
|
|
852
|
+
if (switched.cancelled) ctx.ui.notify(`queue: session switch cancelled or unsupported by this host. Open it from the session list, or run pi --session ${JSON.stringify(file)} in a terminal.`, "info");
|
|
851
853
|
return;
|
|
852
854
|
}
|
|
853
855
|
if (cmd === "problem") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacknyu/pi-flow",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Plan a large task, answer its questions, and let a queue of fresh Pi sessions build it — with verified checks, adaptive parallelism and session cleanup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/runner.mjs
CHANGED
|
@@ -637,9 +637,32 @@ export function webClient(c) {
|
|
|
637
637
|
};
|
|
638
638
|
return {
|
|
639
639
|
available: () => call("GET", "/api/agent/running", undefined, 3000).then(() => true, () => false),
|
|
640
|
-
running: () =>
|
|
640
|
+
running: async (id) => {
|
|
641
|
+
const snap = await call("GET", "/api/agent/running", undefined, 15_000);
|
|
642
|
+
const active = [...(snap.runningSessionIds ?? []), ...(snap.subagentsActiveSessionIds ?? [])];
|
|
643
|
+
if (id && !active.includes(id) && active.length) {
|
|
644
|
+
const { sessions } = await call("GET", "/api/sessions?summary=1");
|
|
645
|
+
const family = new Set([id]);
|
|
646
|
+
for (let grew = true; grew;) {
|
|
647
|
+
grew = false;
|
|
648
|
+
for (const s of sessions) if (s.relation?.kind === "subagent" && family.has(s.relation.parentSessionId) && !family.has(s.id)) {
|
|
649
|
+
family.add(s.id); grew = true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (active.some((sid) => family.has(sid))) snap.subagentsActiveSessionIds = [...(snap.subagentsActiveSessionIds ?? []), id];
|
|
653
|
+
}
|
|
654
|
+
return snap;
|
|
655
|
+
},
|
|
641
656
|
create: (body) => call("POST", "/api/agent/new", body, 300_000),
|
|
642
657
|
send: (id, body) => call("POST", `/api/agent/${encodeURIComponent(id)}`, body, 120_000),
|
|
658
|
+
history: async (id) => {
|
|
659
|
+
let before = "";
|
|
660
|
+
while (true) {
|
|
661
|
+
const response = await call("GET", `/api/sessions/${encodeURIComponent(id)}${before ? "/context" : ""}?tree=summary&tail=1&deferMedia=1&deferThinking=1${before ? `&before=${encodeURIComponent(before)}` : ""}`);
|
|
662
|
+
if (response.context.messages.some((m) => m.role === "assistant") || !response.context.hasMore) return response;
|
|
663
|
+
before = response.context.oldestEntryId;
|
|
664
|
+
}
|
|
665
|
+
},
|
|
643
666
|
del: (ids) => call("POST", "/api/sessions/delete", { ids }, 60_000),
|
|
644
667
|
ui: (body) => call("POST", "/api/sessions/ui-state", body, 30_000),
|
|
645
668
|
};
|
|
@@ -704,6 +727,7 @@ async function webTurn(ctx, cwd, ref, name, prompt, minutes, onActivity = () =>
|
|
|
704
727
|
const deadline = Date.now() + minutes * 60_000;
|
|
705
728
|
const [provider, ...rest] = (c.model || "").split("/");
|
|
706
729
|
const modelId = rest.join("/");
|
|
730
|
+
let previousAssistantId;
|
|
707
731
|
try {
|
|
708
732
|
if (!ref.id) {
|
|
709
733
|
const r = await web.create({
|
|
@@ -717,6 +741,8 @@ async function webTurn(ctx, cwd, ref, name, prompt, minutes, onActivity = () =>
|
|
|
717
741
|
ctx.onWorker?.({ sessionId: ref.id, web: true });
|
|
718
742
|
await web.send(ref.id, { type: "set_session_name", name }).catch(() => {});
|
|
719
743
|
}
|
|
744
|
+
const { context } = await web.history(ref.id);
|
|
745
|
+
previousAssistantId = context.entryIds[context.messages.findLastIndex((m) => m.role === "assistant")];
|
|
720
746
|
await web.send(ref.id, { type: "prompt", message: prompt });
|
|
721
747
|
} catch (e) {
|
|
722
748
|
return { status: "error", reply: "", err: String(e?.message ?? e), code: -1, quiescent: false };
|
|
@@ -731,13 +757,13 @@ async function webTurn(ctx, cwd, ref, name, prompt, minutes, onActivity = () =>
|
|
|
731
757
|
await new Promise((r) => setTimeout(r, 3000));
|
|
732
758
|
let snap;
|
|
733
759
|
try {
|
|
734
|
-
snap = await web.running();
|
|
760
|
+
snap = await web.running(ref.id);
|
|
735
761
|
} catch {
|
|
736
762
|
if (!stopRequested() && Date.now() <= deadline) continue;
|
|
737
763
|
await web.send(ref.id, { type: "abort" }).catch(() => {});
|
|
738
764
|
return { status: "error", reply: "", err: "Pi Web unreachable; worker termination is unconfirmed", code: -1, quiescent: false };
|
|
739
765
|
}
|
|
740
|
-
const running = (snap.runningSessionIds ?? []).includes(ref.id);
|
|
766
|
+
const running = [...(snap.runningSessionIds ?? []), ...(snap.subagentsActiveSessionIds ?? [])].includes(ref.id);
|
|
741
767
|
const awaiting = (snap.awaitingInputSessionIds ?? []).includes(ref.id);
|
|
742
768
|
if (!running) {
|
|
743
769
|
if (++notRunning >= 2) break;
|
|
@@ -772,8 +798,8 @@ async function webTurn(ctx, cwd, ref, name, prompt, minutes, onActivity = () =>
|
|
|
772
798
|
let quiescent = false;
|
|
773
799
|
for (let i = 0; i < 20; i++) {
|
|
774
800
|
await new Promise((r) => setTimeout(r, 3000));
|
|
775
|
-
const s2 = await web.running().catch(() => null);
|
|
776
|
-
if (s2 && !(s2.runningSessionIds ?? []).includes(ref.id)) { quiescent = true; break; }
|
|
801
|
+
const s2 = await web.running(ref.id).catch(() => null);
|
|
802
|
+
if (s2 && ![...(s2.runningSessionIds ?? []), ...(s2.subagentsActiveSessionIds ?? [])].includes(ref.id)) { quiescent = true; break; }
|
|
777
803
|
}
|
|
778
804
|
if (!quiescent) return { status: "error", reply: "", err: "worker did not confirm termination", code: -1, quiescent: false };
|
|
779
805
|
if (why === "error") return { status, reply: "", err: "the agent waited for a person's answer for 2 minutes", code: -1 };
|
|
@@ -781,10 +807,11 @@ async function webTurn(ctx, cwd, ref, name, prompt, minutes, onActivity = () =>
|
|
|
781
807
|
}
|
|
782
808
|
}
|
|
783
809
|
try {
|
|
784
|
-
const
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
810
|
+
const { context } = await web.history(ref.id);
|
|
811
|
+
const index = context.messages.findLastIndex((m) => m.role === "assistant");
|
|
812
|
+
const last = context.messages[index];
|
|
813
|
+
if (!last || context.entryIds[index] === previousAssistantId) return { status: "error", reply: "", err: "Pi Web returned no new assistant result", code: -1 };
|
|
814
|
+
const err = last.stopReason === "stop" ? "" : last.errorMessage || `assistant stopped: ${last.stopReason}`;
|
|
788
815
|
return { status: status === "ok" && err ? "error" : status, reply: textOf(last), err, code: err ? -1 : 0 };
|
|
789
816
|
} catch (e) { return { status: "error", reply: "", err: String(e), code: -1 }; }
|
|
790
817
|
}
|
|
@@ -979,16 +1006,17 @@ export async function deleteSessions(ctx, ids, mode = "delete") {
|
|
|
979
1006
|
const webIds = ids.filter((id) => sessions[id]?.web);
|
|
980
1007
|
const cliIds = ids.filter((id) => sessions[id] && !sessions[id].web);
|
|
981
1008
|
let n = 0;
|
|
982
|
-
|
|
1009
|
+
for (let i = 0; ctx.web && i < webIds.length; i += 500) {
|
|
1010
|
+
const batch = webIds.slice(i, i + 500);
|
|
983
1011
|
try {
|
|
984
1012
|
if (mode === "archive") {
|
|
985
|
-
await ctx.web.ui({ action: "set", ids:
|
|
986
|
-
for (const id of
|
|
987
|
-
n +=
|
|
1013
|
+
await ctx.web.ui({ action: "set", ids: batch, archived: true });
|
|
1014
|
+
for (const id of batch) sessions[id].archived = true;
|
|
1015
|
+
n += batch.length;
|
|
988
1016
|
} else {
|
|
989
|
-
const r = await ctx.web.del(
|
|
1017
|
+
const r = await ctx.web.del(batch);
|
|
990
1018
|
for (const id of [...(r.deleted ?? []), ...(r.missing ?? [])]) if (sessions[id]) sessions[id].deleted = true;
|
|
991
|
-
n += (r.deleted ?? []).filter((id) =>
|
|
1019
|
+
n += (r.deleted ?? []).filter((id) => batch.includes(id)).length;
|
|
992
1020
|
}
|
|
993
1021
|
} catch (e) {
|
|
994
1022
|
ctx.log(`CLEANUP failed: ${e?.message ?? e}`);
|