pi-squire 0.7.0 โ†’ 0.8.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 CHANGED
@@ -6,6 +6,34 @@ follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.1] - 2026-10-20
10
+
11
+ ### Fixed
12
+
13
+ - A 429 whose text says the endpoint is unavailable, such as OpenCode Go's
14
+ `429: Endpoint is unavailable`, no longer marks the provider as
15
+ usage-limited. It is an outage, not a used-up quota, so `/delegate` keeps
16
+ launching on the provider instead of refusing it for five minutes or more.
17
+ Pi has already retried the request by then. A running delegate that hits
18
+ it moves to its next fallback model and carries on, as it does on a usage
19
+ limit.
20
+
21
+ ## [0.8.0] - 2026-10-20
22
+
23
+ ### Added
24
+
25
+ - A delegate's footer shows that it is a delegate and whether auto-exit is
26
+ on: `๐Ÿ‘ฆ delegate ยท auto-exit on`, or
27
+ `๐Ÿ‘ฆ delegate ยท auto-exit off`. It follows the
28
+ setting as typing turns it off and `/auto-exit` turns it back on.
29
+
30
+ ### Changed
31
+
32
+ - The parent's footer reads `๐Ÿ‘จ N delegates running` instead of
33
+ `โ‡„ N running`. Each side picks its icon at random per session: ๐Ÿ‘จ or ๐Ÿ‘ฉ
34
+ for the parent, ๐Ÿ‘ฆ or ๐Ÿ‘ง for a delegate. The footer tests in
35
+ `test/delegate.test.ts` change to match.
36
+
9
37
  ## [0.7.0] - 2026-10-19
10
38
 
11
39
  ### Added
@@ -257,7 +285,9 @@ author's dotfiles; the changes below are against that copy.
257
285
 
258
286
  - The published file list includes `child.ts`, which `index.ts` imports.
259
287
 
