@pify/swarm 0.7.1 → 0.7.3

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 CHANGED
@@ -67,7 +67,7 @@ The catalog is the same `.pi/agents/*.md` one [`@pify/subagent`](https://github.
67
67
 
68
68
  ## A background run comes back to you
69
69
 
70
- `swarm_status` on a run still in flight used to say "still running", which left the model one option: ask again. The aggregated report is *sent* into the conversation when the run finishes — honestly: this delivery mechanism is the same one whose only live measurement in the suite (subagent's) currently fails under print mode's teardown, and swarm has no live test of its own yet, so treat delivery as unmeasured and `swarm_status` as the reliable path. Asking early returns a structured result carrying `retryable`, the elapsed time and `pollRequired: false` — a normal answer rather than an error, because a tool error over a condition only time resolves invites the model's retry machinery into a loop.
70
+ `swarm_status` on a run still in flight used to say "still running", which left the model one option: ask again. The aggregated report is **delivered** into the conversation when the run finishes — measured, not assumed: `test/live/delivery-wire.mjs` drives a real background swarm through pi, holds the session open the way an interactive one naturally stays open, and reads the report out of pi's own provider payload (3/3; the run finished and the report arrived unasked). One caveat the measurement taught: delivery is a property of sessions that outlive their runs — interactive sessions do, `pi -p` does not. Asking early returns a structured result carrying `retryable`, the elapsed time and `pollRequired: false` — a normal answer rather than an error, because a tool error over a condition only time resolves invites the model's retry machinery into a loop.
71
71
 
72
72
  ## Command
73
73
 
@@ -25,6 +25,7 @@ import { Text } from "@earendil-works/pi-tui";
25
25
  import { Type } from "typebox";
26
26
 
27
27
  import { BUILTIN_AGENTS } from "../src/builtin.ts";
28
+ import { withUiLock } from "../src/ui-lock.ts";
28
29
  import {
29
30
  consentQuestion,
30
31
  decideConsent,
@@ -542,9 +543,11 @@ export default function swarm(pi: ExtensionAPI) {
542
543
  });
543
544
  if (verdict !== "ask") return verdict === "allow";
544
545
 
545
- const approved = await ctx.ui.confirm(
546
- "Load this project's agent definitions?",
547
- consentQuestion("its own agent definitions, which override the builtins of the same name", dir),
546
+ const approved = await withUiLock(() =>
547
+ ctx.ui.confirm(
548
+ "Load this project's agent definitions?",
549
+ consentQuestion("its own agent definitions, which override the builtins of the same name", dir),
550
+ ),
548
551
  );
549
552
  try {
550
553
  writeFileSync(file, `${JSON.stringify(writeConsent(store, ctx.cwd, "agents", approved), null, 2)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/swarm",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Coordinate multiple pi agents in parallel: swarm_run fan-out with per-item auto-routing, concurrency queue, aggregated reports",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/ui-lock.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * One dialog at a time, across every @pify extension in the process.
3
+ *
4
+ * pi's interactive host has no queue for extension dialogs: opening a second
5
+ * `ui.select`/`ui.confirm`/`ui.input` while one is already up displaces the
6
+ * first component without ever calling its cancel callback, so the first
7
+ * dialog's promise never settles — the turn hangs (measured in pi's
8
+ * interactive-mode source: showExtensionSelector clears and replaces the
9
+ * active selector, and only hideExtensionSelector disposes it). This is a
10
+ * real collision in this suite: ask-question opens a questionnaire while a
11
+ * consent prompt (memory/swarm/workflow/subagent) or a yolo confirm can fire
12
+ * in the same turn, and parallel tool calls make that ordinary.
13
+ *
14
+ * The fix is to never let a second dialog open until the first has closed.
15
+ * A timeout would not help — releasing the lock while the dialog is still on
16
+ * screen just re-creates the collision — so this is a plain FIFO mutex with
17
+ * no deadline. It is deliberately keyed on a cross-realm `Symbol.for`, so all
18
+ * eighteen packages share ONE queue even though each ships its own copy of
19
+ * this file: the state lives on `globalThis`, not in any one module.
20
+ *
21
+ * Vendored per package, like consent.ts, so the suite keeps zero runtime
22
+ * dependencies.
23
+ */
24
+
25
+ const LOCK_KEY = Symbol.for("pify.ui-lock.tail");
26
+
27
+ interface LockGlobal {
28
+ [LOCK_KEY]?: Promise<unknown>;
29
+ }
30
+
31
+ /**
32
+ * Run `fn` only once every dialog queued before it has finished, and hold the
33
+ * queue until `fn` settles. A rejection in an earlier holder never wedges the
34
+ * chain — the next waiter proceeds regardless.
35
+ */
36
+ export async function withUiLock<T>(fn: () => Promise<T>): Promise<T> {
37
+ const g = globalThis as unknown as LockGlobal;
38
+ const prev = g[LOCK_KEY] ?? Promise.resolve();
39
+
40
+ let release!: () => void;
41
+ const mine = new Promise<void>((resolve) => {
42
+ release = resolve;
43
+ });
44
+ g[LOCK_KEY] = prev.then(() => mine);
45
+
46
+ await prev.catch(() => {});
47
+ try {
48
+ return await fn();
49
+ } finally {
50
+ release();
51
+ }
52
+ }