takomi 2.1.30 → 2.1.32

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 (28) hide show
  1. package/.pi/extensions/notify-sound/index.ts +262 -262
  2. package/.pi/extensions/oauth-router/README.md +2 -1
  3. package/.pi/extensions/oauth-router/index.ts +130 -41
  4. package/.pi/extensions/oauth-router/provider.ts +115 -3
  5. package/.pi/extensions/oauth-router/types.ts +18 -0
  6. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +1 -1
  7. package/.pi/extensions/takomi-context-manager/index.ts +1 -1
  8. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +1 -1
  9. package/.pi/extensions/takomi-context-manager/policy-tools.ts +1 -1
  10. package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +1 -1
  11. package/.pi/extensions/takomi-context-manager/skill-tools.ts +1 -1
  12. package/.pi/extensions/takomi-runtime/commands.ts +1 -1
  13. package/.pi/extensions/takomi-runtime/context-panel.ts +708 -708
  14. package/.pi/extensions/takomi-runtime/index.ts +2 -2
  15. package/.pi/extensions/takomi-runtime/shared.ts +1 -1
  16. package/.pi/extensions/takomi-runtime/subagent-controller.ts +1 -1
  17. package/.pi/extensions/takomi-runtime/subagent-render.ts +1 -1
  18. package/.pi/extensions/takomi-runtime/subagent-types.ts +11 -11
  19. package/.pi/extensions/takomi-runtime/ui.ts +2 -2
  20. package/.pi/extensions/takomi-subagents/agents.ts +1 -1
  21. package/.pi/extensions/takomi-subagents/index.ts +1 -1
  22. package/.pi/extensions/takomi-subagents/native-render.ts +3 -3
  23. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +2 -2
  24. package/.pi/extensions/takomi-subagents/tool-runner.ts +1 -1
  25. package/package.json +5 -9
  26. package/src/doctor.js +1 -1
  27. package/src/pi-harness.js +34 -11
  28. package/src/pi-optional-features.js +1 -6
