lsd-pi 1.1.4 → 1.1.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.
Files changed (151) hide show
  1. package/README.md +2 -1
  2. package/dist/resources/extensions/async-jobs/async-bash-tool.js +14 -0
  3. package/dist/resources/extensions/async-jobs/await-tool.js +14 -0
  4. package/dist/resources/extensions/async-jobs/cancel-job-tool.js +7 -0
  5. package/dist/resources/extensions/cache-timer/index.js +4 -0
  6. package/dist/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  7. package/dist/resources/extensions/codex-rotate/README.md +9 -3
  8. package/dist/resources/extensions/codex-rotate/commands.js +15 -8
  9. package/dist/resources/extensions/codex-rotate/index.js +17 -8
  10. package/dist/resources/extensions/memory/auto-extract.js +160 -77
  11. package/dist/resources/extensions/shared/rtk.js +89 -87
  12. package/dist/resources/extensions/subagent/index.js +32 -7
  13. package/dist/startup-model-validation.js +12 -2
  14. package/dist/update-check.js +2 -2
  15. package/dist/update-cmd.js +3 -3
  16. package/package.json +2 -1
  17. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +8 -0
  18. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  19. package/packages/pi-coding-agent/dist/core/agent-session.js +23 -1
  20. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  21. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +3 -1
  22. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  23. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  24. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  25. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  26. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  27. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts +48 -0
  29. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts.map +1 -0
  30. package/packages/pi-coding-agent/dist/core/pty-executor.js +173 -0
  31. package/packages/pi-coding-agent/dist/core/pty-executor.js.map +1 -0
  32. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  33. package/packages/pi-coding-agent/dist/core/sdk.js +16 -3
  34. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  35. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  36. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  37. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  38. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  39. package/packages/pi-coding-agent/dist/core/tool-approval.d.ts.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/tool-approval.js +2 -2
  41. package/packages/pi-coding-agent/dist/core/tool-approval.js.map +1 -1
  42. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +7 -0
  43. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/tools/index.js +23 -2
  45. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +50 -0
  47. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -0
  48. package/packages/pi-coding-agent/dist/core/tools/pty.js +289 -0
  49. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -0
  50. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +3 -5
  55. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +20 -53
  57. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -4
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -4
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts +39 -0
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts.map +1 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +182 -0
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -4
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +0 -2
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +79 -63
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +11 -0
  80. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +47 -11
  82. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  85. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +2 -0
  88. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  90. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +27 -0
  93. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +241 -37
  95. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +2 -2
  97. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts +10 -0
  99. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts.map +1 -0
  100. package/packages/pi-coding-agent/dist/utils/terminal-screen.js +67 -0
  101. package/packages/pi-coding-agent/dist/utils/terminal-screen.js.map +1 -0
  102. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts +7 -0
  103. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts.map +1 -0
  104. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js +67 -0
  105. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js.map +1 -0
  106. package/packages/pi-coding-agent/package.json +9 -4
  107. package/packages/pi-coding-agent/src/core/agent-session.ts +29 -1
  108. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  109. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  110. package/packages/pi-coding-agent/src/core/pty-executor.ts +229 -0
  111. package/packages/pi-coding-agent/src/core/sdk.ts +16 -3
  112. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  113. package/packages/pi-coding-agent/src/core/tool-approval.ts +2 -2
  114. package/packages/pi-coding-agent/src/core/tools/index.ts +35 -2
  115. package/packages/pi-coding-agent/src/core/tools/pty.ts +354 -0
  116. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +9 -0
  117. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +19 -59
  118. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -3
  119. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -3
  120. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +224 -0
  121. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  122. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -3
  123. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +77 -66
  124. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +52 -10
  125. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  126. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +2 -0
  127. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  128. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +286 -46
  129. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +2 -2
  130. package/packages/pi-coding-agent/src/utils/terminal-screen.ts +77 -0
  131. package/packages/pi-coding-agent/src/utils/terminal-serializer.ts +72 -0
  132. package/packages/pi-tui/dist/components/loader.d.ts +26 -6
  133. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  134. package/packages/pi-tui/dist/components/loader.js +178 -18
  135. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  136. package/packages/pi-tui/src/components/loader.ts +196 -19
  137. package/pkg/dist/modes/interactive/theme/themes.js +2 -2
  138. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  139. package/pkg/package.json +1 -1
  140. package/src/resources/extensions/async-jobs/async-bash-tool.ts +13 -0
  141. package/src/resources/extensions/async-jobs/await-tool.ts +13 -0
  142. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +8 -0
  143. package/src/resources/extensions/cache-timer/index.ts +101 -96
  144. package/src/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  145. package/src/resources/extensions/codex-rotate/README.md +9 -3
  146. package/src/resources/extensions/codex-rotate/commands.ts +335 -329
  147. package/src/resources/extensions/codex-rotate/index.ts +85 -75
  148. package/src/resources/extensions/memory/auto-extract.ts +297 -203
  149. package/src/resources/extensions/memory/tests/auto-extract.test.ts +191 -144
  150. package/src/resources/extensions/shared/rtk.js +112 -0
  151. package/src/resources/extensions/subagent/index.ts +34 -6
