@narumitw/pi-usage 0.61.2 → 0.62.0

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/src/usage.ts CHANGED
@@ -22,7 +22,12 @@ import { abortError, awaitWithDeadline, errorMessage, runWithConcurrency, UsageC
22
22
  import { formatProviderStates, formatUsageStatusline } from "./format.js";
23
23
  import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
24
24
  import { UnsupportedOpenAIUsageAuthError } from "./providers/openai-chatgpt.js";
25
- import { adapterForProvider, isStaleExtensionContextError, queryProviderUsage, resolveUsageAuth } from "./query.js";
25
+ import {
26
+ adapterForProvider,
27
+ isStaleExtensionContextError,
28
+ queryProviderUsage,
29
+ resolveUsageAuth as resolveRuntimeUsageAuth,
30
+ } from "./query.js";
26
31
  import { createUsageSettingsRuntime, type UsageSettingsRuntime, type UsageSettingsState } from "./settings.js";
27
32
  import type {
28
33
  PiModel,
@@ -84,10 +89,20 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
84
89
  const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
85
90
  const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
86
91
  const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
92
+ const resolveUsageAuth: typeof resolveRuntimeUsageAuth = (ctx, adapter, salt, reader, candidates) =>
93
+ resolveRuntimeUsageAuth(
94
+ ctx,
95
+ adapter,
96
+ salt,
97
+ reader,
98
+ candidates,
99
+ settingsRuntime.get().settings.openaiCompanionUsage,
100
+ );
87
101
  const cache = new UsageCache(CACHE_TTL_MS);
88
102
  const failureBackoff = new Map<string, { until: number; message: string }>();
89
103
  const latestQueries = new Map<string, number>();
90
104
  const activeControllers = new Set<AbortController>();
105
+ const nativeQueryControllers = new Set<AbortController>();
91
106
  let querySequence = 0;
92
107
  let activeCurrentIdentity: string | undefined;
93
108
  let sessionActive = false;
@@ -183,7 +198,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
183
198
  if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
184
199
  if (
185
200
  sessionActive &&
186
- showCodexResetCountdown &&
201
+ (showCodexResetCountdown || outcome.state.report.source === "openai-chatgpt-companion") &&
187
202
  outcome.state.report.buckets.some(
188
203
  (bucket) => bucket.resetsAt !== undefined && Number.isFinite(bucket.resetsAt) && bucket.resetsAt * 1_000 > now,
189
204
  )
@@ -220,7 +235,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
220
235
  activeCurrentIdentity = nextIdentity;
221
236
  };
222
237
 
223
- const queryAdapterState = async (
238
+ const queryAdapterStateInternal = async (
224
239
  ctx: ExtensionContext,
225
240
  adapter: UsageProviderAdapter,
226
241
  displayState: UsageDisplayState,
@@ -233,6 +248,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
233
248
  const expectedSessionId = ctx.sessionManager.getSessionId();
234
249
  const expectedModelIdentity = modelIdentity(ctx.model);
235
250
  const expectedTargetId = adapter.targets ? settingsRuntime.get().settings.selectedTargets[adapter.id] : undefined;
251
+ const expectedCompanionUsage = settingsRuntime.get().settings.openaiCompanionUsage;
236
252
  const providerName = providerDisplayName(ctx, adapter.id);
237
253
  let auth: ResolvedUsageAuth | undefined;
238
254
  try {
@@ -244,6 +260,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
244
260
  );
245
261
  } catch (error) {
246
262
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
263
+ if (adapter.id === "openai") invalidateProviderState(adapter.id);
247
264
  const authState =
248
265
  error instanceof UnsupportedOpenAIUsageAuthError
249
266
  ? "unsupported"
@@ -284,10 +301,12 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
284
301
  expectedSessionGeneration !== sessionGeneration ||
285
302
  ctx.sessionManager.getSessionId() !== expectedSessionId ||
286
303
  modelIdentity(ctx.model) !== expectedModelIdentity ||
304
+ (adapter.id === "openai" && settingsRuntime.get().settings.openaiCompanionUsage !== expectedCompanionUsage) ||
287
305
  (adapter.targets !== undefined &&
288
306
  settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
289
307
  if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
290
308
  if (!auth) {
309
+ if (adapter.id === "openai") invalidateProviderState(adapter.id);
291
310
  if (displayState === "current") {
292
311
  transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
293
312
  }
@@ -302,6 +321,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
302
321
  authState: "unavailable",
303
322
  };
304
323
  }
324
+ if (adapter.id === "openai" && !auth.openaiCompanion) invalidateProviderState(adapter.id);
305
325
  let retryableAuthChanged = false;
306
326
  const guard = async () => {
307
327
  if (signal.aborted || requestContextChanged()) throw abortError();
@@ -466,6 +486,25 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
466
486
  }
467
487
  };
468
488
 
489
+ const queryAdapterState = async (...args: Parameters<typeof queryAdapterStateInternal>): Promise<QueryOutcome> => {
490
+ const [ctx, adapter, displayState, force, signal, ...rest] = args;
491
+ if (adapter.id !== "openai") return queryAdapterStateInternal(...args);
492
+ const controller = new AbortController();
493
+ nativeQueryControllers.add(controller);
494
+ try {
495
+ return await queryAdapterStateInternal(
496
+ ctx,
497
+ adapter,
498
+ displayState,
499
+ force,
500
+ AbortSignal.any([signal, controller.signal]),
501
+ ...rest,
502
+ );
503
+ } finally {
504
+ nativeQueryControllers.delete(controller);
505
+ }
506
+ };
507
+
469
508
  const loadTargetChoices = async (ctx: ExtensionContext, adapter: UsageProviderAdapter, signal: AbortSignal) => {
470
509
  if (!adapter.targets) throw new Error("Provider does not support usage targets.");
471
510
  const expectedSessionGeneration = sessionGeneration;
@@ -686,7 +725,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
686
725
  const generation = statusGeneration;
687
726
  const outcome = await queryAdapterState(ctx, adapter, displayState, force, signal);
688
727
  if (signal.aborted) throw abortError();
689
- if (!outcome.authState || (await outcomeStillCurrent(ctx, model, generation, outcome, signal))) return outcome;
728
+ if (await outcomeStillCurrent(ctx, model, generation, outcome, signal)) return outcome;
690
729
  force = false;
691
730
  }
692
731
  throw new Error("OpenAI runtime authentication kept changing; reopen /usage to retry.");
@@ -1051,6 +1090,20 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1051
1090
  controller.signal,
1052
1091
  () => statusGeneration === menuGeneration && !controller.signal.aborted,
1053
1092
  (id) => {
1093
+ if (
1094
+ id === "openaiCompanionUsage" &&
1095
+ statusGeneration === menuGeneration &&
1096
+ !controller.signal.aborted
1097
+ ) {
1098
+ for (const active of nativeQueryControllers) active.abort();
1099
+ invalidateProviderState("openai");
1100
+ if (ctx.model?.provider === "openai") {
1101
+ statusController?.abort();
1102
+ statusController = undefined;
1103
+ clearStatusTimers();
1104
+ safeSetStatus(ctx, undefined);
1105
+ }
1106
+ }
1054
1107
  if (
1055
1108
  (id === "codexStatusResetCountdown" || id === "codexStatusPercentage") &&
1056
1109
  stableCurrent &&
@@ -1253,6 +1306,17 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1253
1306
  }
1254
1307
  const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
1255
1308
  if (!revalidated) return { kind: "back" };
1309
+ // Current-provider revalidation can await unrelated work after the configured
1310
+ // native report was checked. Refresh its identity at the publication boundary.
1311
+ if (adapter.id === "openai") {
1312
+ outcome = await runMenuOperation(
1313
+ ctx,
1314
+ "Revalidating ChatGPT companion usage…",
1315
+ controller.signal,
1316
+ (signal) => queryStableAdapterState(ctx, adapter, "configured", false, signal),
1317
+ );
1318
+ if (!outcome) return { kind: "back" };
1319
+ }
1256
1320
  stableCurrent = revalidated;
1257
1321
  current = revalidated.outcome;
1258
1322
  visibleStates = [
@@ -1300,6 +1364,22 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1300
1364
  });
1301
1365
  const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
1302
1366
  if (!revalidated) return { kind: "stay" };
1367
+ const nativeIndex = adapters.findIndex(
1368
+ (adapter) => adapter.id === "openai" && adapter.id !== currentProviderId,
1369
+ );
1370
+ if (nativeIndex >= 0) {
1371
+ const nativeAdapter = adapters[nativeIndex];
1372
+ if (nativeAdapter) {
1373
+ const native = await runMenuOperation(
1374
+ ctx,
1375
+ "Revalidating ChatGPT companion usage…",
1376
+ controller.signal,
1377
+ (signal) => queryStableAdapterState(ctx, nativeAdapter, "configured", false, signal),
1378
+ );
1379
+ if (!native) return { kind: "stay" };
1380
+ queriedStates[nativeIndex] = native.state;
1381
+ }
1382
+ }
1303
1383
  stableCurrent = revalidated;
1304
1384
  current = revalidated.outcome;
1305
1385
  visibleStates = [
@@ -1356,6 +1436,8 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1356
1436
  startStatusRefresh(ctx, ctx.model, false);
1357
1437
  });
1358
1438
  pi.on("model_select", (event, ctx) => {
1439
+ // Cancel read-only native queries, not confirmed legacy reset mutations.
1440
+ for (const controller of nativeQueryControllers) controller.abort();
1359
1441
  startStatusRefresh(ctx, event.model, false);
1360
1442
  });
1361
1443
  pi.on("turn_start", (_event, ctx) => {