oira666_pi-limits-wait 0.5.8 → 0.5.9

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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/ui.ts +8 -33
package/README.md CHANGED
@@ -26,7 +26,7 @@ Restart Pi or run `/reload` after installing.
26
26
  - For any other (unclassified) provider error — including an API `Internal server error` and HTTP `400`/`403` failures — waits and retries the same model. There is deliberately no permanent-HTTP-status allowlist. This retry-all behavior is enabled by default and uses a configurable retry limit and interval; it does not alter model-limit wait intervals.
27
27
  - Reports retry/fallback waits in chat notifications without adding persistent TUI status lines.
28
28
  - In non-interactive hosts (`rpc` and `json` modes) the extension never reports a planned retry as an error. While it is waiting to retry, it publishes `info`-level liveliness notifications (plus a `setStatus` entry) describing what it is doing, how long it will wait, and why. An `error`-level notification is emitted only when the extension stops retrying and surfaces the terminal failure. See [Non-interactive (RPC) reporting](#non-interactive-rpc-reporting).
29
- - Press Enter during a retry wait to skip the wait and retry immediately.
29
+ - Leaves terminal input to Pi, so built-in selectors remain usable during a wait. Choosing another model with `/model` immediately ends the pending wait and continues with the newly selected provider/model.
30
30
  - Optionally falls back to configured models when the current/default model is rate-limited. Every fallback to a different model—even on the same provider—deliberately discards request-owned credentials, headers, environment overrides, and provider-specific options so the target runtime resolves its own configuration.
31
31
  - Preserves Pi's `before_provider_request` / `after_provider_response` callback flow and sanitises only the exact Anthropic OAuth system-block shape generated by current Pi; Pi remains responsible for OAuth identity insertion.
32
32
 
@@ -119,7 +119,7 @@ import {
119
119
 
120
120
  ### Skipping a wait from the host
121
121
 
122
- In `rpc`/`json` mode there is no terminal, so the interactive "press Enter to retry now" affordance is unavailable. Instead, set `PI_LIMITS_WAIT_CONTROL_FILE` to an absolute path (one per pi process; the extension echoes it back as `controlFile` in every structured frame). While — and only while — a wait is in progress, the extension polls that path once per second from the countdown ticker it already runs. Signals are deliberately **not** used: `SIGUSR2` with no handler installed terminates the process, and a signal cannot carry a `waitId`.
122
+ To skip a wait without changing models, set `PI_LIMITS_WAIT_CONTROL_FILE` to an absolute path (one per pi process; the extension echoes it back as `controlFile` in every structured frame). While — and only while — a wait is in progress, the extension polls that path once per second from the countdown ticker it already runs. Signals are deliberately **not** used: `SIGUSR2` with no handler installed terminates the process, and a signal cannot carry a `waitId`.
123
123
 
124
124
  Write a token to make the extension stop waiting and retry immediately:
125
125
 
@@ -132,7 +132,7 @@ Write a token to make the extension stop waiting and retry immediately:
132
132
  - `issuedAt` is epoch ms and must be `>= startedAt` of the current wait.
133
133
  - Writers should write a temp file and rename it into place (atomic move) so a partial read is impossible.
134
134
 
135
- The extension consumes a token by atomically renaming the canonical control path to a unique `.consumed-<waitId>-<random>` path in the same directory, then reading and validating that claimed file. It removes only the unique claimed path. A writer may therefore rename a newer token onto the canonical path immediately after the claim without cleanup deleting that replacement. An accepted claim settles the wait with `outcome: "skipped"` — the exact same path the Enter key uses, so the retry proceeds normally and a `wait_end` frame with `"outcome":"skipped"` is published. Polls are serialized; a slow filesystem operation cannot overlap the next one-second tick.
135
+ The extension consumes a token by atomically renaming the canonical control path to a unique `.consumed-<waitId>-<random>` path in the same directory, then reading and validating that claimed file. It removes only the unique claimed path. A writer may therefore rename a newer token onto the canonical path immediately after the claim without cleanup deleting that replacement. An accepted claim settles the wait with `outcome: "skipped"`, so the retry proceeds normally and a `wait_end` frame with `"outcome":"skipped"` is published. Polls are serialized; a slow filesystem operation cannot overlap the next one-second tick.
136
136
 
137
137
  Two content guards ensure a claimed leftover token can never skip a **later** legitimate wait: (a) `waitId` equality — a token written for wait *N* cannot match wait *N+1*'s freshly generated id; (b) `issuedAt >= startedAt` — a `"*"` token issued before this wait began is rejected. Claimed malformed, stale, and non-matching files are discarded, while any newer token at the canonical path is left untouched. The extension deliberately does not unlink the canonical path at wait start, because that would race with a token written microseconds earlier.
138
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-limits-wait",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "Extension for PI that allows to automatically wait for limits to reset and retry the request",
5
5
  "type": "module",
6
6
  "exports": {
package/src/ui.ts CHANGED
@@ -215,15 +215,12 @@ function startLivelinessReporting(
215
215
  };
216
216
  }
217
217
 
218
- function countdownText(reason: RetryReason, deadline: number, allowSkip: boolean): string {
218
+ function countdownText(reason: RetryReason, deadline: number): string {
219
219
  const remaining = Math.max(0, deadline - Date.now());
220
220
  const totalSecs = Math.ceil(remaining / 1_000);
221
221
  const mins = Math.floor(totalSecs / 60);
222
222
  const secs = (totalSecs % 60).toString().padStart(2, "0");
223
- return (
224
- `⏳ ${reasonLabel(reason)} — next retry in ${mins}m ${secs}s` +
225
- (allowSkip ? " (Enter to retry now)" : "")
226
- );
223
+ return `⏳ ${reasonLabel(reason)} — next retry in ${mins}m ${secs}s`;
227
224
  }
228
225
 
229
226
  export function showAmbientRetryStatus(reason: RetryReason, waitMs: number): void {
@@ -239,7 +236,7 @@ export function showAmbientRetryStatus(reason: RetryReason, waitMs: number): voi
239
236
  state.ambientStatusCleanup?.();
240
237
  return;
241
238
  }
242
- ctx.ui.setWorkingMessage(countdownText(reason, deadline, false));
239
+ ctx.ui.setWorkingMessage(countdownText(reason, deadline));
243
240
  };
244
241
 
245
242
  tick();
@@ -341,7 +338,6 @@ export function waitForRetry(
341
338
  const ctx = state.sharedCtx;
342
339
  let done = false;
343
340
  const stopLiveliness = startLivelinessReporting(statusContext, reason, plannedDeadline, model, telemetry?.error);
344
- let unsubInput: (() => void) | undefined;
345
341
  let ticker: ReturnType<typeof setInterval> | undefined;
346
342
  let timer: ReturnType<typeof setTimeout> | undefined;
347
343
 
@@ -355,7 +351,6 @@ export function waitForRetry(
355
351
  state.activeWaitSkips.delete(skipWait);
356
352
  try { stopLiveliness(false); } catch { /* UI unavailable */ }
357
353
  signal?.removeEventListener("abort", onAbort);
358
- try { unsubInput?.(); } catch { /* UI unavailable */ }
359
354
  try { ctx?.ui.setWorkingMessage(); } catch { /* UI unavailable */ }
360
355
  try { clearAmbientRetryStatus(); } catch { /* UI unavailable */ }
361
356
  }
@@ -437,32 +432,12 @@ export function waitForRetry(
437
432
  return;
438
433
  }
439
434
 
435
+ // Raw terminal listeners run before whichever component currently owns
436
+ // focus. Consuming Enter here would steal confirmation from built-in
437
+ // selectors such as /model. Model changes and the out-of-band control
438
+ // channel call this skip function directly instead.
440
439
  state.activeWaitSkips.add(skipWait);
441
440
 
442
- try {
443
- const unsubscribe = ctx?.ui.onTerminalInput((data) => {
444
- if (done) return undefined;
445
- if (data === "\r" || data === "\n") {
446
- // Enter belongs to the editor whenever it contains text. In
447
- // particular, slash-command autocomplete needs Enter to confirm and
448
- // then execute a command; do not steal either press from it.
449
- let editorText = "";
450
- try { editorText = ctx?.ui.getEditorText() ?? ""; } catch { /* UI unavailable */ }
451
- if (editorText.length > 0) return undefined;
452
- settle("skipped");
453
- return { consume: true };
454
- }
455
- if (data === "\x1b") {
456
- settle("aborted");
457
- return { consume: true };
458
- }
459
- return undefined;
460
- });
461
- if (done) unsubscribe?.();
462
- else unsubInput = unsubscribe;
463
- } catch { /* UI unavailable */ }
464
- if (done) return;
465
-
466
441
  signal?.addEventListener("abort", onAbort);
467
442
  if (signal?.aborted) {
468
443
  settle("aborted");
@@ -470,7 +445,7 @@ export function waitForRetry(
470
445
  }
471
446
 
472
447
  const tick = () => {
473
- try { ctx?.ui.setWorkingMessage(countdownText(reason, plannedDeadline, Boolean(unsubInput))); } catch { /* UI unavailable */ }
448
+ try { ctx?.ui.setWorkingMessage(countdownText(reason, plannedDeadline)); } catch { /* UI unavailable */ }
474
449
  };
475
450
 
476
451
  tick();