@victor-software-house/pi-multicodex 1.0.9 → 1.0.10

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 +53 -5
  2. package/package.json +1 -1
  3. package/status.ts +15 -10
package/README.md CHANGED
@@ -69,11 +69,59 @@ Current direction:
69
69
 
70
70
  Current next step:
71
71
 
72
- - mirror the existing codex usage footer style, including support for displaying both reset countdowns
73
- - debounce expensive refresh work during rapid model cycling
74
- - move each reset countdown next to its matching usage period
75
- - add live preview to the `/multicodex-footer` panel before locking the final style
76
- - tighten footer updates so account switches and quota rotation are reflected immediately
72
+ - refine the footer color palette with small visual adjustments only
73
+ - document the account-rotation behavior contract explicitly
74
+ - improve the `/multicodex-use` and `/multicodex-status` everyday UX
75
+
76
+ ## Behavior contract
77
+
78
+ The current runtime behavior is:
79
+
80
+ ### Account selection priority
81
+
82
+ 1. Use the manual account selected with `/multicodex-use` when it is still available.
83
+ 2. Otherwise clear the stale manual override and select the best available managed account.
84
+ 3. Best-account selection prefers:
85
+ - untouched accounts with usage data
86
+ - then the account whose weekly reset window ends first
87
+ - then a random available account as fallback
88
+
89
+ ### Quota exhaustion semantics
90
+
91
+ - Quota and rate-limit style failures are detected from provider error text.
92
+ - When a request fails before any output is streamed, MultiCodex marks that account exhausted and retries on another account.
93
+ - Exhaustion lasts until the next known reset time.
94
+ - If usage data does not provide a reset time, exhaustion falls back to a 1 hour cooldown.
95
+
96
+ ### Retry policy
97
+
98
+ - MultiCodex retries account rotation up to 5 times for a single request.
99
+ - Retries only happen for quota/rate-limit style failures that occur before output is forwarded.
100
+ - Once output has started streaming, the original error is surfaced instead of rotating.
101
+
102
+ ### Manual override behavior
103
+
104
+ - `/multicodex-use <identifier>` sets the manual account override immediately.
105
+ - `/multicodex-use` with no argument opens the account picker and sets the selected manual override.
106
+ - Manual override is session-local state.
107
+ - Manual override clears automatically when the selected account is no longer available or when it hits quota during rotation.
108
+
109
+ ### Usage cache and refresh rules
110
+
111
+ - Usage is cached in memory for 5 minutes per account.
112
+ - Footer updates render cached usage immediately and refresh in the background when needed.
113
+ - Rapid `model_select` changes debounce background refresh work so non-Codex model switching clears the footer immediately.
114
+
115
+ ### Error classification
116
+
117
+ Quota rotation currently treats these error classes as interchangeable:
118
+
119
+ - HTTP `429`
120
+ - `quota`
121
+ - `usage limit`
122
+ - `rate limit`
123
+ - `too many requests`
124
+ - `limit reached`
77
125
 
78
126
  ## Release validation
79
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-multicodex",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Codex account rotation extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/status.ts CHANGED
@@ -23,6 +23,7 @@ const SETTINGS_FILE = path.join(os.homedir(), ".pi", "agent", "settings.json");
23
23
  const REFRESH_INTERVAL_MS = 60_000;
24
24
  const MODEL_SELECT_REFRESH_DEBOUNCE_MS = 250;
25
25
  const UNKNOWN_PERCENT = "--";
26
+ const BRAND_LABEL = "Codex";
26
27
  const FIVE_HOUR_LABEL = "5h:";
27
28
  const SEVEN_DAY_LABEL = "7d:";
28
29
 
@@ -139,13 +140,21 @@ function usedToDisplayPercent(
139
140
  return mode === "left" ? left : clampPercent(100 - left);
140
141
  }
141
142
 
143
+ function formatBrand(ctx: ExtensionContext): string {
144
+ return ctx.ui.theme.fg("muted", BRAND_LABEL);
145
+ }
146
+
147
+ function formatLoading(ctx: ExtensionContext): string {
148
+ return ctx.ui.theme.fg("muted", "loading...");
149
+ }
150
+
142
151
  function formatPercent(
143
152
  ctx: ExtensionContext,
144
153
  displayPercent: number | undefined,
145
154
  mode: PercentDisplayMode,
146
155
  ): string {
147
156
  if (typeof displayPercent !== "number" || Number.isNaN(displayPercent)) {
148
- return ctx.ui.theme.fg("muted", UNKNOWN_PERCENT);
157
+ return ctx.ui.theme.fg("dim", UNKNOWN_PERCENT);
149
158
  }
150
159
 
151
160
  const text = `${Math.round(clampPercent(displayPercent))}% ${mode}`;
@@ -201,7 +210,7 @@ function formatUsageSegment(
201
210
  if (showReset) {
202
211
  const countdown = formatResetCountdown(resetAt);
203
212
  if (countdown) {
204
- parts.push(ctx.ui.theme.fg("dim", `(↺${countdown})`));
213
+ parts.push(ctx.ui.theme.fg("muted", `(↺${countdown})`));
205
214
  }
206
215
  }
207
216
  return parts.join(" ");
@@ -221,11 +230,7 @@ export function formatActiveAccountStatus(
221
230
  ? ctx.ui.theme.fg("muted", accountEmail)
222
231
  : undefined;
223
232
  if (!usage) {
224
- return [
225
- ctx.ui.theme.fg("dim", "Codex"),
226
- accountText,
227
- ctx.ui.theme.fg("dim", "loading..."),
228
- ]
233
+ return [formatBrand(ctx), accountText, formatLoading(ctx)]
229
234
  .filter(Boolean)
230
235
  .join(" ");
231
236
  }
@@ -249,8 +254,8 @@ export function formatActiveAccountStatus(
249
254
 
250
255
  const leading =
251
256
  preferences.order === "account-first"
252
- ? [ctx.ui.theme.fg("dim", "Codex"), accountText]
253
- : [ctx.ui.theme.fg("dim", "Codex")];
257
+ ? [formatBrand(ctx), accountText]
258
+ : [formatBrand(ctx)];
254
259
  const trailing =
255
260
  preferences.order === "account-first" ? [] : [accountText].filter(Boolean);
256
261
 
@@ -498,7 +503,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
498
503
  draft: FooterPreferences,
499
504
  ): string {
500
505
  const previewText =
501
- getStatusText(ctx, draft) ?? ctx.ui.theme.fg("dim", "Codex loading...");
506
+ getStatusText(ctx, draft) ?? `${formatBrand(ctx)} ${formatLoading(ctx)}`;
502
507
  return `${theme.fg("dim", "Preview")}: ${previewText}`;
503
508
  }
504
509