@ryan_nookpi/pi-extension-subagent 0.5.12 → 0.5.13

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
@@ -41,7 +41,7 @@ Equivalent AI tool call:
41
41
  { "command": "subagent run worker --isolated -- implement the requested change and run tests" }
42
42
  ```
43
43
 
44
- Tool launches are asynchronous. Wait for the automatic completion or failure follow-up instead of polling immediately.
44
+ Tool launches are asynchronous. Wait for the automatic completion or failure follow-up instead of polling immediately. Pi drops queued messages when you press Escape; if a completion was still queued when the run stops, it is added to the session without starting a new turn, and the agent sees it with your next message.
45
45
 
46
46
  ## Agent definitions
47
47
 
@@ -0,0 +1,103 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ type SendMessage = ExtensionAPI["sendMessage"];
4
+ type Message = Parameters<SendMessage>[0];
5
+
6
+ const GUARD_ID_KEY = "deliveryGuardId";
7
+
8
+ function guardId(message: unknown): string | undefined {
9
+ const details = (message as { details?: unknown } | undefined)?.details;
10
+ if (!details || typeof details !== "object") return undefined;
11
+ const id = (details as Record<string, unknown>)[GUARD_ID_KEY];
12
+ return typeof id === "string" ? id : undefined;
13
+ }
14
+
15
+ /**
16
+ * Pi keeps steer and follow-up messages in an in-memory queue until the agent loop picks them up, and
17
+ * Escape clears that queue without restoring custom messages. A completion queued while the agent was
18
+ * busy would vanish. This tracks every turn-triggering message queued during a run, and when the run
19
+ * settles without the message reaching the model, appends it to the session without starting a turn:
20
+ * the user sees it now, and the model sees it on the next prompt.
21
+ *
22
+ * Extensions cannot tell Escape from other aborts: Pi's pending count covers only typed messages, not
23
+ * extension messages. An abort while the model streams keeps the original queued, and Pi delivers it on
24
+ * the next run after the restored copy. Both carry the same guard id, so the model input keeps only the first.
25
+ */
26
+ export function guardQueuedDeliveries(pi: ExtensionAPI): ExtensionAPI {
27
+ let context: ExtensionContext | undefined;
28
+ let nextId = 0;
29
+ const queued = new Map<string, Message>();
30
+
31
+ const busy = () => {
32
+ try {
33
+ return context ? !context.isIdle() : false;
34
+ } catch {
35
+ return false;
36
+ }
37
+ };
38
+
39
+ const restored = new Set<string>();
40
+
41
+ pi.on("agent_start", (_event, ctx) => {
42
+ context = ctx;
43
+ });
44
+ pi.on("message_end", (event) => {
45
+ const id = guardId(event.message);
46
+ if (id) queued.delete(id);
47
+ });
48
+ pi.on("agent_settled", (_event, ctx) => {
49
+ context = ctx;
50
+ if (queued.size === 0) return;
51
+ const lost = [...queued.entries()];
52
+ queued.clear();
53
+ for (const [id, message] of lost) {
54
+ restored.add(id);
55
+ pi.sendMessage(message, { triggerTurn: false });
56
+ }
57
+ });
58
+ pi.on("context", (event) => {
59
+ if (restored.size === 0) return;
60
+ const seen = new Set<string>();
61
+ let dropped = false;
62
+ const messages = event.messages.filter((message) => {
63
+ const id = guardId(message);
64
+ if (!id || !restored.has(id)) return true;
65
+ if (!seen.has(id)) {
66
+ seen.add(id);
67
+ return true;
68
+ }
69
+ dropped = true;
70
+ return false;
71
+ });
72
+ return dropped ? { messages } : undefined;
73
+ });
74
+ pi.on("session_shutdown", () => {
75
+ queued.clear();
76
+ restored.clear();
77
+ context = undefined;
78
+ });
79
+
80
+ return new Proxy(pi, {
81
+ get(target, property, receiver) {
82
+ if (property !== "sendMessage") return Reflect.get(target, property, receiver);
83
+ const send: SendMessage = (message, options) => {
84
+ // An idle send starts its own run with the message as the prompt, so nothing can drop it.
85
+ if (options?.triggerTurn !== true || options.deliverAs === "nextTurn" || !busy()) {
86
+ target.sendMessage(message, options);
87
+ return;
88
+ }
89
+ const id = String(++nextId);
90
+ const details = message.details && typeof message.details === "object" ? message.details : {};
91
+ const tagged = { ...message, details: { ...details, [GUARD_ID_KEY]: id } } as Message;
92
+ queued.set(id, tagged);
93
+ try {
94
+ target.sendMessage(tagged, options);
95
+ } catch (error) {
96
+ queued.delete(id);
97
+ throw error;
98
+ }
99
+ };
100
+ return send;
101
+ },
102
+ });
103
+ }
package/index.ts CHANGED
@@ -30,6 +30,7 @@
30
30
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
31
31
  import { SubagentAsyncTasks } from "./async-task-lifecycle.js";
32
32
  import { HANG_CHECK_INTERVAL_MS } from "./constants.js";
33
+ import { guardQueuedDeliveries } from "./delivery-guard.js";
33
34
  import { SUBAGENT_COMMANDS, SUBAGENT_SHORTCUTS, type SubagentCommandName } from "./registration-manifest.js";
34
35
  import { handleRunningReminderContext, ReminderAnchors } from "./running-reminder.js";
35
36
 
@@ -41,7 +42,9 @@ interface SubagentCore {
41
42
  lifecycle: typeof import("./lifecycle.js");
42
43
  }
43
44
 
44
- export default function (pi: ExtensionAPI) {
45
+ export default function (host: ExtensionAPI) {
46
+ // Completions are queued while the agent is busy; the guard restores any the queue drops on Escape.
47
+ const pi = guardQueuedDeliveries(host);
45
48
  const asyncTasks = new SubagentAsyncTasks(pi);
46
49
  let core: SubagentCore | null = null;
47
50
  let corePromise: Promise<SubagentCore> | null = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,6 +32,7 @@
32
32
  "config.ts",
33
33
  "constants.ts",
34
34
  "context-limits.ts",
35
+ "delivery-guard.ts",
35
36
  "diagnostics.ts",
36
37
  "display-task.ts",
37
38
  "escalation.ts",