@narumitw/pi-usage 0.52.3 → 0.54.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/README.md +111 -10
- package/dist/index.ts +1038 -43
- package/dist/index.ts.map +4 -4
- package/package.json +12 -4
- package/src/format.ts +117 -1
- package/src/index.ts +8 -0
- package/src/providers/kimi-coding.ts +276 -0
- package/src/providers/xai.ts +186 -0
- package/src/providers/zai.ts +150 -0
- package/src/query.ts +251 -3
- package/src/settings.ts +7 -0
- package/src/types.ts +28 -2
- package/src/usage-helpers.ts +3 -3
- package/src/usage-settings-ui.ts +128 -0
- package/src/usage.ts +127 -12
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionCommandContext,
|
|
3
|
+
getSettingsListTheme,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
Container,
|
|
7
|
+
Key,
|
|
8
|
+
matchesKey,
|
|
9
|
+
type SettingItem,
|
|
10
|
+
SettingsList,
|
|
11
|
+
Text,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
13
|
+
import { errorMessage } from "./core.js";
|
|
14
|
+
import type { UsageSettings, UsageSettingsRuntime } from "./settings.js";
|
|
15
|
+
|
|
16
|
+
const OFF = "Off";
|
|
17
|
+
const ON = "On";
|
|
18
|
+
|
|
19
|
+
type UsageSettingId = keyof UsageSettings;
|
|
20
|
+
|
|
21
|
+
export async function showUsageSettings(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
settingsRuntime: UsageSettingsRuntime,
|
|
24
|
+
parentSignal: AbortSignal,
|
|
25
|
+
isCurrent: () => boolean,
|
|
26
|
+
onApplied: (id: UsageSettingId, previous: boolean, next: boolean) => void,
|
|
27
|
+
): Promise<boolean> {
|
|
28
|
+
if (ctx.mode !== "tui") {
|
|
29
|
+
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (parentSignal.aborted || !isCurrent()) return false;
|
|
33
|
+
|
|
34
|
+
return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
|
|
35
|
+
const localController = new AbortController();
|
|
36
|
+
const signal = AbortSignal.any([parentSignal, localController.signal]);
|
|
37
|
+
let changed = false;
|
|
38
|
+
let closing = false;
|
|
39
|
+
let saveQueue = Promise.resolve();
|
|
40
|
+
const state = settingsRuntime.get();
|
|
41
|
+
const items: SettingItem[] = [
|
|
42
|
+
{
|
|
43
|
+
id: "codexFastMode",
|
|
44
|
+
label: "Codex Fast mode",
|
|
45
|
+
description: "Use faster Codex routing at increased plan allowance consumption.",
|
|
46
|
+
currentValue: state.settings.codexFastMode ? ON : OFF,
|
|
47
|
+
values: [OFF, ON],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "xaiUsage",
|
|
51
|
+
label: "xAI usage",
|
|
52
|
+
description: "Report OAuth subscription allowance and credits.",
|
|
53
|
+
currentValue: state.kind !== "invalid" && state.settings.xaiUsage ? ON : OFF,
|
|
54
|
+
values: [OFF, ON],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const container = new Container();
|
|
58
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
|
|
59
|
+
|
|
60
|
+
let settingsList: SettingsList;
|
|
61
|
+
const cancel = () => {
|
|
62
|
+
if (closing) return;
|
|
63
|
+
closing = true;
|
|
64
|
+
localController.abort();
|
|
65
|
+
done(changed);
|
|
66
|
+
};
|
|
67
|
+
settingsList = new SettingsList(
|
|
68
|
+
items,
|
|
69
|
+
items.length + 2,
|
|
70
|
+
getSettingsListTheme(),
|
|
71
|
+
(id, value) => {
|
|
72
|
+
if (closing || signal.aborted || !isCurrent()) return;
|
|
73
|
+
const settingId = id as UsageSettingId;
|
|
74
|
+
const requested = value !== OFF;
|
|
75
|
+
saveQueue = saveQueue.then(async () => {
|
|
76
|
+
const previous = settingsRuntime.get().settings[settingId];
|
|
77
|
+
if (settingsRuntime.get().kind === "invalid") {
|
|
78
|
+
const effectivePrevious = settingId === "xaiUsage" ? false : previous;
|
|
79
|
+
settingsList.updateValue(id, displayValue(settingId, effectivePrevious));
|
|
80
|
+
if (!signal.aborted && isCurrent()) {
|
|
81
|
+
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
82
|
+
tui.requestRender();
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await settingsRuntime.update({ [settingId]: requested }, signal);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (signal.aborted || !isCurrent()) return;
|
|
90
|
+
settingsList.updateValue(id, displayValue(settingId, previous));
|
|
91
|
+
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
92
|
+
tui.requestRender();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (previous !== requested) {
|
|
96
|
+
changed = true;
|
|
97
|
+
onApplied(settingId, previous, requested);
|
|
98
|
+
}
|
|
99
|
+
if (signal.aborted || !isCurrent()) return;
|
|
100
|
+
settingsList.updateValue(id, displayValue(settingId, requested));
|
|
101
|
+
tui.requestRender();
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
cancel,
|
|
105
|
+
);
|
|
106
|
+
container.addChild(settingsList);
|
|
107
|
+
|
|
108
|
+
parentSignal.addEventListener("abort", cancel, { once: true });
|
|
109
|
+
return {
|
|
110
|
+
render: (width: number) => container.render(width),
|
|
111
|
+
invalidate: () => container.invalidate(),
|
|
112
|
+
handleInput(data: string) {
|
|
113
|
+
if (closing) return;
|
|
114
|
+
if (matchesKey(data, Key.ctrl("c"))) cancel();
|
|
115
|
+
else settingsList.handleInput(data);
|
|
116
|
+
tui.requestRender();
|
|
117
|
+
},
|
|
118
|
+
dispose() {
|
|
119
|
+
localController.abort();
|
|
120
|
+
parentSignal.removeEventListener("abort", cancel);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function displayValue(_id: UsageSettingId, enabled: boolean): string {
|
|
127
|
+
return enabled ? ON : OFF;
|
|
128
|
+
}
|
package/src/usage.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// This orchestrator intentionally remains over 1,000 lines because its menu, query generations,
|
|
2
|
+
// account cache, cancellation, and session lifecycle share one consistency boundary.
|
|
1
3
|
import { randomUUID } from "node:crypto";
|
|
2
4
|
import type {
|
|
3
5
|
ExtensionAPI,
|
|
@@ -20,7 +22,13 @@ import {
|
|
|
20
22
|
resetOptionExpiration,
|
|
21
23
|
resolveCodexResetAuth,
|
|
22
24
|
} from "./codex-resets.js";
|
|
23
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
abortError,
|
|
27
|
+
awaitWithDeadline,
|
|
28
|
+
errorMessage,
|
|
29
|
+
runWithConcurrency,
|
|
30
|
+
UsageCache,
|
|
31
|
+
} from "./core.js";
|
|
24
32
|
import { formatProviderStates, formatUsageStatusline } from "./format.js";
|
|
25
33
|
import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
26
34
|
import {
|
|
@@ -45,6 +53,7 @@ import {
|
|
|
45
53
|
providerDisplayName,
|
|
46
54
|
setBoundedMap,
|
|
47
55
|
} from "./usage-helpers.js";
|
|
56
|
+
import { showUsageSettings } from "./usage-settings-ui.js";
|
|
48
57
|
|
|
49
58
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
50
59
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
@@ -57,6 +66,7 @@ const REFRESH_CURRENT = "Refresh current usage";
|
|
|
57
66
|
const VIEW_ANOTHER = "View another configured provider…";
|
|
58
67
|
const VIEW_ALL = "View all configured providers…";
|
|
59
68
|
const CLOSE = "Close";
|
|
69
|
+
const SETTINGS = "Settings";
|
|
60
70
|
const REDEEM_CODEX_RESET = "Redeem usage limit reset…";
|
|
61
71
|
|
|
62
72
|
type UsageExtensionDependencies = {
|
|
@@ -92,10 +102,19 @@ export default function usageExtension(
|
|
|
92
102
|
let activeCurrentIdentity: string | undefined;
|
|
93
103
|
let sessionActive = false;
|
|
94
104
|
let statusGeneration = 0;
|
|
105
|
+
let sessionGeneration = 0;
|
|
106
|
+
let xaiSettingsGeneration = 0;
|
|
95
107
|
let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
96
108
|
let statusController: AbortController | undefined;
|
|
97
109
|
let fastRuntime: ReturnType<typeof registerCodexFastMode>;
|
|
98
110
|
|
|
111
|
+
const xaiUsageEnabled = () => {
|
|
112
|
+
const state = settingsRuntime.get();
|
|
113
|
+
return state.kind !== "invalid" && state.settings.xaiUsage;
|
|
114
|
+
};
|
|
115
|
+
const activeAdapterForProvider = (providerId: string | undefined) =>
|
|
116
|
+
adapterForProvider(providerId, xaiUsageEnabled());
|
|
117
|
+
|
|
99
118
|
const clearStatusTimer = () => {
|
|
100
119
|
if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
|
|
101
120
|
statusRefreshTimer = undefined;
|
|
@@ -136,6 +155,11 @@ export default function usageExtension(
|
|
|
136
155
|
model: PiModel,
|
|
137
156
|
shouldSchedule: boolean,
|
|
138
157
|
) => {
|
|
158
|
+
if (activeAdapterForProvider(model.provider)?.publishesStatusline === false) {
|
|
159
|
+
clearStatusTimer();
|
|
160
|
+
safeSetStatus(ctx, undefined);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
139
163
|
if (outcome.state.status === "unsupported") {
|
|
140
164
|
clearStatusTimer();
|
|
141
165
|
safeSetStatus(ctx, undefined);
|
|
@@ -188,6 +212,10 @@ export default function usageExtension(
|
|
|
188
212
|
signal: AbortSignal,
|
|
189
213
|
): Promise<QueryOutcome> => {
|
|
190
214
|
const startedAt = Date.now();
|
|
215
|
+
const expectedSessionGeneration = sessionGeneration;
|
|
216
|
+
const expectedXaiSettingsGeneration = xaiSettingsGeneration;
|
|
217
|
+
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
218
|
+
const expectedModelIdentity = modelIdentity(ctx.model);
|
|
191
219
|
let auth: ResolvedUsageAuth | undefined;
|
|
192
220
|
try {
|
|
193
221
|
auth = await awaitWithDeadline(
|
|
@@ -211,6 +239,16 @@ export default function usageExtension(
|
|
|
211
239
|
},
|
|
212
240
|
};
|
|
213
241
|
}
|
|
242
|
+
if (
|
|
243
|
+
adapter.id === "xai" &&
|
|
244
|
+
(expectedSessionGeneration !== sessionGeneration ||
|
|
245
|
+
expectedXaiSettingsGeneration !== xaiSettingsGeneration ||
|
|
246
|
+
!xaiUsageEnabled() ||
|
|
247
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
248
|
+
modelIdentity(ctx.model) !== expectedModelIdentity)
|
|
249
|
+
) {
|
|
250
|
+
throw abortError();
|
|
251
|
+
}
|
|
214
252
|
if (!auth) {
|
|
215
253
|
if (displayState === "current") {
|
|
216
254
|
transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
|
|
@@ -265,7 +303,40 @@ export default function usageExtension(
|
|
|
265
303
|
|
|
266
304
|
try {
|
|
267
305
|
const remainingMs = Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt));
|
|
268
|
-
const
|
|
306
|
+
const guard =
|
|
307
|
+
adapter.id === "xai"
|
|
308
|
+
? async () => {
|
|
309
|
+
if (
|
|
310
|
+
signal.aborted ||
|
|
311
|
+
expectedSessionGeneration !== sessionGeneration ||
|
|
312
|
+
expectedXaiSettingsGeneration !== xaiSettingsGeneration ||
|
|
313
|
+
!xaiUsageEnabled() ||
|
|
314
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
315
|
+
modelIdentity(ctx.model) !== expectedModelIdentity
|
|
316
|
+
) {
|
|
317
|
+
throw abortError();
|
|
318
|
+
}
|
|
319
|
+
const revalidated = await awaitWithDeadline(
|
|
320
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
321
|
+
signal,
|
|
322
|
+
Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt)),
|
|
323
|
+
"revalidating xAI runtime auth",
|
|
324
|
+
);
|
|
325
|
+
if (
|
|
326
|
+
signal.aborted ||
|
|
327
|
+
expectedSessionGeneration !== sessionGeneration ||
|
|
328
|
+
expectedXaiSettingsGeneration !== xaiSettingsGeneration ||
|
|
329
|
+
!xaiUsageEnabled() ||
|
|
330
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
331
|
+
modelIdentity(ctx.model) !== expectedModelIdentity ||
|
|
332
|
+
revalidated?.fingerprint !== auth.fingerprint
|
|
333
|
+
) {
|
|
334
|
+
throw abortError();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
: undefined;
|
|
338
|
+
const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
|
|
339
|
+
if (guard) await guard();
|
|
269
340
|
if (latestQueries.get(failureKey) === queryId) {
|
|
270
341
|
cache.set(adapter.id, auth.fingerprint, report);
|
|
271
342
|
failureBackoff.delete(failureKey);
|
|
@@ -314,7 +385,7 @@ export default function usageExtension(
|
|
|
314
385
|
force: boolean,
|
|
315
386
|
signal: AbortSignal,
|
|
316
387
|
): Promise<QueryOutcome> => {
|
|
317
|
-
const adapter =
|
|
388
|
+
const adapter = activeAdapterForProvider(model?.provider);
|
|
318
389
|
if (!adapter) {
|
|
319
390
|
const providerId = model?.provider ?? "none";
|
|
320
391
|
transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
|
|
@@ -324,9 +395,12 @@ export default function usageExtension(
|
|
|
324
395
|
providerName: providerDisplayName(ctx, providerId),
|
|
325
396
|
displayState: "current",
|
|
326
397
|
status: "unsupported",
|
|
327
|
-
message:
|
|
328
|
-
|
|
329
|
-
|
|
398
|
+
message:
|
|
399
|
+
providerId === "xai" && !xaiUsageEnabled()
|
|
400
|
+
? "xAI usage is disabled. Open Settings to enable it."
|
|
401
|
+
: model
|
|
402
|
+
? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
|
|
403
|
+
: "No model is selected.",
|
|
330
404
|
},
|
|
331
405
|
};
|
|
332
406
|
}
|
|
@@ -338,13 +412,17 @@ export default function usageExtension(
|
|
|
338
412
|
model: PiModel | undefined,
|
|
339
413
|
force: boolean,
|
|
340
414
|
) => {
|
|
341
|
-
const adapter =
|
|
415
|
+
const adapter = activeAdapterForProvider(model?.provider);
|
|
342
416
|
if (!adapter || !model) {
|
|
343
417
|
const providerId = model?.provider ?? "none";
|
|
344
418
|
transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
|
|
345
419
|
clearStatus(ctx);
|
|
346
420
|
return;
|
|
347
421
|
}
|
|
422
|
+
if (adapter.publishesStatusline === false) {
|
|
423
|
+
clearStatus(ctx);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
348
426
|
statusGeneration += 1;
|
|
349
427
|
const generation = statusGeneration;
|
|
350
428
|
statusController?.abort();
|
|
@@ -416,7 +494,7 @@ export default function usageExtension(
|
|
|
416
494
|
if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
|
|
417
495
|
return false;
|
|
418
496
|
}
|
|
419
|
-
const adapter =
|
|
497
|
+
const adapter = activeAdapterForProvider(model?.provider);
|
|
420
498
|
if (outcome.authState === "unavailable") {
|
|
421
499
|
if (!adapter) return false;
|
|
422
500
|
try {
|
|
@@ -524,6 +602,7 @@ export default function usageExtension(
|
|
|
524
602
|
| "reset-error";
|
|
525
603
|
type Action =
|
|
526
604
|
| "refresh"
|
|
605
|
+
| "settings"
|
|
527
606
|
| "toggle-fast"
|
|
528
607
|
| "another"
|
|
529
608
|
| "all"
|
|
@@ -551,6 +630,7 @@ export default function usageExtension(
|
|
|
551
630
|
lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
|
|
552
631
|
items: [
|
|
553
632
|
{ id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
|
|
633
|
+
{ id: "settings", label: SETTINGS, action: "settings" },
|
|
554
634
|
...(fastAvailability.kind === "available"
|
|
555
635
|
? [
|
|
556
636
|
{
|
|
@@ -588,7 +668,7 @@ export default function usageExtension(
|
|
|
588
668
|
providers: () => ({
|
|
589
669
|
kind: "actions",
|
|
590
670
|
title: "Select a configured provider",
|
|
591
|
-
items: configuredAdapters(ctx)
|
|
671
|
+
items: configuredAdapters(ctx, xaiUsageEnabled())
|
|
592
672
|
.filter((adapter) => adapter.id !== ctx.model?.provider)
|
|
593
673
|
.map((adapter) => ({
|
|
594
674
|
id: adapter.id,
|
|
@@ -656,6 +736,37 @@ export default function usageExtension(
|
|
|
656
736
|
}),
|
|
657
737
|
},
|
|
658
738
|
actions: {
|
|
739
|
+
settings: async () => {
|
|
740
|
+
await showUsageSettings(
|
|
741
|
+
ctx,
|
|
742
|
+
settingsRuntime,
|
|
743
|
+
controller.signal,
|
|
744
|
+
() => statusGeneration === menuGeneration && !controller.signal.aborted,
|
|
745
|
+
(id, _previous, next) => {
|
|
746
|
+
if (id !== "xaiUsage") return;
|
|
747
|
+
xaiSettingsGeneration += 1;
|
|
748
|
+
invalidateProviderState("xai");
|
|
749
|
+
if (!next) {
|
|
750
|
+
for (const active of activeControllers) {
|
|
751
|
+
if (active !== controller) active.abort();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
);
|
|
756
|
+
fastState = settingsRuntime.get();
|
|
757
|
+
const revalidated = await queryStableCurrent(
|
|
758
|
+
ctx,
|
|
759
|
+
false,
|
|
760
|
+
controller,
|
|
761
|
+
"Applying usage settings…",
|
|
762
|
+
);
|
|
763
|
+
if (!revalidated) return { kind: "stay" };
|
|
764
|
+
stableCurrent = revalidated;
|
|
765
|
+
current = revalidated.outcome;
|
|
766
|
+
visibleStates = [current.state];
|
|
767
|
+
publishStableCurrent(ctx, revalidated);
|
|
768
|
+
return { kind: "stay" };
|
|
769
|
+
},
|
|
659
770
|
"toggle-fast": async () => {
|
|
660
771
|
const availability = fastRuntime.availability(ctx.model);
|
|
661
772
|
if (availability.kind !== "available" || fastState.kind === "invalid") {
|
|
@@ -835,7 +946,7 @@ export default function usageExtension(
|
|
|
835
946
|
return { kind: "stay" };
|
|
836
947
|
},
|
|
837
948
|
another: async () => {
|
|
838
|
-
const others = configuredAdapters(ctx).filter(
|
|
949
|
+
const others = configuredAdapters(ctx, xaiUsageEnabled()).filter(
|
|
839
950
|
(adapter) => adapter.id !== ctx.model?.provider,
|
|
840
951
|
);
|
|
841
952
|
if (others.length === 0) {
|
|
@@ -845,7 +956,7 @@ export default function usageExtension(
|
|
|
845
956
|
return { kind: "to", screen: "providers" };
|
|
846
957
|
},
|
|
847
958
|
provider: async ({ itemId }) => {
|
|
848
|
-
const adapter = configuredAdapters(ctx).find(
|
|
959
|
+
const adapter = configuredAdapters(ctx, xaiUsageEnabled()).find(
|
|
849
960
|
(candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
|
|
850
961
|
);
|
|
851
962
|
if (!adapter) return { kind: "back" };
|
|
@@ -873,7 +984,7 @@ export default function usageExtension(
|
|
|
873
984
|
return { kind: "back" };
|
|
874
985
|
},
|
|
875
986
|
all: async () => {
|
|
876
|
-
const adapters = configuredAdapters(ctx);
|
|
987
|
+
const adapters = configuredAdapters(ctx, xaiUsageEnabled());
|
|
877
988
|
const currentProviderId = ctx.model?.provider;
|
|
878
989
|
const settled = await runMenuOperation(
|
|
879
990
|
ctx,
|
|
@@ -955,6 +1066,8 @@ export default function usageExtension(
|
|
|
955
1066
|
},
|
|
956
1067
|
});
|
|
957
1068
|
pi.on("session_start", (_event, ctx) => {
|
|
1069
|
+
sessionGeneration += 1;
|
|
1070
|
+
xaiSettingsGeneration += 1;
|
|
958
1071
|
statusGeneration += 1;
|
|
959
1072
|
clearStatusTimer();
|
|
960
1073
|
for (const controller of activeControllers) controller.abort();
|
|
@@ -974,6 +1087,8 @@ export default function usageExtension(
|
|
|
974
1087
|
});
|
|
975
1088
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
976
1089
|
sessionActive = false;
|
|
1090
|
+
sessionGeneration += 1;
|
|
1091
|
+
xaiSettingsGeneration += 1;
|
|
977
1092
|
statusGeneration += 1;
|
|
978
1093
|
clearStatusTimer();
|
|
979
1094
|
for (const controller of activeControllers) controller.abort();
|