package/README.md CHANGED
@@ -242,8 +242,9 @@ A few quality-of-life touches in the TUI:
242
242
 
243
243
  - the footer can show a live cache timer for the current prompt-cache window
244
244
  - `/hotkeys` gives you a full shortcut reference on demand
245
- - `/settings` now includes toggles for Codex rotate, the cache timer, RTK shell-command compression, and a configurable **Main accent** preset
245
+ - `/settings` now includes toggles for Codex rotate, the cache timer, **Pin last prompt**, RTK shell-command compression, and a configurable **Main accent** preset
246
246
  - changing the main accent also updates accent-driven UI elements and the text input border across thinking levels
247
+ - when **Pin last prompt** is enabled, LSD keeps your most recent non-command prompt visible above the editor as a lightweight reminder
247
248
 
248
249
  Some workflow/automation commands still use the legacy namespace:
249
250
 
@@ -6,6 +6,7 @@
6
6
  * with await_job.
7
7
  */
8
8
  import { getShellConfig, sanitizeCommand, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, } from "@gsd/pi-coding-agent";
9
+ import { Text } from "@gsd/pi-tui";
9
10
  import { Type } from "@sinclair/typebox";
10
11
  import { spawn, spawnSync } from "node:child_process";
11
12
  import { createWriteStream } from "node:fs";
@@ -70,6 +71,19 @@ export function createAsyncBashTool(getManager, getCwd) {
70
71
  "Check /jobs to see all running and recent background jobs.",
71
72
  ],
72
73
  parameters: schema,
74
+ renderCall(args, theme, options) {
75
+ const cmd = args.command ?? "";
76
+ const display = cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
77
+ const indicator = options?.statusIndicator ? `${options.statusIndicator} ` : "";
78
+ let text = indicator + theme.fg("toolTitle", theme.bold("async_bash "));
79
+ text += theme.fg("bashMode", "$ ");
80
+ text += theme.fg("accent", display || theme.fg("muted", "..."));
81
+ if (args.label)
82
+ text += theme.fg("muted", ` (${args.label})`);
83
+ if (args.timeout)
84
+ text += theme.fg("dim", ` timeout:${args.timeout}s`);
85
+ return new Text(text, 0, 0);
86
+ },
73
87
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
74
88
  const manager = getManager();
75
89
  const cwd = getCwd();
@@ -4,6 +4,7 @@
4
4
  * If specific job IDs are provided, waits for those jobs.
5
5
  * If omitted, waits for any running job to complete.
6
6
  */
