@clanker-code/pi-subagents 0.10.7 → 0.10.8

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 CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.8] - 2026-06-23
11
+
12
+ ### Changed
13
+ - **`get_subagent_result` wait:true now detects queued user messages** — when the parent session has user messages waiting (e.g. the user typed while waiting), the tool returns early with a `pending_message` outcome instead of blocking for the full wait timeout. The queued message is delivered to the parent LLM immediately. Uses `ctx.hasPendingMessages()` polling during the wait. The subagent continues running undisturbed.
14
+
10
15
  ## [0.10.7] - 2026-06-23
11
16
 
12
17
  ### Fixed
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ import { AgentWidget, buildInvocationTags, describeActivity, formatContextWindow
42
42
  import { menuSelect } from "./ui/menu-select.js";
43
43
  import { showSchedulesMenu } from "./ui/schedule-menu.js";
44
44
  import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
45
- import { formatWaitTimeout, raceWait, waitTimeoutMessage } from "./wait.js";
45
+ import { formatWaitTimeout, pollPendingMessages, raceWait, waitTimeoutMessage } from "./wait.js";
46
46
  // ---- Shared helpers ----
47
47
  /** Tool execute return value for a text response. */
48
48
  function textResult(msg, details) {
@@ -928,7 +928,7 @@ export default function (pi) {
928
928
  description: "Return a lightweight tail/filter view of the agent's result or live output file, with line numbers. Ignored when verbose is true.",
929
929
  })),
930
930
  }),
