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 +14 -1
- package/package.json +1 -1
- package/src/child.ts +24 -16
- package/src/limits.ts +4 -2
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.
|
|
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
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
|
|
88
|
-
* marked, and continues it in this session; each
|
|
89
|
-
* Returns the setter for
|
|
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): (
|
|
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
|
|
95
|
+
let failure: string | undefined;
|
|
95
96
|
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
96
|
-
const from =
|
|
97
|
-
|
|
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}
|
|
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 (
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 } : {};
|