@llblab/pi-codex-usage 0.8.0 → 0.8.1
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/BACKLOG.md +5 -1
- package/CHANGELOG.md +7 -0
- package/index.ts +122 -74
- package/package.json +1 -1
package/BACKLOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
# Backlog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Pending Release Verification
|
|
4
|
+
|
|
5
|
+
- [x] Fix stale extension context usage after session replacement or reload. Source: GitHub issue #1. Timers must not keep using captured stale `ctx` after Pi session lifecycle changes.
|
|
6
|
+
- [x] Make usage refresh cache/in-flight state model-aware for Codex vs Spark. A regular Codex cache or request must not temporarily render Spark as `n/a` after model switching.
|
|
7
|
+
- [x] Preserve the active `codex`/`spark` label for failure states. Spark failures should render `spark n/a` or `spark error`, not `codex ...`.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.1: Stale Context Hotfix
|
|
6
|
+
|
|
7
|
+
- Fixed stale extension context crashes from delayed statusline timers after Pi session replacement or reload. Impact: timers now ignore stale `ctx` failures instead of letting issue #1 crash Pi after idle/session lifecycle changes.
|
|
8
|
+
- Fixed Codex/Spark refresh state to key in-flight usage lookups by active quota bucket and only reuse cached reports that contain the active bucket. Impact: switching between regular Codex and Spark no longer temporarily renders the active bucket as unavailable because of a stale request/cache.
|
|
9
|
+
- Fixed Spark failure labels. Impact: Spark unavailable/error states now render with the `spark` label instead of falling back to `codex`.
|
|
10
|
+
- Added regression coverage for active-bucket cache reuse and stale extension context error detection.
|
|
11
|
+
|
|
5
12
|
## 0.8.0: Spark Model Usage Status
|
|
6
13
|
|
|
7
14
|
- Added active-bucket status support for `GPT-5.3-Codex-Spark`. Impact: selecting the Spark Codex subscription model now shows the parallel Spark rate-limit bucket with a `spark` label, while regular Codex models keep the existing `codex` bucket/status behavior.
|
package/index.ts
CHANGED
|
@@ -163,7 +163,9 @@ type PendingRpc = {
|
|
|
163
163
|
export default function codexUsage(pi: ExtensionAPI) {
|
|
164
164
|
let cache: CachedReport | undefined;
|
|
165
165
|
let failedRefreshes = 0;
|
|
166
|
-
let inFlightUsageQuery:
|
|
166
|
+
let inFlightUsageQuery:
|
|
167
|
+
| { limitId: string; promise: Promise<QueryUsageResult> }
|
|
168
|
+
| undefined;
|
|
167
169
|
let statuslineBlinkTimer: TimeoutHandle | undefined;
|
|
168
170
|
let statuslineClearTimer: TimeoutHandle | undefined;
|
|
169
171
|
let statuslineCountdownTimer: TimeoutHandle | undefined;
|
|
@@ -210,8 +212,12 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
210
212
|
const scheduleTemporaryStatuslineClear = (ctx: ExtensionContext) => {
|
|
211
213
|
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
212
214
|
statuslineClearTimer = setTimeout(() => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
try {
|
|
216
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
217
|
+
statuslineClearTimer = undefined;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
handleTimerError(error);
|
|
220
|
+
}
|
|
215
221
|
}, REFRESH_INTERVAL_MS) as TimeoutHandle;
|
|
216
222
|
statuslineClearTimer.unref?.();
|
|
217
223
|
};
|
|
@@ -219,7 +225,7 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
219
225
|
const scheduleStatuslineRefresh = (ctx: ExtensionContext) => {
|
|
220
226
|
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
221
227
|
statuslineRefreshTimer = setTimeout(() => {
|
|
222
|
-
void refreshCurrentCodexUsageStatusline(ctx, true);
|
|
228
|
+
void refreshCurrentCodexUsageStatusline(ctx, true).catch(handleAsyncTimerError);
|
|
223
229
|
}, REFRESH_INTERVAL_MS) as TimeoutHandle;
|
|
224
230
|
statuslineRefreshTimer.unref?.();
|
|
225
231
|
};
|
|
@@ -236,12 +242,16 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
236
242
|
return;
|
|
237
243
|
}
|
|
238
244
|
statuslineCountdownTimer = setTimeout(() => {
|
|
239
|
-
|
|
240
|
-
ctx.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
try {
|
|
246
|
+
if (isOpenAICodexModel(ctx.model)) {
|
|
247
|
+
ctx.ui.setStatus(
|
|
248
|
+
STATUS_KEY,
|
|
249
|
+
formatCodexUsageStatusline(report, ctx, model),
|
|
250
|
+
);
|
|
251
|
+
scheduleStatuslineCountdown(ctx, report, model);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
handleTimerError(error);
|
|
245
255
|
}
|
|
246
256
|
}, delayMs) as TimeoutHandle;
|
|
247
257
|
statuslineCountdownTimer.unref?.();
|
|
@@ -266,9 +276,13 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
266
276
|
if (options.blink) {
|
|
267
277
|
ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
|
|
268
278
|
statuslineBlinkTimer = setTimeout(() => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
279
|
+
try {
|
|
280
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
281
|
+
scheduleStatuslineCountdown(ctx, report, options.model);
|
|
282
|
+
statuslineBlinkTimer = undefined;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
handleTimerError(error);
|
|
285
|
+
}
|
|
272
286
|
}, REDRAW_BLINK_MS) as TimeoutHandle;
|
|
273
287
|
statuslineBlinkTimer.unref?.();
|
|
274
288
|
} else {
|
|
@@ -283,74 +297,87 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
283
297
|
ctx: ExtensionContext,
|
|
284
298
|
model: CodexUsageModel | undefined,
|
|
285
299
|
) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
300
|
+
const limitId = activeUsageLimitId(model);
|
|
301
|
+
if (inFlightUsageQuery?.limitId === limitId) return inFlightUsageQuery.promise;
|
|
302
|
+
const promise = queryUsage(
|
|
303
|
+
ctx,
|
|
304
|
+
{ timeoutMs: DEFAULT_TIMEOUT_MS },
|
|
305
|
+
model,
|
|
306
|
+
).finally(() => {
|
|
307
|
+
if (inFlightUsageQuery?.promise === promise) inFlightUsageQuery = undefined;
|
|
308
|
+
});
|
|
309
|
+
inFlightUsageQuery = { limitId, promise };
|
|
310
|
+
return promise;
|
|
296
311
|
};
|
|
297
312
|
|
|
298
313
|
const refreshCurrentCodexUsageStatusline = async (
|
|
299
314
|
ctx: ExtensionContext,
|
|
300
315
|
force: boolean,
|
|
301
|
-
model
|
|
316
|
+
model?: CodexUsageModel,
|
|
302
317
|
) => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
318
|
+
try {
|
|
319
|
+
const activeModel = model ?? ctx.model;
|
|
320
|
+
if (!isOpenAICodexModel(activeModel)) {
|
|
321
|
+
clearUsageStatusline(ctx);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
307
324
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
if (!cache) ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
|
|
326
|
+
const requestId = statuslineRequestId + 1;
|
|
327
|
+
statuslineRequestId = requestId;
|
|
328
|
+
const cached =
|
|
329
|
+
cache &&
|
|
330
|
+
Date.now() - cache.createdAt < REFRESH_INTERVAL_MS &&
|
|
331
|
+
canReuseCachedReport(cache.report, activeModel)
|
|
332
|
+
? cache
|
|
333
|
+
: undefined;
|
|
334
|
+
if (cached && !force) {
|
|
335
|
+
setUsageStatusline(ctx, cached.report, {
|
|
336
|
+
autoRefresh: true,
|
|
337
|
+
blink: false,
|
|
338
|
+
model: activeModel,
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
323
342
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
343
|
+
const result = await queryCurrentUsage(ctx, activeModel);
|
|
344
|
+
if (requestId !== statuslineRequestId) return;
|
|
345
|
+
if (!isOpenAICodexModel(ctx.model)) {
|
|
346
|
+
clearUsageStatusline(ctx);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
330
349
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
350
|
+
if (!result.ok) {
|
|
351
|
+
failedRefreshes += 1;
|
|
352
|
+
if (!cache || failedRefreshes >= 5) {
|
|
353
|
+
ctx.ui.setStatus(
|
|
354
|
+
STATUS_KEY,
|
|
355
|
+
formatStatuslineProblem(ctx, result.errors, activeModel),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
scheduleStatuslineRefresh(ctx);
|
|
359
|
+
return;
|
|
338
360
|
}
|
|
339
|
-
scheduleStatuslineRefresh(ctx);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
361
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
362
|
+
const previousReport = cache?.report;
|
|
363
|
+
const blink = previousReport
|
|
364
|
+
? formatReportBar(previousReport, activeModel) !==
|
|
365
|
+
formatReportBar(result.report, activeModel)
|
|
366
|
+
: false;
|
|
367
|
+
failedRefreshes = 0;
|
|
368
|
+
cache = { createdAt: Date.now(), report: result.report };
|
|
369
|
+
setUsageStatusline(ctx, result.report, {
|
|
370
|
+
autoRefresh: true,
|
|
371
|
+
blink,
|
|
372
|
+
model: activeModel,
|
|
373
|
+
});
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (isStaleExtensionContextError(error)) {
|
|
376
|
+
clearStatuslineTimers();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
354
381
|
};
|
|
355
382
|
|
|
356
383
|
ensureTelegramStatusLineRegistered();
|
|
@@ -358,19 +385,19 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
358
385
|
pi.on("session_start", (_event, ctx) => {
|
|
359
386
|
ensureTelegramStatusLineRegistered();
|
|
360
387
|
if (isOpenAICodexModel(ctx.model))
|
|
361
|
-
void refreshCurrentCodexUsageStatusline(ctx, false);
|
|
388
|
+
void refreshCurrentCodexUsageStatusline(ctx, false).catch(handleAsyncTimerError);
|
|
362
389
|
else clearUsageStatusline(ctx);
|
|
363
390
|
});
|
|
364
391
|
|
|
365
392
|
pi.on("session_tree", (_event, ctx) => {
|
|
366
393
|
if (isOpenAICodexModel(ctx.model))
|
|
367
|
-
void refreshCurrentCodexUsageStatusline(ctx, false);
|
|
394
|
+
void refreshCurrentCodexUsageStatusline(ctx, false).catch(handleAsyncTimerError);
|
|
368
395
|
else clearUsageStatusline(ctx);
|
|
369
396
|
});
|
|
370
397
|
|
|
371
398
|
pi.on("model_select", (event, ctx) => {
|
|
372
399
|
if (isOpenAICodexModel(event.model)) {
|
|
373
|
-
void refreshCurrentCodexUsageStatusline(ctx, false, event.model);
|
|
400
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, event.model).catch(handleAsyncTimerError);
|
|
374
401
|
} else {
|
|
375
402
|
clearUsageStatusline(ctx);
|
|
376
403
|
}
|
|
@@ -383,6 +410,19 @@ export default function codexUsage(pi: ExtensionAPI) {
|
|
|
383
410
|
});
|
|
384
411
|
}
|
|
385
412
|
|
|
413
|
+
function handleAsyncTimerError(error: unknown): void {
|
|
414
|
+
handleTimerError(error);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function handleTimerError(error: unknown): void {
|
|
418
|
+
if (isStaleExtensionContextError(error)) return;
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
423
|
+
return error instanceof Error && error.message.includes("ctx is stale");
|
|
424
|
+
}
|
|
425
|
+
|
|
386
426
|
function isOpenAICodexModel(
|
|
387
427
|
model: Pick<PiModel, "provider"> | undefined,
|
|
388
428
|
): boolean {
|
|
@@ -1084,8 +1124,9 @@ function formatEmptyStatuslineBar(ctx: ExtensionContext): string {
|
|
|
1084
1124
|
function formatStatuslineProblem(
|
|
1085
1125
|
ctx: ExtensionContext,
|
|
1086
1126
|
errors: UsageQueryError[],
|
|
1127
|
+
model?: CodexUsageModel,
|
|
1087
1128
|
): string {
|
|
1088
|
-
const label = ctx.ui.theme.fg("accent",
|
|
1129
|
+
const label = ctx.ui.theme.fg("accent", activeUsageLabel(model));
|
|
1089
1130
|
const value = isUsageUnavailable(errors)
|
|
1090
1131
|
? ctx.ui.theme.fg("muted", "n/a")
|
|
1091
1132
|
: ctx.ui.theme.fg("error", "error");
|
|
@@ -1179,6 +1220,13 @@ function hasExhaustedQuotaWindow(
|
|
|
1179
1220
|
);
|
|
1180
1221
|
}
|
|
1181
1222
|
|
|
1223
|
+
export function canReuseCachedReport(
|
|
1224
|
+
report: CodexUsageReport,
|
|
1225
|
+
model: CodexUsageModel | undefined,
|
|
1226
|
+
): boolean {
|
|
1227
|
+
return selectActiveUsageSnapshot(report, model) !== undefined;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1182
1230
|
function selectActiveUsageSnapshot(
|
|
1183
1231
|
report: CodexUsageReport,
|
|
1184
1232
|
model: CodexUsageModel | undefined,
|