931
- execute: async (_toolCallId, params, signal, _onUpdate, _ctx) => {
931
+ execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
932
932
  const record = manager.getRecord(params.agent_id);
933
933
  if (!record) {
934
934
  return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
@@ -946,7 +946,18 @@ export default function (pi) {
946
946
  let waitOutcome = "completed";
947
947
  if (params.wait && record.status === "running" && record.promise) {
948
948
  cancelNudge(params.agent_id);
949
- waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds());
949
+ // Poll for queued user messages so we can return early and let the
950
+ // parent LLM process them immediately instead of blocking for the
951
+ // full wait timeout.
952
+ const pending = typeof ctx?.hasPendingMessages === "function"
953
+ ? pollPendingMessages(() => ctx.hasPendingMessages())
954
+ : undefined;
955
+ try {
956
+ waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds(), pending?.promise);
957
+ }
958
+ finally {
959
+ pending?.cancel();
960
+ }
950
961
  if (waitOutcome === "completed") {
951
962
  record.resultConsumed = true;
952
963
  }
package/dist/wait.d.ts CHANGED
@@ -1,10 +1,24 @@
1
- export type WaitOutcome = "completed" | "timeout" | "aborted";
1
+ export type WaitOutcome = "completed" | "timeout" | "aborted" | "pending_message";
2
2
  /** Human-readable "Xm Ys" for a duration in seconds. */
3
3
  export declare function formatWaitTimeout(seconds: number): string;
4
4
  /**
5
- * Race an agent completion promise against the configured wait timeout and the
6
- * parent abort signal. The subagent is never aborted here.
5
+ * Race an agent completion promise against the configured wait timeout, the
6
+ * parent abort signal, and an optional pending-message check. The subagent is
7
+ * never aborted here.
8
+ *
9
+ * @param pendingCheck - Optional promise that resolves when the parent session
10
+ * has queued user messages waiting to be delivered. When it resolves, the
11
+ * wait ends early so the parent turn can process the incoming message.
7
12
  */
8
- export declare function raceWait(promise: Promise<string>, signal: AbortSignal | undefined, timeoutSeconds: number): Promise<WaitOutcome>;
13
+ export declare function raceWait(promise: Promise<string>, signal: AbortSignal | undefined, timeoutSeconds: number, pendingCheck?: Promise<void>): Promise<WaitOutcome>;
9
14
  /** Message returned when a wait ends with the agent still running. */
10
15
  export declare function waitTimeoutMessage(outcome: WaitOutcome, timeoutSeconds: number): string;
16
+ /**
17
+ * Create a promise that resolves when the parent session has queued user
18
+ * messages. Polls at the given interval until `hasPendingMessages()` returns
19
+ * true. The caller should race this against the agent completion / timeout.
20
+ */
21
+ export declare function pollPendingMessages(hasPendingMessages: () => boolean, intervalMs?: number): {
22
+ promise: Promise<void>;
23
+ cancel: () => void;
24
+ };
package/dist/wait.js CHANGED
@@ -5,10 +5,15 @@ export function formatWaitTimeout(seconds) {
5
5
  return m > 0 ? `${m}m${s > 0 ? ` ${s}s` : ""}` : `${s}s`;
6
6
  }
7
7
  /**
8
- * Race an agent completion promise against the configured wait timeout and the
9
- * parent abort signal. The subagent is never aborted here.
8
+ * Race an agent completion promise against the configured wait timeout, the
9
+ * parent abort signal, and an optional pending-message check. The subagent is
10
+ * never aborted here.
11
+ *
12
+ * @param pendingCheck - Optional promise that resolves when the parent session
13
+ * has queued user messages waiting to be delivered. When it resolves, the
14
+ * wait ends early so the parent turn can process the incoming message.
10
15
  */
11
- export function raceWait(promise, signal, timeoutSeconds) {
16
+ export function raceWait(promise, signal, timeoutSeconds, pendingCheck) {
12
17
  return new Promise((resolve) => {
13
18
  let settled = false;
14
19
  const finish = (outcome) => {
@@ -23,6 +28,7 @@ export function raceWait(promise, signal, timeoutSeconds) {
23
28
  const onAbort = () => finish("aborted");
24
29
  signal?.addEventListener("abort", onAbort, { once: true });
25
30
  promise.then(() => finish("completed"));
31
+ pendingCheck?.then(() => finish("pending_message"));
26
32
  });
27
33
  }
28
34
  /** Message returned when a wait ends with the agent still running. */
@@ -33,5 +39,44 @@ export function waitTimeoutMessage(outcome, timeoutSeconds) {
33
39
  if (outcome === "aborted") {
34
40
  return `Agent is still running. The wait was cancelled by the user (parent turn aborted). The subagent was NOT stopped — it continues in the background.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
35
41
  }
42
+ if (outcome === "pending_message") {
43
+ return `Agent is still running. The wait was interrupted by an incoming user message. The subagent was NOT stopped — it continues in the background.\nThe queued message will be delivered after this tool returns.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
44
+ }
36
45
  return "Agent is still running. Use peek to check recent progress, wait: true to block until it finishes, or check back later.";
37
46
  }
47
+ /**
48
+ * Create a promise that resolves when the parent session has queued user
49
+ * messages. Polls at the given interval until `hasPendingMessages()` returns
50
+ * true. The caller should race this against the agent completion / timeout.
51
+ */
52
+ export function pollPendingMessages(hasPendingMessages, intervalMs = 1000) {
53
+ let settled = false;
54
+ let resolve;
55
+ const promise = new Promise((r) => { resolve = r; });
56
+ // Check immediately in case a message arrived between the tool call
57
+ // start and this poll setup.
58
+ if (hasPendingMessages()) {
59
+ settled = true;
60
+ resolve();
61
+ return { promise, cancel: () => { } };
62
+ }
63
+ const timer = setInterval(() => {
64
+ if (settled)
65
+ return;
66
+ if (hasPendingMessages()) {
67
+ settled = true;
68
+ clearInterval(timer);
69
+ resolve();
70
+ }
71
+ }, intervalMs);
72
+ return {
73
+ promise,
74
+ cancel: () => {
75
+ if (!settled) {
76
+ settled = true;
77
+ clearInterval(timer);
78
+ resolve();
79
+ }
80
+ },
81
+ };
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clanker-code/pi-subagents",
3
- "version": "0.10.7",
3
+ "version": "0.10.8",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "clankercode",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -55,7 +55,7 @@ import type { WidgetAgentSnapshot, WidgetDisplayMode } from "./ui/agent-widget-t
55
55
  import { menuSelect } from "./ui/menu-select.js";
56
56
  import { showSchedulesMenu } from "./ui/schedule-menu.js";
57
57
  import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
58
- import { formatWaitTimeout, raceWait, type WaitOutcome, waitTimeoutMessage } from "./wait.js";
58
+ import { formatWaitTimeout, pollPendingMessages, raceWait, type WaitOutcome, waitTimeoutMessage } from "./wait.js";
59
59
 
60
60
  // ---- Shared helpers ----
61
61
 
@@ -1077,7 +1077,7 @@ export default function (pi: ExtensionAPI) {
1077
1077
  }),
1078
1078
  ),
1079
1079
  }),
1080
- execute: async (_toolCallId, params, signal, _onUpdate, _ctx) => {
1080
+ execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
1081
1081
  const record = manager.getRecord(params.agent_id);
1082
1082
  if (!record) {
1083
1083
  return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
@@ -1099,7 +1099,17 @@ export default function (pi: ExtensionAPI) {
1099
1099
  let waitOutcome: WaitOutcome = "completed";
1100
1100
  if (params.wait && record.status === "running" && record.promise) {
1101
1101
  cancelNudge(params.agent_id);
1102
- waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds());
1102
+ // Poll for queued user messages so we can return early and let the
1103
+ // parent LLM process them immediately instead of blocking for the
1104
+ // full wait timeout.
1105
+ const pending = typeof ctx?.hasPendingMessages === "function"
1106
+ ? pollPendingMessages(() => ctx.hasPendingMessages())
1107
+ : undefined;
1108
+ try {
1109
+ waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds(), pending?.promise);
1110
+ } finally {
1111
+ pending?.cancel();
1112
+ }
1103
1113
  if (waitOutcome === "completed") {
1104
1114
  record.resultConsumed = true;
1105
1115
  }
package/src/wait.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type WaitOutcome = "completed" | "timeout" | "aborted";
1
+ export type WaitOutcome = "completed" | "timeout" | "aborted" | "pending_message";
2
2
 
3
3
  /** Human-readable "Xm Ys" for a duration in seconds. */
4
4
  export function formatWaitTimeout(seconds: number): string {
@@ -8,13 +8,19 @@ export function formatWaitTimeout(seconds: number): string {
8
8
  }
9
9
 
10
10
  /**
11
- * Race an agent completion promise against the configured wait timeout and the
12
- * parent abort signal. The subagent is never aborted here.
11
+ * Race an agent completion promise against the configured wait timeout, the
12
+ * parent abort signal, and an optional pending-message check. The subagent is
13
+ * never aborted here.
14
+ *
15
+ * @param pendingCheck - Optional promise that resolves when the parent session
16
+ * has queued user messages waiting to be delivered. When it resolves, the
17
+ * wait ends early so the parent turn can process the incoming message.
13
18
  */
14
19
  export function raceWait(
15
20
  promise: Promise<string>,
16
21
  signal: AbortSignal | undefined,
17
22
  timeoutSeconds: number,
23
+ pendingCheck?: Promise<void>,
18
24
  ): Promise<WaitOutcome> {
19
25
  return new Promise((resolve) => {
20
26
  let settled = false;
@@ -29,6 +35,7 @@ export function raceWait(
29
35
  const onAbort = () => finish("aborted");
30
36
  signal?.addEventListener("abort", onAbort, { once: true });
31
37
  promise.then(() => finish("completed"));
38
+ pendingCheck?.then(() => finish("pending_message"));
32
39
  });
33
40
  }
34
41
 
@@ -40,5 +47,50 @@ export function waitTimeoutMessage(outcome: WaitOutcome, timeoutSeconds: number)
40
47
  if (outcome === "aborted") {
41
48
  return `Agent is still running. The wait was cancelled by the user (parent turn aborted). The subagent was NOT stopped — it continues in the background.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
42
49
  }
50
+ if (outcome === "pending_message") {
51
+ return `Agent is still running. The wait was interrupted by an incoming user message. The subagent was NOT stopped — it continues in the background.\nThe queued message will be delivered after this tool returns.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
52
+ }
43
53
  return "Agent is still running. Use peek to check recent progress, wait: true to block until it finishes, or check back later.";
44
54
  }
55
+
56
+ /**
57
+ * Create a promise that resolves when the parent session has queued user
58
+ * messages. Polls at the given interval until `hasPendingMessages()` returns
59
+ * true. The caller should race this against the agent completion / timeout.
60
+ */
61
+ export function pollPendingMessages(
62
+ hasPendingMessages: () => boolean,
63
+ intervalMs = 1000,
64
+ ): { promise: Promise<void>; cancel: () => void } {
65
+ let settled = false;
66
+ let resolve!: () => void;
67
+ const promise = new Promise<void>((r) => { resolve = r; });
68
+
69
+ // Check immediately in case a message arrived between the tool call
70
+ // start and this poll setup.
71
+ if (hasPendingMessages()) {
72
+ settled = true;
73
+ resolve();
74
+ return { promise, cancel: () => {} };
75
+ }
76
+
77
+ const timer = setInterval(() => {
78
+ if (settled) return;
79
+ if (hasPendingMessages()) {
80
+ settled = true;
81
+ clearInterval(timer);
82
+ resolve();
83
+ }
84
+ }, intervalMs);
85
+
86
+ return {
87
+ promise,
88
+ cancel: () => {
89
+ if (!settled) {
90
+ settled = true;
91
+ clearInterval(timer);
92
+ resolve();
93
+ }
94
+ },
95
+ };
96
+ }