pi-goal-x 0.30.3 → 0.30.5

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.
@@ -174,7 +174,10 @@ export function registerGoalEvents(core: GoalCore): void {
174
174
  core.accountProgress(ctx, { completedTurnTokens: tokens });
175
175
 
176
176
  if (isAbortedAssistantMessage(message)) {
177
- core.pauseActiveGoal(ctx);
177
+ // Pause only on a genuine user abort (signal fired). A provider- or
178
+ // transport-side abort without the signal routes into recovery via
179
+ // agent_end instead of stranding the goal.
180
+ if (ctx.signal?.aborted) core.pauseActiveGoal(ctx);
178
181
  return;
179
182
  }
180
183
  // Provider failures are not completed work: do not turn one failed turn
@@ -266,7 +269,9 @@ export function registerGoalEvents(core: GoalCore): void {
266
269
  });
267
270
 
268
271
  pi.on("message_end", async (event, ctx) => {
269
- if (isAbortedAssistantMessage(event.message)) core.pauseActiveGoal(ctx);
272
+ // Signal-aware: see turn_end — only user aborts pause; provider-side
273
+ // aborts are handled by agent_end's recovery path.
274
+ if (isAbortedAssistantMessage(event.message) && ctx.signal?.aborted) core.pauseActiveGoal(ctx);
270
275
  const raw = asRecord(event.message);
271
276
  if (raw?.role === "custom" && raw.customType === GOAL_EVENT_ENTRY && raw.display !== false) {
272
277
  return { message: { ...event.message, display: false } as typeof event.message };
@@ -494,14 +499,19 @@ export function registerGoalEvents(core: GoalCore): void {
494
499
  if (!core.state.goal || core.state.goal.status !== "active" || !core.state.goal.autoContinue) return;
495
500
  if (endedGoalId && core.state.goal.id !== endedGoalId) return;
496
501
  if (!core.reconcileFocusedGoalFromDisk(ctx)) return;
497
- if (hasAbortedAssistantMessage(event.messages) || ctx.signal?.aborted) {
502
+ // A genuine user abort pauses the goal. An assistant message with
503
+ // stopReason "aborted" WITHOUT a user abort signal is a provider- or
504
+ // transport-side termination (e.g. after Pi exhausts its retries) —
505
+ // pausing there stranded goals during outages, so it routes into the
506
+ // same bounded recovery as classified transient errors instead.
507
+ if (ctx.signal?.aborted) {
498
508
  core.pauseActiveGoal(ctx);
499
509
  return;
500
510
  }
501
511
  // Provider failures are not completed work: persist and refresh the
502
512
  // display, but never queue a continuation for a run whose messages
503
513
  // include an assistant error (danim47c pattern).
504
- if (hasNetworkErrorAssistantMessage(event.messages)) {
514
+ if (hasNetworkErrorAssistantMessage(event.messages) || hasAbortedAssistantMessage(event.messages)) {
505
515
  core.persist(ctx);
506
516
  core.updateUI(ctx);
507
517
  networkErrorRecoveryAfterSettleFor = core.state.goal.id;
@@ -228,16 +228,20 @@ export function isErrorAssistantMessage(message: unknown): boolean {
228
228
 
229
229
  /**
230
230
  * Transient provider failures are safe for goal-level recovery backoff:
231
- * Pi-declared network errors plus HTTP 5xx-style outages surfaced by
232
- * providers (503 server_error, "Endpoint is unavailable", overloaded,
233
- * gateway failures). Deliberately excludes 4xx client errors such as auth
234
- * and malformed-request failures, which retrying cannot fix.
231
+ * Pi-declared network errors plus HTTP 429/5xx-style outages surfaced by
232
+ * providers (429 rate limits, 503 server_error, "Endpoint is unavailable",
233
+ * overloaded, gateway failures). Deliberately excludes auth and
234
+ * malformed-request failures (and quota/billing text), which retrying
235
+ * cannot fix.
235
236
  */
236
237
  const TRANSIENT_PROVIDER_ERROR_RE = new RegExp(
237
238
  [
238
239
  "\\bnetwork[_\\s-]?error\\b",
239
240
  "\\bserver[_\\s]?error\\b",
240
- "\\b(?:502|503|504|529)\\b",
241
+ "\\b(?:429|502|503|504|529)\\b",
242
+ "\\brate[_\\s-]?limited\\b",
243
+ "\\brate[_\\s-]?limit\\b",
244
+ "\\btoo many requests\\b",
241
245
  "\\bservice unavailable\\b",
242
246
  "\\bbad gateway\\b",
243
247
  "\\bgateway timeout\\b",
@@ -248,6 +252,19 @@ const TRANSIENT_PROVIDER_ERROR_RE = new RegExp(
248
252
  "i",
249
253
  );
250
254
 
255
+ /** Quota/billing exhaustion is deterministic; retrying cannot fix it. */
256
+ const NON_TRANSIENT_PROVIDER_ERROR_RE = new RegExp(
257
+ [
258
+ "\\binsufficient[_\\s]?quota\\b",
259
+ "\\bout of budget\\b",
260
+ "\\bquota exceeded\\b",
261
+ "\\bbilling\\b",
262
+ "GoUsageLimitError",
263
+ "FreeUsageLimitError",
264
+ ].join("|"),
265
+ "i",
266
+ );
267
+
251
268
  /** A transient provider failure is safe for the bounded goal backoff. */
252
269
  export function isNetworkErrorAssistantMessage(message: unknown): boolean {
253
270
  if (!isErrorAssistantMessage(message)) return false;
@@ -255,6 +272,8 @@ export function isNetworkErrorAssistantMessage(message: unknown): boolean {
255
272
  const details = [raw?.rawStopReason, raw?.errorMessage]
256
273
  .filter((value): value is string => typeof value === "string")
257
274
  .join(" ");
275
+ // Quota/billing exhaustion is deterministic and must never be retried.
276
+ if (NON_TRANSIENT_PROVIDER_ERROR_RE.test(details)) return false;
258
277
  return TRANSIENT_PROVIDER_ERROR_RE.test(details);
259
278
  }
260
279
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.30.3",
3
+ "version": "0.30.5",
4
4
  "description": "Goal mode extension for pi: durable long-running objectives, five model tools, structured tasks, independent completion audit, Sisyphus mode, auto-continue, and a status overlay.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",