pi-squire 0.8.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,18 @@ 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
+
9
21
  ## [0.8.0] - 2026-10-20
10
22
 
11
23
  ### Added
@@ -273,7 +285,8 @@ author's dotfiles; the changes below are against that copy.
273
285
 
274
286
  - The published file list includes `child.ts`, which `index.ts` imports.
275
287
 
276
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.8.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
277
290
  [0.8.0]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...v0.8.0
278
291
  [0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
279
292
  [0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.8.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
@@ -84,27 +84,39 @@ function registerTakeover(pi: ExtensionAPI, parent: string): void {
84
84
  }
85
85
 
86
86
  /**
87
- * A usage limit moves the run to the next fallback that is usable and not
88
- * marked, and continues it in this session; each fallback is tried once.
89
- * 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>`.
90
91
  */
91
- function registerFallback(pi: ExtensionAPI): (model: string | undefined) => void {
92
+ function registerFallback(pi: ExtensionAPI): (failure: string | undefined) => void {
92
93
  /** The models still to move to on a usage limit, in order. */
93
94
  const fallback = (process.env[FALLBACK_ENV] ?? "").split(",").filter((model) => model.includes("/"));
94
- let limited: string | undefined;
95
+ let failure: string | undefined;
95
96
  pi.on("agent_before_settle", async (_event, ctx) => {
96
- const from = limited;
97
- limited = undefined;
97
+ const from = failure;
98
+ failure = undefined;
98
99
  const next = from === undefined ? undefined : await switchModel(pi, ctx, fallback);
99
100
  if (next === undefined) return;
100
- 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.`;
101
102
  return { continue: true, entries: [{ type: "custom_message", customType: "delegate", content, display: true }] };
102
103
  });
103
- return (model) => {
104
- limited = model;
104
+ return (value) => {
105
+ failure = value;
105
106
  };
106
107
  }
107
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
+
108
120
  /** This delegate's footer icon, picked once per load. */
109
121
  const ICON = Math.random() < 0.5 ? "👦" : "👧";
110
122
 
@@ -120,7 +132,7 @@ function showStatus(ctx: ExtensionContext, armed: boolean): void {
120
132
 
121
133
  export function registerChild(pi: ExtensionAPI, parent: string): void {
122
134
  registerTakeover(pi, parent);
123
- const setLimited = registerFallback(pi);
135
+ const setFailure = registerFallback(pi);
124
136
 
125
137
  let armed = process.env[AUTO_EXIT_ENV] === "1";
126
138
  /** Whether the run that just ended finished without the user stopping it. */
@@ -162,11 +174,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
162
174
  const stopped = last?.stopReason === "aborted" || ctx.signal?.aborted === true;
163
175
  if (stopped) disarm(ctx);
164
176
  if (!stopped && last?.stopReason === "error") recordUsageLimit(last, ctx, response);
165
- setLimited(
166
- !stopped && typeof last?.errorMessage === "string" && classify(last.errorMessage) !== undefined
167
- ? `${String(last.provider)}/${String(last.model)}`
168
- : undefined,
169
- );
177
+ setFailure(stopped ? undefined : fallbackReason(last));
170
178
  // A failure ends the work like a completion: the parent gets the error, the session file stays.
171
179
  completed = !stopped;
172
180
  });
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 } : {};