oira666_pi-limits-wait 0.5.9 → 0.6.2

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/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "oira666_pi-limits-wait",
3
- "version": "0.5.9",
4
- "description": "Extension for PI that allows to automatically wait for limits to reset and retry the request",
5
- "type": "module",
6
- "exports": {
7
- ".": "./src/index.ts"
8
- },
9
- "license": "MIT",
10
- "files": [
11
- "src",
12
- "!src/__tests__",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "engines": {
17
- "node": ">=22.19.0"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "https://github.com/gee666/pi-limits-wait.git"
22
- },
23
- "bugs": {
24
- "url": "https://github.com/gee666/pi-limits-wait/issues"
25
- },
26
- "homepage": "https://github.com/gee666/pi-limits-wait#readme",
27
- "publishConfig": {
28
- "access": "public"
29
- },
30
- "scripts": {
31
- "check": "tsc -p tsconfig.json --noEmit",
32
- "test": "tsx src/__tests__/test-extension.ts",
33
- "pack:check": "npm pack --dry-run",
34
- "prepublishOnly": "npm run check && npm test && npm run pack:check"
35
- },
36
- "pi": {
37
- "extensions": [
38
- "./src/index.ts"
39
- ]
40
- },
41
- "dependencies": {},
42
- "peerDependencies": {
43
- "@earendil-works/pi-ai": ">=0.80.10",
44
- "@earendil-works/pi-coding-agent": ">=0.80.10"
45
- },
46
- "devDependencies": {
47
- "@earendil-works/pi-ai": "0.83.0",
48
- "@earendil-works/pi-coding-agent": "0.83.0",
49
- "tsx": "^4.20.6",
50
- "typescript": "^5.9.3"
51
- }
52
- }
1
+ {
2
+ "name": "oira666_pi-limits-wait",
3
+ "version": "0.6.2",
4
+ "description": "Extension for PI that allows to automatically wait for limits to reset and retry the request",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "license": "MIT",
10
+ "files": [
11
+ "src",
12
+ "!src/__tests__",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22.19.0"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/gee666/pi-limits-wait.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/gee666/pi-limits-wait/issues"
25
+ },
26
+ "homepage": "https://github.com/gee666/pi-limits-wait#readme",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "check": "tsc -p tsconfig.json --noEmit",
32
+ "test": "tsx src/__tests__/test-extension.ts",
33
+ "pack:check": "npm pack --dry-run",
34
+ "prepublishOnly": "npm run check && npm test && npm run pack:check"
35
+ },
36
+ "pi": {
37
+ "extensions": [
38
+ "./src/index.ts"
39
+ ]
40
+ },
41
+ "dependencies": {},
42
+ "peerDependencies": {
43
+ "@earendil-works/pi-ai": ">=0.80.10",
44
+ "@earendil-works/pi-coding-agent": ">=0.80.10"
45
+ },
46
+ "devDependencies": {
47
+ "@earendil-works/pi-ai": "0.83.0",
48
+ "@earendil-works/pi-coding-agent": "0.83.0",
49
+ "tsx": "^4.20.6",
50
+ "typescript": "^5.9.3"
51
+ }
52
+ }
package/src/constants.ts CHANGED
@@ -1,55 +1,62 @@
1
- export const CLAUDE_CODE_IDENTITY =
2
- "You are Claude Code, Anthropic's official CLI for Claude.";
3
-
4
- export const PI_REMOVAL_ANCHORS = [
5
- "pi-coding-agent",
6
- "@earendil-works/pi-coding-agent",
7
- "badlogic/pi-mono",
8
- ] as const;
9
-
10
- export const PI_IDENTITY_SENTENCE_PATTERN =
11
- /(?:^|\n)\s*You are pi\b[^.!?\n]*(?:[.!?](?=\s|$)|(?=\n|$))/gi;
12
-
13
- // Strips the host-harness identity clause pi prepends to its system prompt, e.g.
14
- // "You are an expert coding assistant operating inside pi, a coding agent harness."
15
- // The Claude Code identity block already establishes who the assistant is, so
16
- // leaving this clause in tells the model (and Anthropic) it is running inside pi.
17
- export const PI_HARNESS_IDENTITY_PATTERN =
18
- /\s+operating inside pi,\s*a coding agent harness/gi;
19
-
20
- export const CLAUDE_CODE_IDENTITY_PATTERN =
21
- /(?:^|\n)\s*You are Claude Code, Anthropic's official CLI for Claude\.\s*/gi;
22
-
23
- export const DEFAULT_RATE_LIMIT_WAIT_MS = 30 * 60 * 1_000; // 30 minutes
24
- export const DEFAULT_OVERLOADED_WAIT_MS = 5 * 60 * 1_000; // 5 minutes
25
- export const DEFAULT_NETWORK_WAIT_MS = 15 * 1_000; // 15 seconds
26
- export const DEFAULT_NON_RETRYABLE_FREEZE_MS = 10 * 60 * 1_000; // 10 minutes
27
-
28
- // Unknown provider errors are retried separately from rate-limit, overload,
29
- // authentication, and network failures.
30
- export const DEFAULT_UNKNOWN_ERROR_MAX_RETRIES = 999_999;
31
- export const DEFAULT_UNKNOWN_ERROR_RETRY_INTERVAL_MS = 5 * 1_000; // 5 seconds
32
-
33
- // Non-interactive hosts (RPC/JSON) cannot render the TUI countdown, so retry
34
- // progress is published as periodic liveliness notifications instead.
35
- export const LIVELINESS_STATUS_KEY = "oira666.pi-limits-wait";
36
- export const DEFAULT_LIVELINESS_INTERVAL_MS = 15 * 1_000; // 15 seconds
37
-
38
- /** Machine-readable sibling of LIVELINESS_STATUS_KEY. Compact JSON, never prose. */
39
- export const LIVELINESS_JSON_STATUS_KEY = "oira666.pi-limits-wait.json";
40
- /** Payload schema version for LIVELINESS_JSON_STATUS_KEY. Bump only on a breaking change. */
41
- export const LIVELINESS_JSON_VERSION = 1;
42
- export const EXTENSION_VERSION = "0.5.7";
43
-
44
- export const SETTINGS_FILE_NAME = "limits-wait.json";
45
- export const FALLBACK_MODELS_KEY = "fallback-models";
46
- export const DISABLE_ALL_WAITING_ENV_VAR = "PI_LIMITS_WAIT_DISABLE_ALL_WAITING";
47
- export const FREEZING_ENV_VAR = "PI_LIMITS_WAIT_FREEZING_ENABLED";
48
- export const DEFAULT_WAITING_ENV_VAR = "PI_LIMITS_WAIT_DEFAULT_WAITING";
49
- export const MAX_RETRY_ENV_VAR = "PI_LIMITS_WAIT_MAX_RETRY";
50
- export const RETRY_INTERVAL_ENV_VAR = "PI_LIMITS_WAIT_RETRY_INTERVAL";
51
- export const LIVELINESS_INTERVAL_ENV_VAR = "PI_LIMITS_WAIT_LIVELINESS_INTERVAL";
52
- /** Set to false/0/no/off to suppress the structured status channel. */
53
- export const STATUS_JSON_ENV_VAR = "PI_LIMITS_WAIT_STATUS_JSON";
54
- /** Absolute path of the out-of-band control file polled while waiting. Unset => no control. */
55
- export const CONTROL_FILE_ENV_VAR = "PI_LIMITS_WAIT_CONTROL_FILE";
1
+ export const CLAUDE_CODE_IDENTITY =
2
+ "You are Claude Code, Anthropic's official CLI for Claude.";
3
+
4
+ export const PI_REMOVAL_ANCHORS = [
5
+ "pi-coding-agent",
6
+ "@earendil-works/pi-coding-agent",
7
+ "badlogic/pi-mono",
8
+ ] as const;
9
+
10
+ export const PI_IDENTITY_SENTENCE_PATTERN =
11
+ /(?:^|\n)\s*You are pi\b[^.!?\n]*(?:[.!?](?=\s|$)|(?=\n|$))/gi;
12
+
13
+ // Strips the host-harness identity clause pi prepends to its system prompt, e.g.
14
+ // "You are an expert coding assistant operating inside pi, a coding agent harness."
15
+ // The Claude Code identity block already establishes who the assistant is, so
16
+ // leaving this clause in tells the model (and Anthropic) it is running inside pi.
17
+ export const PI_HARNESS_IDENTITY_PATTERN =
18
+ /\s+operating inside pi,\s*a coding agent harness/gi;
19
+
20
+ export const CLAUDE_CODE_IDENTITY_PATTERN =
21
+ /(?:^|\n)\s*You are Claude Code, Anthropic's official CLI for Claude\.\s*/gi;
22
+
23
+ export const DEFAULT_RATE_LIMIT_WAIT_MS = 30 * 60 * 1_000; // 30 minutes
24
+ export const DEFAULT_OVERLOADED_WAIT_MS = 5 * 60 * 1_000; // 5 minutes
25
+ export const DEFAULT_NETWORK_WAIT_MS = 15 * 1_000; // 15 seconds
26
+ export const DEFAULT_NON_RETRYABLE_FREEZE_MS = 10 * 60 * 1_000; // 10 minutes
27
+
28
+ // A model that is currently limited/frozen is re-probed this often, so a
29
+ // higher-priority model can be reclaimed long before its remembered deadline.
30
+ export const DEFAULT_PROBE_INTERVAL_MS = 60 * 1_000; // 1 minute
31
+
32
+ // Unknown provider errors are retried separately from rate-limit, overload,
33
+ // authentication, and network failures.
34
+ export const DEFAULT_UNKNOWN_ERROR_MAX_RETRIES = 999_999;
35
+ export const DEFAULT_UNKNOWN_ERROR_RETRY_INTERVAL_MS = 5 * 1_000; // 5 seconds
36
+
37
+ // Non-interactive hosts (RPC/JSON) cannot render the TUI countdown, so retry
38
+ // progress is published as periodic liveliness notifications instead.
39
+ export const LIVELINESS_STATUS_KEY = "oira666.pi-limits-wait";
40
+ export const DEFAULT_LIVELINESS_INTERVAL_MS = 15 * 1_000; // 15 seconds
41
+
42
+ /** Machine-readable sibling of LIVELINESS_STATUS_KEY. Compact JSON, never prose. */
43
+ export const LIVELINESS_JSON_STATUS_KEY = "oira666.pi-limits-wait.json";
44
+ /** Payload schema version for LIVELINESS_JSON_STATUS_KEY. Bump only on a breaking change. */
45
+ export const LIVELINESS_JSON_VERSION = 1;
46
+ export const EXTENSION_VERSION = "0.6.2";
47
+
48
+ export const SETTINGS_FILE_NAME = "limits-wait.json";
49
+ export const FALLBACK_MODELS_KEY = "fallback-models";
50
+ export const PROBE_INTERVAL_KEY = "probe-interval-seconds";
51
+ export const DISABLE_ALL_WAITING_ENV_VAR = "PI_LIMITS_WAIT_DISABLE_ALL_WAITING";
52
+ export const FREEZING_ENV_VAR = "PI_LIMITS_WAIT_FREEZING_ENABLED";
53
+ export const DEFAULT_WAITING_ENV_VAR = "PI_LIMITS_WAIT_DEFAULT_WAITING";
54
+ export const MAX_RETRY_ENV_VAR = "PI_LIMITS_WAIT_MAX_RETRY";
55
+ /** Seconds between re-probes of a higher-priority limited/frozen model. 0 disables probing. */
56
+ export const PROBE_INTERVAL_ENV_VAR = "PI_LIMITS_WAIT_PROBE_INTERVAL";
57
+ export const RETRY_INTERVAL_ENV_VAR = "PI_LIMITS_WAIT_RETRY_INTERVAL";
58
+ export const LIVELINESS_INTERVAL_ENV_VAR = "PI_LIMITS_WAIT_LIVELINESS_INTERVAL";
59
+ /** Set to false/0/no/off to suppress the structured status channel. */
60
+ export const STATUS_JSON_ENV_VAR = "PI_LIMITS_WAIT_STATUS_JSON";
61
+ /** Absolute path of the out-of-band control file polled while waiting. Unset => no control. */
62
+ export const CONTROL_FILE_ENV_VAR = "PI_LIMITS_WAIT_CONTROL_FILE";
package/src/index.ts CHANGED
@@ -1,152 +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
- readFallbackSettings,
25
- } from "./settings.js";
26
- export {
27
- __createRetrySummaryCoordinatorForTests,
28
- LIMITS_WAIT_ENTRY_TYPE,
29
- RetrySummaryCoordinator,
30
- } from "./retry-summary.js";
31
- export {
32
- LIMITS_WAIT_EVENT_CHANNEL,
33
- } from "./telemetry.js";
34
- export {
35
- CONTROL_FILE_ENV_VAR,
36
- EXTENSION_VERSION,
37
- LIVELINESS_JSON_STATUS_KEY,
38
- LIVELINESS_JSON_VERSION,
39
- LIVELINESS_STATUS_KEY,
40
- STATUS_JSON_ENV_VAR,
41
- } from "./constants.js";
42
- export {
43
- clearStatusJson,
44
- controlFilePath,
45
- publishStatusJson,
46
- statusJsonEnabled,
47
- statusPayload,
48
- } from "./status-json.js";
49
- export type {
50
- LimitsWaitStatusContext,
51
- LimitsWaitStatusEvent,
52
- LimitsWaitStatusPayload,
53
- } from "./status-json.js";
54
- export type {
55
- LimitsWaitEndEventPayload,
56
- LimitsWaitEventPayload,
57
- LimitsWaitModel,
58
- LimitsWaitOutcome,
59
- LimitsWaitStartEventPayload,
60
- LimitsWaitWaitEndPayload,
61
- LimitsWaitWaitEventPayload,
62
- LimitsWaitWaitOutcome,
63
- LimitsWaitWaitStartPayload,
64
- } from "./telemetry.js";
65
- export type { LimitsWaitSessionEntryData } from "./retry-summary.js";
66
- export {
67
- __configureFallbackModelsForTests,
68
- __setNonRetryableTuningForTests,
69
- disableModelRuntimeInterception,
70
- installModelRuntimeInterception,
71
- streamWithLimitsRetry,
72
- streamWithRateLimitRetry,
73
- } from "./stream.js";
74
- export {
75
- allWaitingDisabled,
76
- clearLivelinessStatus,
77
- freezingEnabled,
78
- isNonInteractiveHost,
79
- loadUnknownErrorRetrySettings,
80
- notifyFinalFailure,
81
- notifyLiveliness,
82
- waitForRateLimit,
83
- waitForRetry,
84
- } from "./ui.js";
85
- export type { FallbackModel } from "./types.js";
86
-
87
- export default function (pi: ExtensionAPI) {
88
- // Current pi-ai inserts the Claude Code identity as the first system block
89
- // for Anthropic OAuth. Sanitise only that exact payload shape; authentication,
90
- // identity insertion, Anthropic headers, and all other request composition
91
- // remain owned by ModelRuntime.
92
- pi.on("before_provider_request", (event) => sanitiseAnthropicPayloadSystem(event.payload));
93
-
94
- // Keep prompt sanitisation and the runtime's normal Anthropic header path,
95
- // but install no retry/error interception or retry-related lifecycle hooks.
96
- if (allWaitingDisabled()) {
97
- disableModelRuntimeInterception();
98
- return;
99
- }
100
-
101
- state.extensionApi = pi;
102
- loadUnknownErrorRetrySettings();
103
- const retrySummaries = new RetrySummaryCoordinator(pi);
104
- const releaseInterception = installModelRuntimeInterception(() => retrySummaries.createStream());
105
-
106
- pi.on("model_select", (event, ctx) => {
107
- state.sharedCtx = ctx;
108
- if (consumeExpectedModelSelection(event.model)) return;
109
- if (isInternalSyntheticModel(event.model)) return;
110
- state.primaryModel = event.model;
111
- state.primaryThinkingLevel = pi.getThinkingLevel();
112
- state.userModelSelectionGeneration++;
113
- // A user-selected model takes precedence over a pending retry. Wake every
114
- // wait so its stream can adopt the new primary model immediately.
115
- for (const skip of [...state.activeWaitSkips]) skip();
116
- });
117
-
118
- pi.on("before_agent_start", (_event, ctx) => {
119
- state.sharedCtx = ctx;
120
- if (!state.primaryModel && ctx.model && !isInternalSyntheticModel(ctx.model)) state.primaryModel = ctx.model;
121
- state.primaryThinkingLevel ??= pi.getThinkingLevel();
122
- loadFallbackSettings(ctx);
123
- });
124
-
125
- pi.on("agent_start", (_event, ctx) => {
126
- state.sharedCtx = ctx;
127
- });
128
-
129
- // turn_end runs after Pi has persisted the assistant turn, so these custom
130
- // entries remain durable transcript metadata without entering LLM context.
131
- pi.on("turn_end", () => {
132
- retrySummaries.flush();
133
- });
134
-
135
- pi.on("agent_end", () => {
136
- retrySummaries.finalizeAndFlush();
137
- });
138
-
139
- pi.on("session_shutdown", () => {
140
- // An old extension instance must not disable or clear state owned by a
141
- // newer /reload instance.
142
- const ownsInterception = releaseInterception();
143
- retrySummaries.finalizeAndFlush();
144
- if (!ownsInterception) return;
145
- state.ambientStatusCleanup?.();
146
- state.modelStatusCleanup?.();
147
- state.expectedModelSelections.clear();
148
- state.activeWaitSkips.clear();
149
- state.sharedCtx = undefined;
150
- state.extensionApi = undefined;
151
- });
152
- }
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
+ }