@@ -1,262 +1,262 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import { homedir, platform } from "node:os";
5
- import { spawn } from "node:child_process";
6
-
7
- type NotifySoundConfig = {
8
- enabled: boolean;
9
- };
10
-
11
- type NotifyMethod = "auto" | "wav";
12
-
13
- const GLOBAL_NOTIFY_DIR = join(homedir(), ".pi", "agent", "notify-sound");
14
- const CONFIG_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.json");
15
- const WAV_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.wav");
16
- const PS1_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.ps1");
17
- const VBS_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.vbs");
18
- const STALE_AGENT_START_MS = 24 * 60 * 60 * 1000;
19
-
20
- let config: NotifySoundConfig = { enabled: true };
21
- let lastAgentStartedAt = 0;
22
-
23
- export default async function notifySoundExtension(pi: ExtensionAPI) {
24
- await loadConfig();
25
- await ensureTuneWav();
26
-
27
- pi.on("session_start", async (_event, ctx) => {
28
- updateStatus(ctx);
29
- });
30
-
31
- pi.on("agent_start", async () => {
32
- lastAgentStartedAt = Date.now();
33
- });
34
-
35
- pi.on("agent_end", async (_event, ctx) => {
36
- updateStatus(ctx);
37
-
38
- if (!config.enabled) return;
39
- if (!lastAgentStartedAt) return;
40
- if (Date.now() - lastAgentStartedAt > STALE_AGENT_START_MS) return;
41
-
42
- playCompletionTune();
43
- });
44
-
45
- const command = {
46
- description: "Toggle or test the agent completion tune notification",
47
- getArgumentCompletions: (argumentPrefix: string) => {
48
- const token = argumentPrefix.trim().toLowerCase();
49
- return [
50
- { value: "status", label: "status", description: "Show whether the completion tune is on" },
51
- { value: "test", label: "test", description: "Play the completion tune" },
52
- { value: "test wav", label: "test wav", description: "Play the generated WAV completion tune" },
53
- { value: "on", label: "on", description: "Enable the completion tune" },
54
- { value: "off", label: "off", description: "Disable the completion tune" },
55
- { value: "toggle", label: "toggle", description: "Toggle the completion tune" },
56
- ].filter((completion) => !token || completion.value.startsWith(token));
57
- },
58
- handler: async (args: string, ctx: ExtensionCommandContext) => {
59
- const action = args.trim().toLowerCase() || "toggle";
60
-
61
- if (action === "test") {
62
- playCompletionTune("wav");
63
- ctx.ui.notify("Played completion tune using WAV mode. This is the default reliable Windows path.", "info");
64
- return;
65
- }
66
-
67
- if (action === "test wav") {
68
- playCompletionTune("wav");
69
- ctx.ui.notify("Played completion tune using WAV mode.", "info");
70
- return;
71
- }
72
-
73
- if (action.startsWith("test ")) {
74
- ctx.ui.notify("Usage: /notify test", "warning");
75
- return;
76
- }
77
-
78
- if (action === "status") {
79
- updateStatus(ctx);
80
- ctx.ui.notify(`Completion tune is ${config.enabled ? "ON" : "OFF"}.`, "info");
81
- return;
82
- }
83
-
84
- if (action === "on") {
85
- await setEnabled(true, ctx);
86
- return;
87
- }
88
-
89
- if (action === "off") {
90
- await setEnabled(false, ctx);
91
- return;
92
- }
93
-
94
- if (action === "toggle") {
95
- await setEnabled(!config.enabled, ctx);
96
- return;
97
- }
98
-
99
- ctx.ui.notify("Usage: /notify [test|status|on|off|toggle]", "warning");
100
- },
101
- };
102
-
103
- pi.registerCommand("notify-sound", command);
104
- pi.registerCommand("notify", {
105
- ...command,
106
- description: "Alias for /notify-sound",
107
- });
108
- }
109
-
110
- async function loadConfig(): Promise<void> {
111
- try {
112
- const raw = await readFile(CONFIG_PATH, "utf8");
113
- const parsed = JSON.parse(raw) as Partial<NotifySoundConfig>;
114
- config = { enabled: parsed.enabled !== false };
115
- } catch {
116
- config = { enabled: true };
117
- await saveConfig();
118
- }
119
- }
120
-
121
- async function saveConfig(): Promise<void> {
122
- await mkdir(dirname(CONFIG_PATH), { recursive: true });
123
- await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
124
- }
125
-
126
- async function setEnabled(enabled: boolean, ctx: ExtensionContext): Promise<void> {
127
- config.enabled = enabled;
128
- await saveConfig();
129
- updateStatus(ctx);
130
- ctx.ui.notify(`Completion tune ${enabled ? "enabled" : "disabled"}.`, "info");
131
- }
132
-
133
- async function ensureTuneWav(): Promise<void> {
134
- await mkdir(dirname(WAV_PATH), { recursive: true });
135
- await writeFile(WAV_PATH, createTuneWav(), "binary");
136
- const wav = WAV_PATH.replace(/'/g, "''");
137
- await writeFile(PS1_PATH, [
138
- "$ErrorActionPreference = 'SilentlyContinue'",
139
- `if (Test-Path '${wav}') {`,
140
- ` $p = New-Object System.Media.SoundPlayer '${wav}'`,
141
- " $p.Load()",
142
- " $p.PlaySync()",
143
- "}",
144
- "[System.Media.SystemSounds]::Asterisk.Play()",
145
- "Start-Sleep -Milliseconds 250",
146
- "",
147
- ].join("\n"), "utf8");
148
- const ps1 = PS1_PATH.replace(/"/g, "\"\"");
149
- await writeFile(VBS_PATH, [
150
- "Set shell = CreateObject(\"WScript.Shell\")",
151
- `shell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ""${ps1}""", 0, False`,
152
- "",
153
- ].join("\r\n"), "utf8");
154
- }
155
-
156
- function createTuneWav(): Buffer {
157
- const sampleRate = 44100;
158
- const notes = [
159
- [659, 0.11],
160
- [784, 0.11],
161
- [988, 0.15],
162
- [1319, 0.21],
163
- [1175, 0.12],
164
- [1319, 0.26],
165
- ];
166
- const gapSeconds = 0.035;
167
- const samples: number[] = [];
168
-
169
- for (const [freq, seconds] of notes) {
170
- const count = Math.floor(sampleRate * seconds);
171
- for (let i = 0; i < count; i++) {
172
- const t = i / sampleRate;
173
- const fadeIn = Math.min(1, i / 120);
174
- const fadeOut = Math.min(1, (count - i) / 300);
175
- const env = Math.min(fadeIn, fadeOut) * 0.28;
176
- samples.push(Math.sin(2 * Math.PI * freq * t) * env);
177
- }
178
- for (let i = 0; i < Math.floor(sampleRate * gapSeconds); i++) samples.push(0);
179
- }
180
-
181
- const dataSize = samples.length * 2;
182
- const buffer = Buffer.alloc(44 + dataSize);
183
- buffer.write("RIFF", 0);
184
- buffer.writeUInt32LE(36 + dataSize, 4);
185
- buffer.write("WAVE", 8);
186
- buffer.write("fmt ", 12);
187
- buffer.writeUInt32LE(16, 16);
188
- buffer.writeUInt16LE(1, 20);
189
- buffer.writeUInt16LE(1, 22);
190
- buffer.writeUInt32LE(sampleRate, 24);
191
- buffer.writeUInt32LE(sampleRate * 2, 28);
192
- buffer.writeUInt16LE(2, 32);
193
- buffer.writeUInt16LE(16, 34);
194
- buffer.write("data", 36);
195
- buffer.writeUInt32LE(dataSize, 40);
196
-
197
- let offset = 44;
198
- for (const sample of samples) {
199
- const value = Math.max(-1, Math.min(1, sample));
200
- buffer.writeInt16LE(Math.round(value * 32767), offset);
201
- offset += 2;
202
- }
203
- return buffer;
204
- }
205
-
206
- function updateStatus(ctx: ExtensionContext): void {
207
- if (!ctx.hasUI) return;
208
- ctx.ui.setStatus("notify-sound", ctx.ui.theme.fg("dim", `tune:${config.enabled ? "on" : "off"}`));
209
- }
210
-
211
- function playCompletionTune(method: NotifyMethod = "auto"): void {
212
- const os = platform();
213
-
214
- if (os === "win32") {
215
- playWindowsTune(method);
216
- return;
217
- }
218
-
219
- if (os === "darwin") {
220
- runDetached("osascript", ["-e", "beep 1", "-e", "delay 0.12", "-e", "beep 1", "-e", "delay 0.12", "-e", "beep 2"]);
221
- return;
222
- }
223
-
224
- playLinuxTune();
225
- }
226
-
227
- function playWindowsTune(method: NotifyMethod): void {
228
- // Windows uses the generated WAV path because Console.Beep, system sounds,
229
- // and terminal bells are silent on many modern Windows machines.
230
- playWindowsWav();
231
- }
232
-
233
- function playWindowsWav(): void {
234
- // WScript launches the PowerShell player hidden without stealing focus. This
235
- // is more reliable than spawning hidden PowerShell directly from Pi's TUI.
236
- runDetached("wscript.exe", [VBS_PATH]);
237
- }
238
-
239
- function playLinuxTune(): void {
240
- // Prefer shell printf bells for broad compatibility; terminal may silence them.
241
- // If paplay/aplay exists, play the freedesktop complete sound as an additional fallback.
242
- runDetached("sh", [
243
- "-c",
244
- "(command -v paplay >/dev/null 2>&1 && paplay /usr/share/sounds/freedesktop/stereo/complete.oga >/dev/null 2>&1) || " +
245
- "(command -v aplay >/dev/null 2>&1 && aplay /usr/share/sounds/alsa/Front_Center.wav >/dev/null 2>&1) || " +
246
- "printf '\\a'; sleep 0.12; printf '\\a'; sleep 0.12; printf '\\a'",
247
- ]);
248
- }
249
-
250
- function runDetached(command: string, args: string[]): void {
251
- try {
252
- const child = spawn(command, args, {
253
- detached: true,
254
- stdio: "ignore",
255
- windowsHide: true,
256
- shell: false,
257
- });
258
- child.unref();
259
- } catch {
260
- // Notification failures should never interrupt pi.
261
- }
262
- }
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir, platform } from "node:os";
5
+ import { spawn } from "node:child_process";
6
+
7
+ type NotifySoundConfig = {
8
+ enabled: boolean;
9
+ };
10
+
11
+ type NotifyMethod = "auto" | "wav";
12
+
13
+ const GLOBAL_NOTIFY_DIR = join(homedir(), ".pi", "agent", "notify-sound");
14
+ const CONFIG_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.json");
15
+ const WAV_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.wav");
16
+ const PS1_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.ps1");
17
+ const VBS_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.vbs");
18
+ const STALE_AGENT_START_MS = 24 * 60 * 60 * 1000;
19
+
20
+ let config: NotifySoundConfig = { enabled: true };
21
+ let lastAgentStartedAt = 0;
22
+
23
+ export default async function notifySoundExtension(pi: ExtensionAPI) {
24
+ await loadConfig();
25
+ await ensureTuneWav();
26
+
27
+ pi.on("session_start", async (_event, ctx) => {
28
+ updateStatus(ctx);
29
+ });
30
+
31
+ pi.on("agent_start", async () => {
32
+ lastAgentStartedAt = Date.now();
33
+ });
34
+
35
+ pi.on("agent_end", async (_event, ctx) => {
36
+ updateStatus(ctx);
37
+
38
+ if (!config.enabled) return;
39
+ if (!lastAgentStartedAt) return;
40
+ if (Date.now() - lastAgentStartedAt > STALE_AGENT_START_MS) return;
41
+
42
+ playCompletionTune();
43
+ });
44
+
45
+ const command = {
46
+ description: "Toggle or test the agent completion tune notification",
47
+ getArgumentCompletions: (argumentPrefix: string) => {
48
+ const token = argumentPrefix.trim().toLowerCase();
49
+ return [
50
+ { value: "status", label: "status", description: "Show whether the completion tune is on" },
51
+ { value: "test", label: "test", description: "Play the completion tune" },
52
+ { value: "test wav", label: "test wav", description: "Play the generated WAV completion tune" },
53
+ { value: "on", label: "on", description: "Enable the completion tune" },
54
+ { value: "off", label: "off", description: "Disable the completion tune" },
55
+ { value: "toggle", label: "toggle", description: "Toggle the completion tune" },
56
+ ].filter((completion) => !token || completion.value.startsWith(token));
57
+ },
58
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
59
+ const action = args.trim().toLowerCase() || "toggle";
60
+
61
+ if (action === "test") {
62
+ playCompletionTune("wav");
63
+ ctx.ui.notify("Played completion tune using WAV mode. This is the default reliable Windows path.", "info");
64
+ return;
65
+ }
66
+
67
+ if (action === "test wav") {
68
+ playCompletionTune("wav");
69
+ ctx.ui.notify("Played completion tune using WAV mode.", "info");
70
+ return;
71
+ }
72
+
73
+ if (action.startsWith("test ")) {
74
+ ctx.ui.notify("Usage: /notify test", "warning");
75
+ return;
76
+ }
77
+
78
+ if (action === "status") {
79
+ updateStatus(ctx);
80
+ ctx.ui.notify(`Completion tune is ${config.enabled ? "ON" : "OFF"}.`, "info");
81
+ return;
82
+ }
83
+
84
+ if (action === "on") {
85
+ await setEnabled(true, ctx);
86
+ return;
87
+ }
88
+
89
+ if (action === "off") {
90
+ await setEnabled(false, ctx);
91
+ return;
92
+ }
93
+
94
+ if (action === "toggle") {
95
+ await setEnabled(!config.enabled, ctx);
96
+ return;
97
+ }
98
+
99
+ ctx.ui.notify("Usage: /notify [test|status|on|off|toggle]", "warning");
100
+ },
101
+ };
102
+
103
+ pi.registerCommand("notify-sound", command);
104
+ pi.registerCommand("notify", {
105
+ ...command,
106
+ description: "Alias for /notify-sound",
107
+ });
108
+ }
109
+
110
+ async function loadConfig(): Promise<void> {
111
+ try {
112
+ const raw = await readFile(CONFIG_PATH, "utf8");
113
+ const parsed = JSON.parse(raw) as Partial<NotifySoundConfig>;
114
+ config = { enabled: parsed.enabled !== false };
115
+ } catch {
116
+ config = { enabled: true };
117
+ await saveConfig();
118
+ }
119
+ }
120
+
121
+ async function saveConfig(): Promise<void> {
122
+ await mkdir(dirname(CONFIG_PATH), { recursive: true });
123
+ await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
124
+ }
125
+
126
+ async function setEnabled(enabled: boolean, ctx: ExtensionContext): Promise<void> {
127
+ config.enabled = enabled;
128
+ await saveConfig();
129
+ updateStatus(ctx);
130
+ ctx.ui.notify(`Completion tune ${enabled ? "enabled" : "disabled"}.`, "info");
131
+ }
132
+
133
+ async function ensureTuneWav(): Promise<void> {
134
+ await mkdir(dirname(WAV_PATH), { recursive: true });
135
+ await writeFile(WAV_PATH, createTuneWav(), "binary");
136
+ const wav = WAV_PATH.replace(/'/g, "''");
137
+ await writeFile(PS1_PATH, [
138
+ "$ErrorActionPreference = 'SilentlyContinue'",
139
+ `if (Test-Path '${wav}') {`,
140
+ ` $p = New-Object System.Media.SoundPlayer '${wav}'`,
141
+ " $p.Load()",
142
+ " $p.PlaySync()",
143
+ "}",
144
+ "[System.Media.SystemSounds]::Asterisk.Play()",
145
+ "Start-Sleep -Milliseconds 250",
146
+ "",
147
+ ].join("\n"), "utf8");
148
+ const ps1 = PS1_PATH.replace(/"/g, "\"\"");
149
+ await writeFile(VBS_PATH, [
150
+ "Set shell = CreateObject(\"WScript.Shell\")",
151
+ `shell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ""${ps1}""", 0, False`,
152
+ "",
153
+ ].join("\r\n"), "utf8");
154
+ }
155
+
156
+ function createTuneWav(): Buffer {
157
+ const sampleRate = 44100;
158
+ const notes = [
159
+ [659, 0.11],
160
+ [784, 0.11],
161
+ [988, 0.15],
162
+ [1319, 0.21],
163
+ [1175, 0.12],
164
+ [1319, 0.26],
165
+ ];
166
+ const gapSeconds = 0.035;
167
+ const samples: number[] = [];
168
+
169
+ for (const [freq, seconds] of notes) {
170
+ const count = Math.floor(sampleRate * seconds);
171
+ for (let i = 0; i < count; i++) {
172
+ const t = i / sampleRate;
173
+ const fadeIn = Math.min(1, i / 120);
174
+ const fadeOut = Math.min(1, (count - i) / 300);
175
+ const env = Math.min(fadeIn, fadeOut) * 0.28;
176
+ samples.push(Math.sin(2 * Math.PI * freq * t) * env);
177
+ }
178
+ for (let i = 0; i < Math.floor(sampleRate * gapSeconds); i++) samples.push(0);
179
+ }
180
+
181
+ const dataSize = samples.length * 2;
182
+ const buffer = Buffer.alloc(44 + dataSize);
183
+ buffer.write("RIFF", 0);
184
+ buffer.writeUInt32LE(36 + dataSize, 4);
185
+ buffer.write("WAVE", 8);
186
+ buffer.write("fmt ", 12);
187
+ buffer.writeUInt32LE(16, 16);
188
+ buffer.writeUInt16LE(1, 20);
189
+ buffer.writeUInt16LE(1, 22);
190
+ buffer.writeUInt32LE(sampleRate, 24);
191
+ buffer.writeUInt32LE(sampleRate * 2, 28);
192
+ buffer.writeUInt16LE(2, 32);
193
+ buffer.writeUInt16LE(16, 34);
194
+ buffer.write("data", 36);
195
+ buffer.writeUInt32LE(dataSize, 40);
196
+
197
+ let offset = 44;
198
+ for (const sample of samples) {
199
+ const value = Math.max(-1, Math.min(1, sample));
200
+ buffer.writeInt16LE(Math.round(value * 32767), offset);
201
+ offset += 2;
202
+ }
203
+ return buffer;
204
+ }
205
+
206
+ function updateStatus(ctx: ExtensionContext): void {
207
+ if (!ctx.hasUI) return;
208
+ ctx.ui.setStatus("notify-sound", ctx.ui.theme.fg("dim", `tune:${config.enabled ? "on" : "off"}`));
209
+ }
210
+
211
+ function playCompletionTune(method: NotifyMethod = "auto"): void {
212
+ const os = platform();
213
+
214
+ if (os === "win32") {
215
+ playWindowsTune(method);
216
+ return;
217
+ }
218
+
219
+ if (os === "darwin") {
220
+ runDetached("osascript", ["-e", "beep 1", "-e", "delay 0.12", "-e", "beep 1", "-e", "delay 0.12", "-e", "beep 2"]);
221
+ return;
222
+ }
223
+
224
+ playLinuxTune();
225
+ }
226
+
227
+ function playWindowsTune(method: NotifyMethod): void {
228
+ // Windows uses the generated WAV path because Console.Beep, system sounds,
229
+ // and terminal bells are silent on many modern Windows machines.
230
+ playWindowsWav();
231
+ }
232
+
233
+ function playWindowsWav(): void {
234
+ // WScript launches the PowerShell player hidden without stealing focus. This
235
+ // is more reliable than spawning hidden PowerShell directly from Pi's TUI.
236
+ runDetached("wscript.exe", [VBS_PATH]);
237
+ }
238
+
239
+ function playLinuxTune(): void {
240
+ // Prefer shell printf bells for broad compatibility; terminal may silence them.
241
+ // If paplay/aplay exists, play the freedesktop complete sound as an additional fallback.
242
+ runDetached("sh", [
243
+ "-c",
244
+ "(command -v paplay >/dev/null 2>&1 && paplay /usr/share/sounds/freedesktop/stereo/complete.oga >/dev/null 2>&1) || " +
245
+ "(command -v aplay >/dev/null 2>&1 && aplay /usr/share/sounds/alsa/Front_Center.wav >/dev/null 2>&1) || " +
246
+ "printf '\\a'; sleep 0.12; printf '\\a'; sleep 0.12; printf '\\a'",
247
+ ]);
248
+ }
249
+
250
+ function runDetached(command: string, args: string[]): void {
251
+ try {
252
+ const child = spawn(command, args, {
253
+ detached: true,
254
+ stdio: "ignore",
255
+ windowsHide: true,
256
+ shell: false,
257
+ });
258
+ child.unref();
259
+ } catch {
260
+ // Notification failures should never interrupt pi.
261
+ }
262
+ }
@@ -28,6 +28,7 @@ Pi extension that auto-loads and registers an `oauth-router` provider with multi
28
28
  - 429 cooldowns
29
29
  - transient failure penalties
30
30
  - client/network transport failure tracking without default account cooldown
31
+ - live Pi UI footer/notification updates when an upstream attempt, retry delay, failover, or final router error happens
31
32
  - 5 router-level client/network retries by default before pre-output failover, starting at 5s and doubling
32
33
  - auth failure quarantine
33
34
  - safe pre-output failover
@@ -47,7 +48,7 @@ The extension ships with two default upstream profiles:
47
48
  - api: `openai-responses`
48
49
  - default models: `gpt-4o`, `gpt-4.1`, `o4-mini`
49
50
 
50
- Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, change model catalogs, or tune retry behavior. By default client-side transport failures such as `Codex SSE response headers timed out after 10000ms` retry the same account 5 times with exponential backoff (`5s`, `10s`, `20s`, `40s`, then capped at `60s`) before router failover. These failures are recorded but do not cool down an account unless `clientNetworkPenaltyMs` is set above `0`.
51
+ Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, change model catalogs, or tune retry behavior. By default client-side transport failures such as `Codex SSE response headers timed out after 10000ms` retry the same account 5 times with exponential backoff (`5s`, `10s`, `20s`, `40s`, then capped at `60s`) before router failover. While this is happening, Pi's footer shows the active retry/failover/error state so the UI no longer looks frozen. These failures are recorded but do not cool down an account unless `clientNetworkPenaltyMs` is set above `0`.
51
52
 
52
53
  ## Setup
53
54