7
+ import { Text } from "@gsd/pi-tui";
7
8
  import { Type } from "@sinclair/typebox";
8
9
  const DEFAULT_TIMEOUT_SECONDS = 120;
9
10
  const schema = Type.Object({
@@ -21,6 +22,19 @@ export function createAwaitTool(getManager) {
21
22
  label: "Await Background Job",
22
23
  description: "Wait for background jobs to complete. Provide specific job IDs or omit to wait for the next job that finishes. Returns results of completed jobs.",
23
24
  parameters: schema,
25
+ renderCall(args, theme, options) {
26
+ const indicator = options?.statusIndicator ? `${options.statusIndicator} ` : "";
27
+ let text = indicator + theme.fg("toolTitle", theme.bold("await_job"));
28
+ if (args.jobs && args.jobs.length > 0) {
29
+ text += " " + theme.fg("accent", args.jobs.join(", "));
30
+ }
31
+ else {
32
+ text += theme.fg("muted", " (any)");
33
+ }
34
+ if (args.timeout)
35
+ text += theme.fg("dim", ` timeout:${args.timeout}s`);
36
+ return new Text(text, 0, 0);
37
+ },
24
38
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
25
39
  const manager = getManager();
26
40
  const { jobs: jobIds, timeout } = params;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * cancel_job tool — cancel a running background job.
3
3
  */
4
+ import { Text } from "@gsd/pi-tui";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  const schema = Type.Object({
6
7
  job_id: Type.String({ description: "The background job ID to cancel (e.g. bg_a1b2c3d4)" }),
@@ -11,6 +12,12 @@ export function createCancelJobTool(getManager) {
11
12
  label: "Cancel Background Job",
12
13
  description: "Cancel a running background job by its ID.",
13
14
  parameters: schema,
15
+ renderCall(args, theme, options) {
16
+ const indicator = options?.statusIndicator ? `${options.statusIndicator} ` : "";
17
+ let text = indicator + theme.fg("toolTitle", theme.bold("cancel_job "));
18
+ text += theme.fg("accent", args.job_id);
19
+ return new Text(text, 0, 0);
20
+ },
14
21
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
15
22
  const manager = getManager();
16
23
  const result = manager.cancel(params.job_id);
@@ -14,6 +14,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { getAgentDir } from "@gsd/pi-coding-agent";
16
16
  const STATUS_KEY = "cache-timer";
17
+ const IS_MEMORY_MAINTENANCE_WORKER = process.env.LSD_MEMORY_EXTRACT === "1" || process.env.LSD_MEMORY_DREAM === "1";
17
18
  // ANSI color codes for timer display
18
19
  const ANSI_RESET = "\x1b[0m";
19
20
  const ANSI_YELLOW = "\x1b[33m";
@@ -65,6 +66,9 @@ function formatElapsed(ms) {
65
66
  return `⏱ ${time}`;
66
67
  }
67
68
  export default function cacheTimerExtension(pi) {
69
+ if (IS_MEMORY_MAINTENANCE_WORKER) {
70
+ return;
71
+ }
68
72
  let timer = null;
69
73
  let startTime = null;
70
74
  let enabled = readEnabled();
@@ -18,6 +18,7 @@ Implemented a Codex OAuth rotation extension for LSD that manages multiple ChatG
18
18
  - Writes `openai-codex` credential array with `api_key` type
19
19
  - Avoids the `set()` method (which appends duplicates)
20
20
  - Maintains stable credential order for index-based backoff
21
+ - Followed by a live `AuthStorage.reload()` in the extension so the running process sees account changes immediately
21
22
 
22
23
  3. **Background Refresh Timer** (`index.ts`)
23
24
  - Runs every 10 minutes
@@ -42,11 +43,10 @@ Implemented a Codex OAuth rotation extension for LSD that manages multiple ChatG
42
43
  - `/codex import-cockpit` - Import from Cockpit
43
44
  - `/codex sync` - Force refresh all tokens
44
45
 
45
- 6. **Error Detection** (`quota.ts`)
46
- - Detects quota/rate limit errors
47
- - Detects auth errors (401, expired tokens)
48
- - Classifies errors by type
49
- - Integrates with `AuthStorage.markUsageLimitReached()`
46
+ 6. **Error Detection / Rotation Path**
47
+ - Quota/rate-limit/auth classification lives in `quota.ts`
48
+ - Same-turn retry and per-credential backoff are performed by core `RetryHandler`
49
+ - The extension's responsibility is to keep `auth.json` and the live in-memory auth state in sync
50
50
 
51
51
  ## File Structure
52
52
 
@@ -87,6 +87,7 @@ Using `FileAuthStorageBackend.withLockAsync()`:
87
87
  - Prevents race conditions with multiple LSD instances
88
88
  - Replaces entire credential array (not just updating keys)
89
89
  - Maintains stable order for index-based backoff
90
+ - Requires a follow-up live auth reload in the current process so retry rotation uses the updated credential set
90
91
 
91
92
  ### 4. Session-Sticky Credential Selection
92
93
 
@@ -105,15 +106,15 @@ LSD already uses session-sticky selection by default:
105
106
 
106
107
  ## Next Steps: Phase 2 (Resilience)
107
108
 
108
- The following features were planned but not yet implemented:
109
+ Remaining improvements that could still be added:
109
110
 
110
- 1. **Enhanced error detection in `agent_end`**
111
- - Already implemented basic error detection
112
- - Could add more sophisticated quota pattern matching
111
+ 1. **Richer `/codex status` diagnostics**
112
+ - Show live backoff / credential-availability state from `AuthStorage`
113
+ - Make it easier to verify when an account was rotated away after a usage-limit hit
113
114
 
114
- 2. **Better credential change listener**
115
- - Could listen for external auth.json modifications
116
- - Would re-sync on change detection
115
+ 2. **External change detection**
116
+ - Could listen for external auth/account file modifications
117
+ - Would re-sync / reload on change detection
117
118
 
118
119
  3. **Migration helpers**
119
120
  - Could add `/codex migrate` to assist users moving from Cockpit
@@ -121,8 +122,12 @@ The following features were planned but not yet implemented:
121
122
 
122
123
  ## Testing
123
124
 
124
- To test the extension:
125
+ Implemented regression coverage now includes:
126
+ - `src/tests/codex-rotate-auth-reload.test.ts`
127
+ - Verifies that codex-rotate syncs require a live auth reload for the running process to see the latest credentials
128
+ - Verifies `quota_exhausted` backoff on one `openai-codex` credential causes the next retry to select the next credential
125
129
 
130
+ Manual smoke test:
126
131
  1. Add an account: `/codex add`
127
132
  2. Check status: `/codex status`
128
133
  3. Add another account: `/codex add`
@@ -97,14 +97,20 @@ Codex access tokens work identically to API keys:
97
97
  - Background timer runs every 10 minutes
98
98
  - Tokens are refreshed when expiring within 5 minutes
99
99
  - Refreshed tokens are atomically synced to auth.json
100
+ - After every successful sync, the extension reloads live `AuthStorage` so retries and rotation see the latest credentials immediately
100
101
  - Failed refreshes disable the account with a reason
101
102
 
102
103
  ### Error Handling
103
104
 
104
- The extension hooks into `agent_end` events to detect:
105
+ The extension now relies on LSD core retry/backoff handling rather than its own `agent_end` hook:
106
+ - The Codex provider surfaces friendly usage-limit / rate-limit / auth errors
107
+ - Core `RetryHandler` classifies those failures and calls `markUsageLimitReached(...)`
108
+ - If another `openai-codex` credential is available, LSD automatically rotates and retries the same prompt
105
109
  - Rate limit errors (429) → 30s backoff
106
- - Quota exhausted errors → 30min backoff
107
- - Auth errors (401) → treated as rate limits for immediate rotation
110
+ - Quota exhausted / usage limit errors → 30min backoff
111
+ - Auth errors (401) → treated as immediate credential-rotation failures
112
+
113
+ To make that work reliably, the extension reloads the live auth state after every successful sync so the retry handler sees the current credential pool immediately.
108
114
 
109
115
  ## Security
110
116
 
@@ -63,6 +63,13 @@ function displayAccounts(ctx, accounts) {
63
63
  });
64
64
  ctx.ui.notify(lines.join("\n"), "info");
65
65
  }
66
+ async function syncAccountsToAuthAndReload(ctx) {
67
+ const synced = await syncAccountsToAuth(getAllAccounts());
68
+ if (synced) {
69
+ ctx.modelRegistry.authStorage.reload();
70
+ }
71
+ return synced;
72
+ }
66
73
  /**
67
74
  * Register the /codex command
68
75
  */
@@ -118,7 +125,7 @@ export function registerCodexCommand(pi) {
118
125
  disabled: false,
119
126
  });
120
127
  // Sync to auth.json
121
- const success = await syncAccountsToAuth(getAllAccounts());
128
+ const success = await syncAccountsToAuthAndReload(ctx);
122
129
  if (success) {
123
130
  ctx.ui.notify(`Added account: ${email || account.accountId}. Synced to auth.json.`, "success");
124
131
  }
@@ -176,7 +183,7 @@ export function registerCodexCommand(pi) {
176
183
  const confirmed = await ctx.ui.select(`Remove account: ${account.email || account.accountId}?`, ["Yes, remove", "Cancel"], { signal: AbortSignal.timeout(30000) });
177
184
  if (confirmed === "Yes, remove") {
178
185
  removeAccount(account.id);
179
- await syncAccountsToAuth(getAllAccounts());
186
+ await syncAccountsToAuthAndReload(ctx);
180
187
  ctx.ui.notify(`Removed account: ${account.email || account.accountId}`, "success");
181
188
  }
182
189
  return;
@@ -200,7 +207,7 @@ export function registerCodexCommand(pi) {
200
207
  return;
201
208
  }
202
209
  updateAccount(account.id, { disabled: false, disabledReason: undefined });
203
- await syncAccountsToAuth(getAllAccounts());
210
+ await syncAccountsToAuthAndReload(ctx);
204
211
  ctx.ui.notify(`Enabled account: ${account.email || account.accountId}`, "success");
205
212
  return;
206
213
  }
@@ -223,7 +230,7 @@ export function registerCodexCommand(pi) {
223
230
  return;
224
231
  }
225
232
  updateAccount(account.id, { disabled: true, disabledReason: "manually disabled" });
226
- await syncAccountsToAuth(getAllAccounts());
233
+ await syncAccountsToAuthAndReload(ctx);
227
234
  ctx.ui.notify(`Disabled account: ${account.email || account.accountId}`, "success");
228
235
  return;
229
236
  }
@@ -234,7 +241,7 @@ export function registerCodexCommand(pi) {
234
241
  ctx.ui.notify("No account found to import.", "warning");
235
242
  return;
236
243
  }
237
- const account = addAccount({
244
+ addAccount({
238
245
  email: imported.email,
239
246
  accountId: imported.accountId,
240
247
  refreshToken: imported.refreshToken,
@@ -243,7 +250,7 @@ export function registerCodexCommand(pi) {
243
250
  lastUsed: undefined,
244
251
  disabled: false,
245
252
  });
246
- await syncAccountsToAuth(getAllAccounts());
253
+ await syncAccountsToAuthAndReload(ctx);
247
254
  ctx.ui.notify(`Imported account: ${imported.email || imported.accountId}`, "success");
248
255
  return;
249
256
  }
@@ -257,7 +264,7 @@ export function registerCodexCommand(pi) {
257
264
  for (const acc of imported) {
258
265
  addAccount(acc);
259
266
  }
260
- await syncAccountsToAuth(getAllAccounts());
267
+ await syncAccountsToAuthAndReload(ctx);
261
268
  ctx.ui.notify(`Imported ${imported.length} account(s) from Cockpit Tools`, "success");
262
269
  return;
263
270
  }
@@ -278,7 +285,7 @@ export function registerCodexCommand(pi) {
278
285
  results.failed++;
279
286
  }
280
287
  }
281
- await syncAccountsToAuth(getAllAccounts());
288
+ await syncAccountsToAuthAndReload(ctx);
282
289
  if (results.failed === 0) {
283
290
  ctx.ui.notify(`Synced ${results.success} account(s) to auth.json`, "success");
284
291
  }
@@ -10,10 +10,13 @@ import { registerCodexCommand } from "./commands.js";
10
10
  import { REFRESH_INTERVAL_MS } from "./config.js";
11
11
  import { logCodexRotateError } from "./logger.js";
12
12
  let refreshTimer = null;
13
+ async function reloadLiveAuthState(ctx) {
14
+ ctx.modelRegistry.authStorage.reload();
15
+ }
13
16
  /**
14
17
  * Refresh all accounts that need it
15
18
  */
16
- async function refreshExpiringAccounts() {
19
+ async function refreshExpiringAccounts(ctx) {
17
20
  try {
18
21
  const accountsNeedingRefresh = getAccountsNeedingRefresh();
19
22
  if (accountsNeedingRefresh.length === 0) {
@@ -41,7 +44,10 @@ async function refreshExpiringAccounts() {
41
44
  if (successCount > 0) {
42
45
  // Sync refreshed accounts to auth.json
43
46
  const allAccounts = getAllAccounts();
44
- await syncAccountsToAuth(allAccounts);
47
+ const synced = await syncAccountsToAuth(allAccounts);
48
+ if (synced) {
49
+ await reloadLiveAuthState(ctx);
50
+ }
45
51
  }
46
52
  if (failCount > 0 && successCount === 0) {
47
53
  logCodexRotateError(`Failed to refresh ${failCount} account(s)`);
@@ -54,12 +60,12 @@ async function refreshExpiringAccounts() {
54
60
  /**
55
61
  * Start the background refresh timer
56
62
  */
57
- function startRefreshTimer() {
63
+ function startRefreshTimer(ctx) {
58
64
  if (refreshTimer) {
59
65
  clearInterval(refreshTimer);
60
66
  }
61
67
  refreshTimer = setInterval(() => {
62
- void refreshExpiringAccounts();
68
+ void refreshExpiringAccounts(ctx);
63
69
  }, REFRESH_INTERVAL_MS);
64
70
  }
65
71
  /**
@@ -78,17 +84,20 @@ export default function CodexRotateExtension(pi) {
78
84
  // Register commands
79
85
  registerCodexCommand(pi);
80
86
  // Session start hook
81
- pi.on("session_start", async (_event) => {
87
+ pi.on("session_start", async (_event, ctx) => {
82
88
  const accounts = getAllAccounts();
83
89
  if (accounts.length === 0) {
84
90
  return;
85
91
  }
86
92
  // Refresh any expiring accounts immediately
87
- await refreshExpiringAccounts();
93
+ await refreshExpiringAccounts(ctx);
88
94
  // Sync to auth.json
89
- await syncAccountsToAuth(getAllAccounts());
95
+ const synced = await syncAccountsToAuth(getAllAccounts());
96
+ if (synced) {
97
+ await reloadLiveAuthState(ctx);
98
+ }
90
99
  // Start background refresh timer
91
- startRefreshTimer();
100
+ startRefreshTimer(ctx);
92
101
  });
93
102
  // Session shutdown hook
94
103
  pi.on("session_shutdown", () => {