oira666_pi-limits-wait 0.6.1 → 0.6.3

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/README.md CHANGED
@@ -16,6 +16,7 @@ Restart Pi or run `/reload` after installing.
16
16
 
17
17
  - Targets Pi 0.80.10+ (developed and tested against Pi 0.83.0) and works with model providers that use `ModelRuntime.streamSimple`.
18
18
  - Intercepts the current runtime without registering synthetic providers; every retry and fallback re-enters Pi's normal provider/auth/base URL/configured-header composition, including OAuth refresh checks.
19
+ - On Pi 0.86+, stops background cache warming via `cache_warming_decision` while retry interception is enabled, preventing background refreshes from triggering foreground retries or fallbacks. Normal request caching is unchanged. With `PI_LIMITS_WAIT_DISABLE_ALL_WAITING=true`, Pi's cache-warming policy is left untouched. As with any handler for this event, a later extension can override the decision.
19
20
  - Reload-safe ownership ensures `/reload` transfers interception cleanly and aborts waits still owned by the previous extension instance. The interception has one process-global active extension owner, matching the interactive Pi CLI's single active session runtime lifecycle; SDK processes running multiple concurrent Pi sessions are not supported.
20
21
  - On rate-limit errors (`429`, `rate_limit`, `too many requests`, quota/reset messages), waits and retries in a loop.