260
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...HEAD
288
+ [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.8.1...HEAD
289
+ [0.8.1]: https://github.com/gvanderclay/pi-squire/compare/v0.8.0...v0.8.1
290
+ [0.8.0]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...v0.8.0
261
291
  [0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
262
292
  [0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
263
293
  [0.5.1]: https://github.com/gvanderclay/pi-squire/compare/v0.5.0...v0.5.1
package/README.md CHANGED
@@ -393,6 +393,10 @@ The parent resolves each delegation's auto-exit from the call
393
393
  notice: typed or RPC input, or a run the user stopped (Esc).
394
394
  - `/auto-exit` inside the delegate turns it on again for the next normal
395
395
  completion, whatever the launch said.
396
+ - The footer shows that this is a delegate and whether auto-exit is on:
397
+ `๐Ÿ‘ฆ delegate ยท auto-exit on`, or
398
+ `๐Ÿ‘ฆ delegate ยท auto-exit off`, with ๐Ÿ‘ฆ or ๐Ÿ‘ง
399
+ picked at random per session.
396
400
 
397
401
  There is no `subagent_done` tool: auto-exit off only means the window stays.
398
402
 
@@ -439,8 +443,9 @@ Delegate session: <session file or id>
439
443
  current tool calls, before the next model call. A follow-up would wait
440
444
  until the parent had answered, by which time it had often read the
441
445
  envelope itself, and the late result cost a second, redundant turn.
442
- - The footer counts the running delegations (`โ‡„ N running`), clears one entry
443
- per result or close and hides itself at zero.
446
+ - The footer counts the running delegations (`๐Ÿ‘จ N delegates running`, with ๐Ÿ‘จ or ๐Ÿ‘ฉ
447
+ picked at random per session), clears one entry per result or close and
448
+ hides itself at zero.
444
449
  - Each result and each close is recorded in the parent session, and the
445
450
  records are rebuilt synchronously at session start. A resume still
446
451
  recognises replies, including one that arrived while the parent was
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Starts a delegate Pi session in a background tmux window, sends it the task over message:*, and shows the parent its result",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/child.ts CHANGED
@@ -21,6 +21,7 @@
21
21
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
22
 
23
23
  import { activeMark, classify, type Response, recordLimit } from "./limits.ts";
24
+ import { STATUS_KEY } from "./results.ts";
24
25
 
25
26
  /** The launch variable that carries the parent's resolved setting. */
26
27
  export const AUTO_EXIT_ENV = "PI_DELEGATE_AUTO_EXIT";
@@ -83,39 +84,68 @@ function registerTakeover(pi: ExtensionAPI, parent: string): void {
83
84
  }
84
85
 
85
86
  /**
86
- * A usage limit moves the run to the next fallback that is usable and not
87
- * marked, and continues it in this session; each fallback is tried once.
88
- * Returns the setter for the model whose usage limit ended the last run.
87
+ * A usage limit or an unavailable endpoint moves the run to the next fallback
88
+ * that is usable and not marked, and continues it in this session; each
89
+ * fallback is tried once. Returns the setter for what ended the last run, as
90
+ * `<provider/model> <what happened>`.
89
91
  */
90
- function registerFallback(pi: ExtensionAPI): (model: string | undefined) => void {
92
+ function registerFallback(pi: ExtensionAPI): (failure: string | undefined) => void {
91
93
  /** The models still to move to on a usage limit, in order. */
92
94
  const fallback = (process.env[FALLBACK_ENV] ?? "").split(",").filter((model) => model.includes("/"));
93
- let limited: string | undefined;
95
+ let failure: string | undefined;
94
96
  pi.on("agent_before_settle", async (_event, ctx) => {
95
- const from = limited;
96
- limited = undefined;
97
+ const from = failure;
98
+ failure = undefined;
97
99
  const next = from === undefined ? undefined : await switchModel(pi, ctx, fallback);
98
100
  if (next === undefined) return;
99
- const content = `[delegate] ${from} hit a usage limit, so this session switched to ${next}. Carry on with the task from where it stopped.`;
101
+ const content = `[delegate] ${from}, so this session switched to ${next}. Carry on with the task from where it stopped.`;
100
102
  return { continue: true, entries: [{ type: "custom_message", customType: "delegate", content, display: true }] };
101
103
  });
102
- return (model) => {
103
- limited = model;
104
+ return (value) => {
105
+ failure = value;
104
106
  };
105
107
  }
106
108
 
109
+ /** Why a failed run should move to a fallback, as `<provider/model> <what happened>`; undefined when it should not. */
110
+ function fallbackReason(last: Message | undefined): string | undefined {
111
+ const error = last?.errorMessage;
112
+ if (typeof error !== "string") return undefined;
113
+ const model = `${String(last?.provider)}/${String(last?.model)}`;
114
+ if (classify(error) !== undefined) return `${model} hit a usage limit`;
115
+ // Pi has already retried an unavailable endpoint; it marks nothing, but this run moves on.
116
+ if (/unavailable/i.test(error)) return `${model} was unavailable (${error})`;
117
+ return undefined;
118
+ }
119
+
120
+ /** This delegate's footer icon, picked once per load. */
121
+ const ICON = Math.random() < 0.5 ? "๐Ÿ‘ฆ" : "๐Ÿ‘ง";
122
+
123
+ function showStatus(ctx: ExtensionContext, armed: boolean): void {
124
+ const { theme } = ctx.ui;
125
+ ctx.ui.setStatus(
126
+ STATUS_KEY,
127
+ armed
128
+ ? `${ICON} delegate ยท auto-exit ${theme.fg("success", "on")}`
129
+ : `${ICON} delegate ยท auto-exit ${theme.fg("dim", "off")}`,
130
+ );
131
+ }
132
+
107
133
  export function registerChild(pi: ExtensionAPI, parent: string): void {
108
134
  registerTakeover(pi, parent);
109
- const setLimited = registerFallback(pi);
135
+ const setFailure = registerFallback(pi);
110
136
 
111
137
  let armed = process.env[AUTO_EXIT_ENV] === "1";
112
138
  /** Whether the run that just ended finished without the user stopping it. */
113
139
  let completed = false;
114
140
 
141
+ // Reload and session replacement clear extension statuses, then start again.
142
+ pi.on("session_start", async (_event, ctx) => showStatus(ctx, armed));
143
+
115
144
  function disarm(ctx: ExtensionContext): void {
116
145
  if (!armed) return;
117
146
  armed = false;
118
147
  ctx.ui.notify(OFF_NOTICE, "info");
148
+ showStatus(ctx, armed);
119
149
  }
120
150
 
121
151
  // Typed text and RPC input are the user's; a prompt an extension sends is not.
@@ -144,11 +174,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
144
174
  const stopped = last?.stopReason === "aborted" || ctx.signal?.aborted === true;
145
175
  if (stopped) disarm(ctx);
146
176
  if (!stopped && last?.stopReason === "error") recordUsageLimit(last, ctx, response);
147
- setLimited(
148
- !stopped && typeof last?.errorMessage === "string" && classify(last.errorMessage) !== undefined
149
- ? `${String(last.provider)}/${String(last.model)}`
150
- : undefined,
151
- );
177
+ setFailure(stopped ? undefined : fallbackReason(last));
152
178
  // A failure ends the work like a completion: the parent gets the error, the session file stays.
153
179
  completed = !stopped;
154
180
  });
@@ -166,6 +192,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
166
192
  handler: async (_args, ctx) => {
167
193
  armed = true;
168
194
  ctx.ui.notify(ON_NOTICE, "info");
195
+ showStatus(ctx, armed);
169
196
  },
170
197
  });
171
198
  }
package/src/limits.ts CHANGED
@@ -145,10 +145,12 @@ export function headerReset(response: Response | undefined, now = Date.now()): n
145
145
 
146
146
  /**
147
147
  * Whether an error message (Pi's `<status>: <body>`) is a usage limit, and
148
- * the reset time the text states, if any. Overloaded and 5xx errors never are.
148
+ * the reset time the text states, if any. Overloaded, unavailable and 5xx
149
+ * errors never are: OpenCode Go sends "429: Endpoint is unavailable" for an
150
+ * outage, with quota left.
149
151
  */
150
152
  export function classify(message: string, now = Date.now()): { resetAt?: number } | undefined {
151
- if (/overloaded/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
153
+ if (/overloaded|unavailable/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
152
154
  if (!LIMIT_PATTERN.test(message) && !/^\s*429\b/.test(message)) return undefined;
153
155
  const resetAt = statedReset(message, now);
154
156
  return resetAt !== undefined && resetAt > now ? { resetAt } : {};
package/src/results.ts CHANGED
@@ -237,6 +237,7 @@ export function createResults(pi: ExtensionAPI): Results {
237
237
  let ctx: ExtensionContext | undefined;
238
238
  /** Whether the footer currently shows the entry, so a clear is sent only when needed. */
239
239
  let footerShown = false;
240
+ const icon = Math.random() < 0.5 ? "๐Ÿ‘จ" : "๐Ÿ‘ฉ";
240
241
 
241
242
  /** A delegation still going: no result and no recorded close. */
242
243
  const running = (): Recorded[] =>
@@ -251,7 +252,7 @@ export function createResults(pi: ExtensionAPI): Results {
251
252
  const visible = count > 0;
252
253
  if (!visible && !footerShown) return;
253
254
  footerShown = visible;
254
- ctx.ui.setStatus(STATUS_KEY, visible ? `โ‡„ ${count} running` : undefined);
255
+ ctx.ui.setStatus(STATUS_KEY, visible ? `${icon} ${count} delegate${count === 1 ? "" : "s"} running` : undefined);
255
256
  }
256
257
 
257
258
  /** Rebuild the records from the session's entries, so a resume still recognises replies. */