@victor-software-house/pi-multicodex 1.0.9 → 1.0.11
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 +53 -5
- package/package.json +1 -1
- package/status.ts +55 -29
package/README.md
CHANGED
|
@@ -69,11 +69,59 @@ Current direction:
|
|
|
69
69
|
|
|
70
70
|
Current next step:
|
|
71
71
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
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
package/status.ts
CHANGED
|
@@ -23,6 +23,8 @@ 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";
|
|
27
|
+
const SEGMENT_SEPARATOR = "·";
|
|
26
28
|
const FIVE_HOUR_LABEL = "5h:";
|
|
27
29
|
const SEVEN_DAY_LABEL = "7d:";
|
|
28
30
|
|
|
@@ -139,25 +141,48 @@ function usedToDisplayPercent(
|
|
|
139
141
|
return mode === "left" ? left : clampPercent(100 - left);
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
function
|
|
143
|
-
ctx
|
|
144
|
+
function formatBrand(ctx: ExtensionContext): string {
|
|
145
|
+
return ctx.ui.theme.fg("muted", BRAND_LABEL);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatLoading(ctx: ExtensionContext): string {
|
|
149
|
+
return ctx.ui.theme.fg("muted", "loading...");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatSeparator(ctx: ExtensionContext): string {
|
|
153
|
+
return ctx.ui.theme.fg("muted", SEGMENT_SEPARATOR);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getUsageSeverityToken(
|
|
144
157
|
displayPercent: number | undefined,
|
|
145
158
|
mode: PercentDisplayMode,
|
|
146
|
-
):
|
|
159
|
+
): "success" | "thinkingMedium" | "warning" | "error" | "dim" {
|
|
147
160
|
if (typeof displayPercent !== "number" || Number.isNaN(displayPercent)) {
|
|
148
|
-
return
|
|
161
|
+
return "dim";
|
|
149
162
|
}
|
|
150
163
|
|
|
151
|
-
const text = `${Math.round(clampPercent(displayPercent))}% ${mode}`;
|
|
152
164
|
if (mode === "left") {
|
|
153
|
-
if (displayPercent <= 10) return
|
|
154
|
-
if (displayPercent <= 25) return
|
|
155
|
-
return
|
|
165
|
+
if (displayPercent <= 10) return "error";
|
|
166
|
+
if (displayPercent <= 25) return "warning";
|
|
167
|
+
if (displayPercent <= 50) return "thinkingMedium";
|
|
168
|
+
return "success";
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
if (displayPercent >= 90) return
|
|
159
|
-
if (displayPercent >= 75) return
|
|
160
|
-
return
|
|
171
|
+
if (displayPercent >= 90) return "error";
|
|
172
|
+
if (displayPercent >= 75) return "warning";
|
|
173
|
+
if (displayPercent >= 50) return "thinkingMedium";
|
|
174
|
+
return "success";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatPercent(
|
|
178
|
+
displayPercent: number | undefined,
|
|
179
|
+
mode: PercentDisplayMode,
|
|
180
|
+
): string {
|
|
181
|
+
if (typeof displayPercent !== "number" || Number.isNaN(displayPercent)) {
|
|
182
|
+
return UNKNOWN_PERCENT;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return `${Math.round(clampPercent(displayPercent))}% ${mode}`;
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
function formatResetCountdown(resetAt: number | undefined): string | undefined {
|
|
@@ -191,20 +216,23 @@ function formatUsageSegment(
|
|
|
191
216
|
showReset: boolean,
|
|
192
217
|
preferences: FooterPreferences,
|
|
193
218
|
): string {
|
|
219
|
+
const displayPercent = usedToDisplayPercent(
|
|
220
|
+
usedPercent,
|
|
221
|
+
preferences.usageMode,
|
|
222
|
+
);
|
|
194
223
|
const parts = [
|
|
195
|
-
`${
|
|
196
|
-
ctx,
|
|
197
|
-
usedToDisplayPercent(usedPercent, preferences.usageMode),
|
|
198
|
-
preferences.usageMode,
|
|
199
|
-
)}`,
|
|
224
|
+
`${label}${formatPercent(displayPercent, preferences.usageMode)}`,
|
|
200
225
|
];
|
|
201
226
|
if (showReset) {
|
|
202
227
|
const countdown = formatResetCountdown(resetAt);
|
|
203
228
|
if (countdown) {
|
|
204
|
-
parts.push(
|
|
229
|
+
parts.push(`(↺${countdown})`);
|
|
205
230
|
}
|
|
206
231
|
}
|
|
207
|
-
return
|
|
232
|
+
return ctx.ui.theme.fg(
|
|
233
|
+
getUsageSeverityToken(displayPercent, preferences.usageMode),
|
|
234
|
+
parts.join(" "),
|
|
235
|
+
);
|
|
208
236
|
}
|
|
209
237
|
|
|
210
238
|
export function isManagedModel(model: MaybeModel): boolean {
|
|
@@ -218,14 +246,10 @@ export function formatActiveAccountStatus(
|
|
|
218
246
|
preferences: FooterPreferences,
|
|
219
247
|
): string {
|
|
220
248
|
const accountText = preferences.showAccount
|
|
221
|
-
? ctx.ui.theme.fg("
|
|
249
|
+
? ctx.ui.theme.fg("text", accountEmail)
|
|
222
250
|
: undefined;
|
|
223
251
|
if (!usage) {
|
|
224
|
-
return [
|
|
225
|
-
ctx.ui.theme.fg("dim", "Codex"),
|
|
226
|
-
accountText,
|
|
227
|
-
ctx.ui.theme.fg("dim", "loading..."),
|
|
228
|
-
]
|
|
252
|
+
return [formatBrand(ctx), accountText, formatLoading(ctx)]
|
|
229
253
|
.filter(Boolean)
|
|
230
254
|
.join(" ");
|
|
231
255
|
}
|
|
@@ -247,16 +271,18 @@ export function formatActiveAccountStatus(
|
|
|
247
271
|
preferences,
|
|
248
272
|
);
|
|
249
273
|
|
|
274
|
+
const usageSegments = [fiveHour, sevenDay].filter(Boolean);
|
|
275
|
+
const usageText = usageSegments.join(` ${formatSeparator(ctx)} `);
|
|
250
276
|
const leading =
|
|
251
277
|
preferences.order === "account-first"
|
|
252
|
-
? [ctx
|
|
253
|
-
: [ctx
|
|
278
|
+
? [formatBrand(ctx), accountText, usageText]
|
|
279
|
+
: [formatBrand(ctx), usageText];
|
|
254
280
|
const trailing =
|
|
255
281
|
preferences.order === "account-first" ? [] : [accountText].filter(Boolean);
|
|
256
282
|
|
|
257
|
-
return [...leading,
|
|
283
|
+
return [...leading, ...trailing]
|
|
258
284
|
.filter(Boolean)
|
|
259
|
-
.join(
|
|
285
|
+
.join(` ${formatSeparator(ctx)} `);
|
|
260
286
|
}
|
|
261
287
|
|
|
262
288
|
function getBooleanLabel(value: boolean): string {
|
|
@@ -498,7 +524,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
498
524
|
draft: FooterPreferences,
|
|
499
525
|
): string {
|
|
500
526
|
const previewText =
|
|
501
|
-
getStatusText(ctx, draft) ?? ctx
|
|
527
|
+
getStatusText(ctx, draft) ?? `${formatBrand(ctx)} ${formatLoading(ctx)}`;
|
|
502
528
|
return `${theme.fg("dim", "Preview")}: ${previewText}`;
|
|
503
529
|
}
|
|
504
530
|
|