21
22
  - Remembers provider retry timing exposed through response callbacks or provider errors (`retry-after`, `retry-after-ms`, `retry in ...`, reset messages). With fallback probing enabled, explicit Retry-After deadlines may intentionally be probed early; set the probe interval to `0` to honor those deadlines (unless a wait is manually skipped).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-limits-wait",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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": {
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "dependencies": {},
42
42
  "peerDependencies": {
43
- "@earendil-works/pi-ai": ">=0.80.10",
44
- "@earendil-works/pi-coding-agent": ">=0.80.10"
43
+ "@earendil-works/pi-ai": "*",
44
+ "@earendil-works/pi-coding-agent": "*"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@earendil-works/pi-ai": "0.83.0",
package/src/constants.ts CHANGED
@@ -43,7 +43,7 @@ export const DEFAULT_LIVELINESS_INTERVAL_MS = 15 * 1_000; // 15 seconds
43
43
  export const LIVELINESS_JSON_STATUS_KEY = "oira666.pi-limits-wait.json";
44
44
  /** Payload schema version for LIVELINESS_JSON_STATUS_KEY. Bump only on a breaking change. */
45
45
  export const LIVELINESS_JSON_VERSION = 1;
46
- export const EXTENSION_VERSION = "0.6.0";
46
+ export const EXTENSION_VERSION = "0.6.2";
47
47
 
48
48
  export const SETTINGS_FILE_NAME = "limits-wait.json";
49
49
  export const FALLBACK_MODELS_KEY = "fallback-models";
package/src/index.ts CHANGED
@@ -1,166 +1,174 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { isInternalSyntheticModel } from "./models.js";
3
- import { sanitiseAnthropicPayloadSystem } from "./prompt.js";
4
- import { RetrySummaryCoordinator } from "./retry-summary.js";
5
- import { loadFallbackSettings } from "./settings.js";
6
- import { allWaitingDisabled, loadUnknownErrorRetrySettings } from "./ui.js";
7
- import { consumeExpectedModelSelection, state } from "./state.js";
8
- import { disableModelRuntimeInterception, installModelRuntimeInterception } from "./stream.js";
9
-
10
- export { sanitiseAnthropicPayloadSystem, sanitiseSystemPrompt } from "./prompt.js";
11
- export {
12
- getRetryableError,
13
- isAuthenticationRefreshError,
14
- isRateLimitError,
15
- isServerOverloadedError,
16
- isTransientNetworkError,
17
- parseRetryDelayMs,
18
- rateLimitWaitMs,
19
- retryAfterHeaderMs,
20
- } from "./retry-errors.js";
21
- export {
22
- __readFallbackSettingsForTests,
23
- configFileCandidates,
24
- parseProbeIntervalSeconds,
25
- readFallbackSettings,
26
- } from "./settings.js";
27
- export {
28
- blockedState,
29
- candidateOrder,
30
- earliestCandidateWakeup,
31
- initialAttempt,
32
- isModelBlocked,
33
- nextAttemptCandidate,
34
- nextAvailableCandidate,
35
- probeIntervalMs,
36
- } from "./models.js";
37
- export {
38
- __createRetrySummaryCoordinatorForTests,
39
- LIMITS_WAIT_ENTRY_TYPE,
40
- RetrySummaryCoordinator,
41
- } from "./retry-summary.js";
42
- export {
43
- LIMITS_WAIT_EVENT_CHANNEL,
44
- } from "./telemetry.js";
45
- export {
46
- CONTROL_FILE_ENV_VAR,
47
- EXTENSION_VERSION,
48
- LIVELINESS_JSON_STATUS_KEY,
49
- LIVELINESS_JSON_VERSION,
50
- LIVELINESS_STATUS_KEY,
51
- PROBE_INTERVAL_ENV_VAR,
52
- STATUS_JSON_ENV_VAR,
53
- } from "./constants.js";
54
- export {
55
- clearStatusJson,
56
- controlFilePath,
57
- publishStatusJson,
58
- statusJsonEnabled,
59
- statusPayload,
60
- } from "./status-json.js";
61
- export type {
62
- LimitsWaitStatusContext,
63
- LimitsWaitStatusEvent,
64
- LimitsWaitStatusPayload,
65
- } from "./status-json.js";
66
- export type {
67
- LimitsWaitEndEventPayload,
68
- LimitsWaitEventPayload,
69
- LimitsWaitModel,
70
- LimitsWaitOutcome,
71
- LimitsWaitStartEventPayload,
72
- LimitsWaitWaitEndPayload,
73
- LimitsWaitWaitEventPayload,
74
- LimitsWaitWaitOutcome,
75
- LimitsWaitWaitStartPayload,
76
- } from "./telemetry.js";
77
- export type { LimitsWaitSessionEntryData } from "./retry-summary.js";
78
- export {
79
- __configureFallbackModelsForTests,
80
- __setNonRetryableTuningForTests,
81
- __setProbeIntervalForTests,
82
- disableModelRuntimeInterception,
83
- installModelRuntimeInterception,
84
- streamWithLimitsRetry,
85
- streamWithRateLimitRetry,
86
- } from "./stream.js";
87
- export {
88
- allWaitingDisabled,
89
- clearLivelinessStatus,
90
- configuredProbeIntervalMs,
91
- freezingEnabled,
92
- isNonInteractiveHost,
93
- loadUnknownErrorRetrySettings,
94
- notifyFinalFailure,
95
- notifyLiveliness,
96
- waitForRateLimit,
97
- waitForRetry,
98
- } from "./ui.js";
99
- export type { FallbackModel } from "./types.js";
100
-
101
- export default function (pi: ExtensionAPI) {
102
- // Current pi-ai inserts the Claude Code identity as the first system block
103
- // for Anthropic OAuth. Sanitise only that exact payload shape; authentication,
104
- // identity insertion, Anthropic headers, and all other request composition
105
- // remain owned by ModelRuntime.
106
- pi.on("before_provider_request", (event) => sanitiseAnthropicPayloadSystem(event.payload));
107
-
108
- // Keep prompt sanitisation and the runtime's normal Anthropic header path,
109
- // but install no retry/error interception or retry-related lifecycle hooks.
110
- if (allWaitingDisabled()) {
111
- disableModelRuntimeInterception();
112
- return;
113
- }
114
-
115
- state.extensionApi = pi;
116
- loadUnknownErrorRetrySettings();
117
- const retrySummaries = new RetrySummaryCoordinator(pi);
118
- const releaseInterception = installModelRuntimeInterception(() => retrySummaries.createStream());
119
-
120
- pi.on("model_select", (event, ctx) => {
121
- state.sharedCtx = ctx;
122
- if (consumeExpectedModelSelection(event.model)) return;
123
- if (isInternalSyntheticModel(event.model)) return;
124
- state.primaryModel = event.model;
125
- state.primaryThinkingLevel = pi.getThinkingLevel();
126
- state.userModelSelectionGeneration++;
127
- // A user-selected model takes precedence over a pending retry. Wake every
128
- // wait so its stream can adopt the new primary model immediately.
129
- for (const skip of [...state.activeWaitSkips]) skip();
130
- });
131
-
132
- pi.on("before_agent_start", (_event, ctx) => {
133
- state.sharedCtx = ctx;
134
- if (!state.primaryModel && ctx.model && !isInternalSyntheticModel(ctx.model)) state.primaryModel = ctx.model;
135
- state.primaryThinkingLevel ??= pi.getThinkingLevel();
136
- loadFallbackSettings(ctx);
137
- });
138
-
139
- pi.on("agent_start", (_event, ctx) => {
140
- state.sharedCtx = ctx;
141
- });
142
-
143
- // turn_end runs after Pi has persisted the assistant turn, so these custom
144
- // entries remain durable transcript metadata without entering LLM context.
145
- pi.on("turn_end", () => {
146
- retrySummaries.flush();
147
- });
148
-
149
- pi.on("agent_end", () => {
150
- retrySummaries.finalizeAndFlush();
151
- });
152
-
153
- pi.on("session_shutdown", () => {
154
- // An old extension instance must not disable or clear state owned by a
155
- // newer /reload instance.
156
- const ownsInterception = releaseInterception();
157
- retrySummaries.finalizeAndFlush();
158
- if (!ownsInterception) return;
159
- state.ambientStatusCleanup?.();
160
- state.modelStatusCleanup?.();
161
- state.expectedModelSelections.clear();
162
- state.activeWaitSkips.clear();
163
- state.sharedCtx = undefined;
164
- state.extensionApi = undefined;
165
- });
166
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { isInternalSyntheticModel } from "./models.js";
3
+ import { sanitiseAnthropicPayloadSystem } from "./prompt.js";
4
+ import { RetrySummaryCoordinator } from "./retry-summary.js";
5
+ import { loadFallbackSettings } from "./settings.js";
6
+ import { allWaitingDisabled, loadUnknownErrorRetrySettings } from "./ui.js";
7
+ import { consumeExpectedModelSelection, state } from "./state.js";
8
+ import { disableModelRuntimeInterception, installModelRuntimeInterception } from "./stream.js";
9
+
10
+ export { sanitiseAnthropicPayloadSystem, sanitiseSystemPrompt } from "./prompt.js";
11
+ export {
12
+ getRetryableError,
13
+ isAuthenticationRefreshError,
14
+ isRateLimitError,
15
+ isServerOverloadedError,
16
+ isTransientNetworkError,
17
+ parseRetryDelayMs,
18
+ rateLimitWaitMs,
19
+ retryAfterHeaderMs,
20
+ } from "./retry-errors.js";
21
+ export {
22
+ __readFallbackSettingsForTests,
23
+ configFileCandidates,
24
+ parseProbeIntervalSeconds,
25
+ readFallbackSettings,
26
+ } from "./settings.js";
27
+ export {
28
+ blockedState,
29
+ candidateOrder,
30
+ earliestCandidateWakeup,
31
+ initialAttempt,
32
+ isModelBlocked,
33
+ nextAttemptCandidate,
34
+ nextAvailableCandidate,
35
+ probeIntervalMs,
36
+ } from "./models.js";
37
+ export {
38
+ __createRetrySummaryCoordinatorForTests,
39
+ LIMITS_WAIT_ENTRY_TYPE,
40
+ RetrySummaryCoordinator,
41
+ } from "./retry-summary.js";
42
+ export {
43
+ LIMITS_WAIT_EVENT_CHANNEL,
44
+ } from "./telemetry.js";
45
+ export {
46
+ CONTROL_FILE_ENV_VAR,
47
+ EXTENSION_VERSION,
48
+ LIVELINESS_JSON_STATUS_KEY,
49
+ LIVELINESS_JSON_VERSION,
50
+ LIVELINESS_STATUS_KEY,
51
+ PROBE_INTERVAL_ENV_VAR,
52
+ STATUS_JSON_ENV_VAR,
53
+ } from "./constants.js";
54
+ export {
55
+ clearStatusJson,
56
+ controlFilePath,
57
+ publishStatusJson,
58
+ statusJsonEnabled,
59
+ statusPayload,
60
+ } from "./status-json.js";
61
+ export type {
62
+ LimitsWaitStatusContext,
63
+ LimitsWaitStatusEvent,
64
+ LimitsWaitStatusPayload,
65
+ } from "./status-json.js";
66
+ export type {
67
+ LimitsWaitEndEventPayload,
68
+ LimitsWaitEventPayload,
69
+ LimitsWaitModel,
70
+ LimitsWaitOutcome,
71
+ LimitsWaitStartEventPayload,
72
+ LimitsWaitWaitEndPayload,
73
+ LimitsWaitWaitEventPayload,
74
+ LimitsWaitWaitOutcome,
75
+ LimitsWaitWaitStartPayload,
76
+ } from "./telemetry.js";
77
+ export type { LimitsWaitSessionEntryData } from "./retry-summary.js";
78
+ export {
79
+ __configureFallbackModelsForTests,
80
+ __setNonRetryableTuningForTests,
81
+ __setProbeIntervalForTests,
82
+ disableModelRuntimeInterception,
83
+ installModelRuntimeInterception,
84
+ streamWithLimitsRetry,
85
+ streamWithRateLimitRetry,
86
+ } from "./stream.js";
87
+ export {
88
+ allWaitingDisabled,
89
+ clearLivelinessStatus,
90
+ configuredProbeIntervalMs,
91
+ freezingEnabled,
92
+ isNonInteractiveHost,
93
+ loadUnknownErrorRetrySettings,
94
+ notifyFinalFailure,
95
+ notifyLiveliness,
96
+ waitForRateLimit,
97
+ waitForRetry,
98
+ } from "./ui.js";
99
+ export type { FallbackModel } from "./types.js";
100
+
101
+ export default function (pi: ExtensionAPI) {
102
+ // Current pi-ai inserts the Claude Code identity as the first system block
103
+ // for Anthropic OAuth. Sanitise only that exact payload shape; authentication,
104
+ // identity insertion, Anthropic headers, and all other request composition
105
+ // remain owned by ModelRuntime.
106
+ pi.on("before_provider_request", (event) => sanitiseAnthropicPayloadSystem(event.payload));
107
+
108
+ // Keep prompt sanitisation and the runtime's normal Anthropic header path,
109
+ // but install no retry/error interception or retry-related lifecycle hooks.
110
+ if (allWaitingDisabled()) {
111
+ disableModelRuntimeInterception();
112
+ return;
113
+ }
114
+
115
+ state.extensionApi = pi;
116
+ loadUnknownErrorRetrySettings();
117
+ const retrySummaries = new RetrySummaryCoordinator(pi);
118
+ const releaseInterception = installModelRuntimeInterception(() => retrySummaries.createStream());
119
+
120
+ // Pi 0.86+ warms caches through the same runtime as foreground requests.
121
+ // Stop background refreshes before they can enter our retry/fallback logic.
122
+ // Keep the older Pi dependency range: its event registry accepts this hook
123
+ // but never emits it. The narrow overload bridges the pre-0.86 typings.
124
+ (pi as ExtensionAPI & {
125
+ on(event: "cache_warming_decision", handler: () => { action: "stop" }): void;
126
+ }).on("cache_warming_decision", () => ({ action: "stop" }));
127
+
128
+ pi.on("model_select", (event, ctx) => {
129
+ state.sharedCtx = ctx;
130
+ if (consumeExpectedModelSelection(event.model)) return;
131
+ if (isInternalSyntheticModel(event.model)) return;
132
+ state.primaryModel = event.model;
133
+ state.primaryThinkingLevel = pi.getThinkingLevel();
134
+ state.userModelSelectionGeneration++;
135
+ // A user-selected model takes precedence over a pending retry. Wake every
136
+ // wait so its stream can adopt the new primary model immediately.
137
+ for (const skip of [...state.activeWaitSkips]) skip();
138
+ });
139
+
140
+ pi.on("before_agent_start", (_event, ctx) => {
141
+ state.sharedCtx = ctx;
142
+ if (!state.primaryModel && ctx.model && !isInternalSyntheticModel(ctx.model)) state.primaryModel = ctx.model;
143
+ state.primaryThinkingLevel ??= pi.getThinkingLevel();
144
+ loadFallbackSettings(ctx);
145
+ });
146
+
147
+ pi.on("agent_start", (_event, ctx) => {
148
+ state.sharedCtx = ctx;
149
+ });
150
+
151
+ // turn_end runs after Pi has persisted the assistant turn, so these custom
152
+ // entries remain durable transcript metadata without entering LLM context.
153
+ pi.on("turn_end", () => {
154
+ retrySummaries.flush();
155
+ });
156
+
157
+ pi.on("agent_end", () => {
158
+ retrySummaries.finalizeAndFlush();
159
+ });
160
+
161
+ pi.on("session_shutdown", () => {
162
+ // An old extension instance must not disable or clear state owned by a
163
+ // newer /reload instance.
164
+ const ownsInterception = releaseInterception();
165
+ retrySummaries.finalizeAndFlush();
166
+ if (!ownsInterception) return;
167
+ state.ambientStatusCleanup?.();
168
+ state.modelStatusCleanup?.();
169
+ state.expectedModelSelections.clear();
170
+ state.activeWaitSkips.clear();
171
+ state.sharedCtx = undefined;
172
+ state.extensionApi = undefined;
173
+ });
174
+ }