dsh-codex-subscription 2.2.1 → 2.2.3

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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash, randomBytes, randomUUID } from "node:crypto";
3
- import { closeOpenAICodexWebSocketSessions, resetOpenAICodexWebSocketDebugStats } from "@earendil-works/pi-ai/api/openai-codex-responses";
3
+ import { closeOpenAICodexWebSocketSessions, getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats } from "@earendil-works/pi-ai/api/openai-codex-responses";
4
4
  import { execFile, spawn } from "node:child_process";
5
5
  import { request } from "node:https";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -369,6 +369,54 @@ const PREFERENCE_FIELDS = Object.freeze({
369
369
  }
370
370
  });
371
371
  //#endregion
372
+ //#region src/diagnostic-events.js
373
+ const areas = /* @__PURE__ */ new Set([
374
+ "login",
375
+ "model",
376
+ "catalog",
377
+ "quota",
378
+ "quota-reset",
379
+ "search",
380
+ "image"
381
+ ]);
382
+ const codes$1 = /* @__PURE__ */ new Set([
383
+ "timeout",
384
+ "dns",
385
+ "tls",
386
+ "connection",
387
+ "network",
388
+ "http-error"
389
+ ]);
390
+ /** Process-local bounded evidence. No bodies, URLs, exception strings or identities. */
391
+ function createDiagnosticEvents({ limit = 32, now = Date.now } = {}) {
392
+ const events = [];
393
+ const capacity = Math.max(1, Math.min(128, Number.isInteger(limit) ? limit : 32));
394
+ let dropped = 0, sequence = 0;
395
+ return {
396
+ record(area, value) {
397
+ if (!areas.has(area) || !["ok", "failed"].includes(value?.status)) return;
398
+ events.push({
399
+ sequence: ++sequence,
400
+ area,
401
+ status: value.status,
402
+ observedAt: now(),
403
+ ...codes$1.has(value.code) ? { code: value.code } : {},
404
+ ...Number.isInteger(value.httpStatus) && value.httpStatus >= 100 && value.httpStatus <= 599 ? { httpStatus: value.httpStatus } : {}
405
+ });
406
+ if (events.length > capacity) {
407
+ events.shift();
408
+ dropped++;
409
+ }
410
+ },
411
+ snapshot: () => ({
412
+ scope: "plugin-process",
413
+ capacity,
414
+ dropped,
415
+ events: events.map((value) => ({ ...value }))
416
+ })
417
+ };
418
+ }
419
+ //#endregion
372
420
  //#region src/oauth-network.js
373
421
  const execFileAsync = promisify(execFile);
374
422
  const CODEX_AUTH_HOST = "auth.openai.com";
@@ -612,6 +660,11 @@ const elapsedBucket = (elapsed) => elapsed < 1e3 ? "under-1s" : elapsed < 5e3 ?
612
660
  function createCodexNetworkTransport(options = {}) {
613
661
  const attempts = /* @__PURE__ */ new Map();
614
662
  const now = options.now ?? Date.now;
663
+ const evidence = createDiagnosticEvents({ now });
664
+ const remember = (area, value) => {
665
+ attempts.set(area, value);
666
+ evidence.record(area, value);
667
+ };
615
668
  const run = async (area, operation, connection = {}) => {
616
669
  const startedAt = now();
617
670
  let route = attempts.get(area)?.route ?? "direct";
@@ -625,7 +678,7 @@ function createCodexNetworkTransport(options = {}) {
625
678
  routed = true;
626
679
  }
627
680
  });
628
- if (value instanceof Response && !value.ok) attempts.set(area, {
681
+ if (value instanceof Response && !value.ok) remember(area, {
629
682
  status: "failed",
630
683
  stage: "http",
631
684
  code: "http-error",
@@ -633,14 +686,14 @@ function createCodexNetworkTransport(options = {}) {
633
686
  route,
634
687
  elapsed: elapsedBucket(now() - startedAt)
635
688
  });
636
- else if (routed || value instanceof Response) attempts.set(area, {
689
+ else if (routed || value instanceof Response) remember(area, {
637
690
  status: "ok",
638
691
  route,
639
692
  elapsed: elapsedBucket(now() - startedAt)
640
693
  });
641
694
  return value;
642
695
  } catch (error) {
643
- if (routed) attempts.set(area, {
696
+ if (routed) remember(area, {
644
697
  status: "failed",
645
698
  stage: "transport",
646
699
  code: classifyTransportError(error),
@@ -652,6 +705,7 @@ function createCodexNetworkTransport(options = {}) {
652
705
  };
653
706
  return Object.freeze({
654
707
  run,
708
+ history: evidence.snapshot,
655
709
  fetch: (area, input, init) => run(area, () => globalThis.fetch(input, init)),
656
710
  snapshot: () => Object.fromEntries([...attempts].map(([area, value]) => [area, { ...value }]))
657
711
  });
@@ -662,6 +716,21 @@ function createSubscriptionConnection({ resolveMode = () => "sse", resolveProxy
662
716
  const namespace = randomUUID();
663
717
  const sessions = /* @__PURE__ */ new Set();
664
718
  return {
719
+ snapshot() {
720
+ const totals = Object.fromEntries([
721
+ "requests",
722
+ "connectionsCreated",
723
+ "connectionsReused",
724
+ "deltaRequests",
725
+ "websocketFailures",
726
+ "sseFallbacks"
727
+ ].map((key) => [key, 0]));
728
+ for (const session of sessions) {
729
+ const stats = getOpenAICodexWebSocketDebugStats(session);
730
+ for (const key of Object.keys(totals)) if (Number.isSafeInteger(stats?.[key]) && stats[key] >= 0) totals[key] = Math.min(Number.MAX_SAFE_INTEGER, totals[key] + stats[key]);
731
+ }
732
+ return totals;
733
+ },
665
734
  async prepare(options = {}) {
666
735
  if (resolveMode() !== "websocket") return { options: {
667
736
  ...options,
@@ -716,6 +785,11 @@ const MAX_BYTES = 2 * 1024 * 1024;
716
785
  const MAX_AGE_MS = 10080 * 60 * 1e3;
717
786
  function createCompactionBridge({ enabled = () => false, threshold = () => 1e5, accountScope, diagnostic = () => {}, now = Date.now }) {
718
787
  const scope = new AsyncLocalStorage();
788
+ const stats = {
789
+ requests: 0,
790
+ checkpointsSaved: 0,
791
+ checkpointsReused: 0
792
+ };
719
793
  const wrapStream = (factory, options) => (async function* () {
720
794
  if (!enabled() || options.provider !== "openai-codex") {
721
795
  yield* factory(options);
@@ -775,6 +849,7 @@ function createCompactionBridge({ enabled = () => false, threshold = () => 1e5,
775
849
  }
776
850
  }
777
851
  };
852
+ stats.checkpointsSaved = Math.min(Number.MAX_SAFE_INTEGER, stats.checkpointsSaved + 1);
778
853
  }
779
854
  yield event;
780
855
  }
@@ -783,6 +858,7 @@ function createCompactionBridge({ enabled = () => false, threshold = () => 1e5,
783
858
  }
784
859
  })();
785
860
  return {
861
+ snapshot: () => ({ ...stats }),
786
862
  wrapAdapter(adapter) {
787
863
  return new Proxy(adapter, { get(target, property) {
788
864
  if (property === "stream") return (options) => wrapStream((o) => target.stream(o), options);
@@ -800,6 +876,11 @@ function createCompactionBridge({ enabled = () => false, threshold = () => 1e5,
800
876
  preparePayload(payload, contextWindow) {
801
877
  const state = scope.getStore();
802
878
  if (!state) return payload;
879
+ if (!state.counted) {
880
+ state.counted = true;
881
+ stats.requests = Math.min(Number.MAX_SAFE_INTEGER, stats.requests + 1);
882
+ if (state.suffix) stats.checkpointsReused = Math.min(Number.MAX_SAFE_INTEGER, stats.checkpointsReused + 1);
883
+ }
803
884
  const configured = threshold();
804
885
  if (!Number.isSafeInteger(configured) || configured < 1e3) throw new Error("Invalid compaction threshold");
805
886
  const limit = Number.isFinite(contextWindow) && contextWindow >= 2e3 ? Math.min(configured, Math.floor(contextWindow / 2)) : configured;
@@ -902,6 +983,8 @@ const RPC_ENDPOINTS = Object.freeze([
902
983
  "runtime/install",
903
984
  "runtime/remove",
904
985
  "runtime/cancel",
986
+ "storage/status",
987
+ "storage/clear-forecast",
905
988
  "reset-credit/inspect",
906
989
  "reset-credit/prepare",
907
990
  "reset-credit/consume",
@@ -1287,6 +1370,21 @@ function createSubagentBackendSwitcher({ entries, prepare, persist }) {
1287
1370
  backgroundMode: "one-shot",
1288
1371
  maxDepth: "provider-managed"
1289
1372
  });
1373
+ const ownedFields = [
1374
+ "provider",
1375
+ "modelSelectionSettings",
1376
+ "backgroundMode",
1377
+ "maxDepth"
1378
+ ];
1379
+ const restore = (current, original) => {
1380
+ const result = { ...current }, applied = convert(original);
1381
+ for (const key of ownedFields) {
1382
+ if (current[key] !== applied[key]) continue;
1383
+ if (Object.hasOwn(original, key)) result[key] = original[key];
1384
+ else delete result[key];
1385
+ }
1386
+ return result;
1387
+ };
1290
1388
  const configure = (fiber, config) => {
1291
1389
  if (!standard(fiber.entry, config)) return config;
1292
1390
  if (!originals.has(fiber)) originals.set(fiber, { ...config });
@@ -1316,7 +1414,8 @@ function createSubagentBackendSwitcher({ entries, prepare, persist }) {
1316
1414
  fiber,
1317
1415
  before
1318
1416
  });
1319
- await fiber.update(mode === "codex" ? convert(original) : original, true);
1417
+ if (mode === "codex" && before.provider !== "codex-subscription-subagent") originals.set(fiber, { ...before });
1418
+ await fiber.update(mode === "codex" ? convert(before) : restore(before, original), true);
1320
1419
  }
1321
1420
  await persist?.(mode);
1322
1421
  } catch (error) {
@@ -1335,7 +1434,7 @@ function createSubagentBackendSwitcher({ entries, prepare, persist }) {
1335
1434
  await tail.catch(() => {});
1336
1435
  disposed = true;
1337
1436
  selected = "dsh";
1338
- for (const [fiber, original] of originals) if (fiber.config?.provider === "codex-subscription-subagent") await fiber.update(original, true);
1437
+ for (const [fiber, original] of originals) if (fiber.config?.provider === "codex-subscription-subagent") await fiber.update(restore(fiber.config, original), true);
1339
1438
  originals.clear();
1340
1439
  }
1341
1440
  };
@@ -2831,8 +2930,28 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
2831
2930
  }
2832
2931
  Object.freeze(["0.82.1", "0.85.1"]);
2833
2932
  //#endregion
2933
+ //#region src/subscription-credentials.js
2934
+ async function readSubscriptionCredentials(getAuth, readCredential, signal) {
2935
+ for (let attempt = 0; attempt < 2; attempt++) {
2936
+ const auth = await getAuth({ signal });
2937
+ signal?.throwIfAborted();
2938
+ if (typeof auth?.auth?.apiKey !== "string" || !auth.auth.apiKey) break;
2939
+ const credential = await readCredential({ signal });
2940
+ signal?.throwIfAborted();
2941
+ if (credential?.type !== "oauth") break;
2942
+ if (credential.access === auth.auth.apiKey) return {
2943
+ access: credential.access,
2944
+ accountId: credential.accountId
2945
+ };
2946
+ }
2947
+ return {
2948
+ access: void 0,
2949
+ accountId: void 0
2950
+ };
2951
+ }
2952
+ //#endregion
2834
2953
  //#region src/version.js
2835
- const PACKAGE_VERSION = "2.2.1";
2954
+ const PACKAGE_VERSION = "2.2.3";
2836
2955
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2837
2956
  //#endregion
2838
2957
  //#region src/model-catalog.js
@@ -2957,12 +3076,8 @@ function createOfficialModelCatalog(options = {}) {
2957
3076
  let timer;
2958
3077
  const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
2959
3078
  const work = (async () => {
2960
- const auth = await options.getAuth({ signal: requestSignal });
3079
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, requestSignal);
2961
3080
  if (currentGeneration !== generation || requestSignal.aborted) return false;
2962
- const credential = await options.readCredential({ signal: requestSignal });
2963
- if (currentGeneration !== generation || requestSignal.aborted) return false;
2964
- const access = auth?.auth?.apiKey;
2965
- const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
2966
3081
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
2967
3082
  const headers = {
2968
3083
  authorization: `Bearer ${access}`,
@@ -2996,6 +3111,7 @@ function createOfficialModelCatalog(options = {}) {
2996
3111
  etag = nonEmpty$2(response.headers.get("etag")) ?? etag;
2997
3112
  revision += 1;
2998
3113
  outcome = "ok";
3114
+ options.onUpdated?.();
2999
3115
  return true;
3000
3116
  })();
3001
3117
  let rejectAborted;
@@ -3048,6 +3164,7 @@ function createOfficialModelCatalog(options = {}) {
3048
3164
  etag = void 0;
3049
3165
  refreshStatus = "idle";
3050
3166
  revision += 1;
3167
+ options.onUpdated?.();
3051
3168
  }
3052
3169
  });
3053
3170
  }
@@ -3117,10 +3234,7 @@ function createCodexSearchProvider(options) {
3117
3234
  signal?.throwIfAborted();
3118
3235
  const preferences = readCapabilitySettings(options.resolvePreferences?.());
3119
3236
  if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
3120
- const auth = await options.getAuth({ signal });
3121
- const credential = await options.readCredential({ signal });
3122
- const access = auth?.auth?.apiKey;
3123
- const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
3237
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, signal);
3124
3238
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
3125
3239
  const model = nonEmpty$1(options.resolveModel?.()) ?? DEFAULT_MODEL;
3126
3240
  const id = nonEmpty$1(options.resolveSessionId?.()) ?? randomUUID();
@@ -3631,10 +3745,7 @@ function createCodexImageTool(options) {
3631
3745
  const prompt = nonEmpty(args.prompt);
3632
3746
  if (prompt === void 0) throw new Error("prompt must be a non-empty string");
3633
3747
  const imageOptions = normalizeImageOptions(args);
3634
- const auth = await options.getAuth({ signal: exec.signal });
3635
- const credential = await options.readCredential({ signal: exec.signal });
3636
- const access = auth?.auth?.apiKey;
3637
- const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
3748
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, exec.signal);
3638
3749
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
3639
3750
  if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
3640
3751
  const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
@@ -3700,7 +3811,7 @@ function createCodexImageTool(options) {
3700
3811
  localPath: options.originalImages.originalPath(original.assetId),
3701
3812
  ...metadata.background === void 0 ? {} : { background: metadata.background },
3702
3813
  ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
3703
- ...metadata.size === void 0 ? {} : { size: metadata.size }
3814
+ size: `${original.width}x${original.height}`
3704
3815
  };
3705
3816
  if (exec.parent !== void 0) exec.deferContext(createUserMessage({
3706
3817
  content: imageContent(result),
@@ -3876,6 +3987,229 @@ var OriginalImageStore = class {
3876
3987
  }
3877
3988
  };
3878
3989
  //#endregion
3990
+ //#region src/diagnostic-checks.js
3991
+ const states = /* @__PURE__ */ new Set([
3992
+ "pass",
3993
+ "fail",
3994
+ "warn",
3995
+ "unknown",
3996
+ "not-applicable"
3997
+ ]);
3998
+ /** Adapters return bounded reason codes, never exceptions or raw service data. */
3999
+ async function collectChecks(definitions, { timeoutMs = 1500, now = Date.now, source = "server", scope = "plugin-process" } = {}) {
4000
+ return Promise.all(definitions.map(async ({ id, capability, run }) => {
4001
+ const startedAt = now();
4002
+ let timer;
4003
+ const controller = new AbortController();
4004
+ const base = {
4005
+ id,
4006
+ capability,
4007
+ source,
4008
+ scope
4009
+ };
4010
+ try {
4011
+ if (!run) return {
4012
+ ...base,
4013
+ status: "unknown",
4014
+ reason: "not-instrumented",
4015
+ observedAt: now()
4016
+ };
4017
+ const result = await Promise.race([Promise.resolve().then(() => run(controller.signal)), new Promise((resolve) => {
4018
+ timer = setTimeout(() => {
4019
+ controller.abort();
4020
+ resolve({
4021
+ status: "unknown",
4022
+ reason: "inspection-timeout"
4023
+ });
4024
+ }, timeoutMs);
4025
+ })]);
4026
+ const valid = states.has(result?.status) && /^[a-z][a-z0-9-]{0,63}$/u.test(result?.reason ?? "");
4027
+ return {
4028
+ ...base,
4029
+ status: valid ? result.status : "unknown",
4030
+ reason: valid ? result.reason : "invalid-inspection-result",
4031
+ observedAt: now(),
4032
+ elapsedMs: Math.max(0, now() - startedAt)
4033
+ };
4034
+ } catch {
4035
+ return {
4036
+ ...base,
4037
+ status: "unknown",
4038
+ reason: "inspection-failed",
4039
+ observedAt: now(),
4040
+ elapsedMs: Math.max(0, now() - startedAt)
4041
+ };
4042
+ } finally {
4043
+ clearTimeout(timer);
4044
+ }
4045
+ }));
4046
+ }
4047
+ const diagnosticCapabilities = Object.freeze([
4048
+ "account",
4049
+ "accounts",
4050
+ "settings",
4051
+ "models",
4052
+ "quota",
4053
+ "search",
4054
+ "images",
4055
+ "sketch",
4056
+ "subagents",
4057
+ "compaction",
4058
+ "transport",
4059
+ "storage",
4060
+ "host-ui"
4061
+ ]);
4062
+ //#endregion
4063
+ //#region src/diagnostic-capabilities.js
4064
+ function capabilityCoverage({ account, preference, runtime, catalog, tools, storage, checks, operations, now = Date.now() }) {
4065
+ const disabled = {
4066
+ images: preference.imageGeneration === false && preference.imageEditing === false,
4067
+ sketch: preference.imageSketch === false || preference.imageEditing === false,
4068
+ search: preference.searchMode === "disabled",
4069
+ compaction: preference.compactionMode === "dsh",
4070
+ subagents: preference.subagentBackend === "dsh"
4071
+ };
4072
+ return diagnosticCapabilities.map((id) => {
4073
+ let readiness = "unknown", reason = "not-instrumented", blockedBy;
4074
+ if (disabled[id]) {
4075
+ readiness = "not-applicable";
4076
+ reason = "disabled-or-host-owned";
4077
+ } else if ([
4078
+ "models",
4079
+ "quota",
4080
+ "search",
4081
+ "images",
4082
+ "compaction"
4083
+ ].includes(id) && account.status === "signed-out") {
4084
+ readiness = "warn";
4085
+ reason = "sign-in-required";
4086
+ blockedBy = "account";
4087
+ } else if (id === "account") {
4088
+ readiness = account.status === "signed-in" ? "pass" : "unknown";
4089
+ reason = account.status === "signed-in" ? "local-sign-in-present" : "sign-in-unverified";
4090
+ } else if (id === "settings" && typeof preference.writable === "boolean") {
4091
+ readiness = preference.writable ? "pass" : "warn";
4092
+ reason = preference.writable ? "settings-writable" : "settings-readonly";
4093
+ } else if (id === "subagents" && runtime) {
4094
+ readiness = runtime.installed && !runtime.restartRequired ? "pass" : "warn";
4095
+ reason = runtime.restartRequired ? "restart-required" : runtime.installed ? "runtime-present" : "runtime-missing";
4096
+ } else if (id === "models" && catalog) {
4097
+ readiness = catalog.source === "online" ? "pass" : "warn";
4098
+ reason = catalog.source === "online" ? "catalog-online" : "catalog-fallback";
4099
+ } else if (id === "storage" && storage?.readable) {
4100
+ readiness = "pass";
4101
+ reason = "storage-readable";
4102
+ } else if (tools && (id === "images" || id === "sketch" && preference.imageSketchAgent === true)) {
4103
+ readiness = tools[id] ? "pass" : "warn";
4104
+ reason = tools[id] ? "tool-registered" : "tool-missing";
4105
+ }
4106
+ const recent = (operations?.events ?? []).filter((event) => event.capability === id && now >= event.observedAt && now - event.observedAt <= 15 * 6e4).at(-1);
4107
+ return {
4108
+ id,
4109
+ readiness,
4110
+ reason,
4111
+ ...blockedBy ? { blockedBy } : {},
4112
+ checks: checks.filter((check) => check.capability === id).map((check) => check.id),
4113
+ execution: recent ? "observed" : "not-verified",
4114
+ ...recent ? { latest: {
4115
+ action: recent.action,
4116
+ source: recent.source,
4117
+ status: recent.status,
4118
+ observedAt: recent.observedAt
4119
+ } } : {}
4120
+ };
4121
+ });
4122
+ }
4123
+ //#endregion
4124
+ //#region src/diagnostic-operations.js
4125
+ const actions = Object.freeze({
4126
+ "login/start": "account",
4127
+ "login/submit": "account",
4128
+ "login/cancel": "account",
4129
+ logout: "account",
4130
+ "account/select": "accounts",
4131
+ "account/remove": "accounts",
4132
+ "preferences/update": "settings",
4133
+ "account/cleanup": "accounts",
4134
+ usage: "quota",
4135
+ "preferences/models": "models",
4136
+ "storage/status": "storage",
4137
+ "storage/clear-forecast": "storage",
4138
+ "runtime/install": "subagents",
4139
+ "runtime/remove": "subagents",
4140
+ "runtime/cancel": "subagents",
4141
+ codex_image_generate: "images",
4142
+ codex_sketch: "sketch"
4143
+ });
4144
+ function safeOperations(raw) {
4145
+ if (!Array.isArray(raw?.events)) return void 0;
4146
+ return {
4147
+ scope: "plugin-process",
4148
+ capacity: 32,
4149
+ dropped: (Number.isSafeInteger(raw.dropped) && raw.dropped >= 0 ? raw.dropped : 0) + Math.max(0, raw.events.length - 32),
4150
+ events: raw.events.slice(-32).flatMap((value) => {
4151
+ if (!Object.hasOwn(actions, value?.action ?? "") || ![
4152
+ "completed",
4153
+ "failed",
4154
+ "cancelled"
4155
+ ].includes(value.status) || !["rpc", "tool"].includes(value.source) || !Number.isSafeInteger(value.observedAt) || value.observedAt < 0) return [];
4156
+ return [{
4157
+ capability: actions[value.action],
4158
+ action: value.action,
4159
+ status: value.status,
4160
+ source: value.source,
4161
+ observedAt: value.observedAt
4162
+ }];
4163
+ })
4164
+ };
4165
+ }
4166
+ function createDiagnosticOperations({ now = Date.now } = {}) {
4167
+ const events = [];
4168
+ let sequence = 0, dropped = 0;
4169
+ const record = (action, status, source = "rpc") => {
4170
+ if (!Object.hasOwn(actions, action) || ![
4171
+ "completed",
4172
+ "failed",
4173
+ "cancelled"
4174
+ ].includes(status) || !["rpc", "tool"].includes(source)) return;
4175
+ events.push({
4176
+ sequence: ++sequence,
4177
+ capability: actions[action],
4178
+ action,
4179
+ source,
4180
+ status,
4181
+ observedAt: now()
4182
+ });
4183
+ if (events.length > 32) {
4184
+ events.shift();
4185
+ dropped++;
4186
+ }
4187
+ };
4188
+ return {
4189
+ record,
4190
+ wrap: (handler) => async (endpoint, payload, signal) => {
4191
+ try {
4192
+ const result = await handler(endpoint, payload, signal);
4193
+ record(endpoint, result?.ok === true ? "completed" : "failed");
4194
+ return result;
4195
+ } catch (error) {
4196
+ record(endpoint, signal?.aborted ? "cancelled" : "failed");
4197
+ throw error;
4198
+ }
4199
+ },
4200
+ tool: (exec, result) => {
4201
+ if (!["codex_image_generate", "codex_sketch"].includes(exec?.name)) return;
4202
+ record(exec.name, exec.signal?.aborted ? "cancelled" : result?.isError === true ? "failed" : result?.isError === false ? "completed" : "failed", "tool");
4203
+ },
4204
+ snapshot: () => ({
4205
+ scope: "plugin-process",
4206
+ capacity: 32,
4207
+ dropped,
4208
+ events: events.map((value) => ({ ...value }))
4209
+ })
4210
+ };
4211
+ }
4212
+ //#endregion
3879
4213
  //#region src/diagnostics.js
3880
4214
  const requestAreas = /* @__PURE__ */ new Set([
3881
4215
  "login",
@@ -3908,6 +4242,26 @@ const elapsedBuckets = /* @__PURE__ */ new Set([
3908
4242
  "5-15s",
3909
4243
  "over-15s"
3910
4244
  ]);
4245
+ function safeHistory(raw) {
4246
+ if (!raw || !Array.isArray(raw.events)) return void 0;
4247
+ const events = raw.events.slice(-32).flatMap((value) => {
4248
+ if (!value || !requestAreas.has(value.area) || !statuses.has(value.status) || !Number.isSafeInteger(value.sequence) || value.sequence < 1 || !Number.isSafeInteger(value.observedAt) || value.observedAt < 0) return [];
4249
+ return [{
4250
+ area: value.area,
4251
+ status: value.status,
4252
+ sequence: value.sequence,
4253
+ observedAt: value.observedAt,
4254
+ ...codes.has(value.code) ? { code: value.code } : {},
4255
+ ...Number.isInteger(value.httpStatus) && value.httpStatus >= 100 && value.httpStatus <= 599 ? { httpStatus: value.httpStatus } : {}
4256
+ }];
4257
+ });
4258
+ return {
4259
+ scope: "plugin-process",
4260
+ capacity: 32,
4261
+ dropped: (Number.isSafeInteger(raw.dropped) && raw.dropped >= 0 ? raw.dropped : 0) + Math.max(0, raw.events.length - 32),
4262
+ events
4263
+ };
4264
+ }
3911
4265
  function safeRequests(network) {
3912
4266
  const raw = network?.snapshot?.() ?? {};
3913
4267
  const result = {};
@@ -3926,17 +4280,113 @@ function safeRequests(network) {
3926
4280
  return result;
3927
4281
  }
3928
4282
  /** Build a support report that deliberately excludes OAuth and account metadata. */
3929
- async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network, modelCatalog }) {
4283
+ async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network, modelCatalog, connection, compaction, inspectionOptions, operations, runtimeManagement, storage, tools }) {
4284
+ const collected = {};
4285
+ const checks = await collectChecks([
4286
+ [
4287
+ "account",
4288
+ "account",
4289
+ () => auth.status()
4290
+ ],
4291
+ [
4292
+ "preferences",
4293
+ "settings",
4294
+ () => preferences.status()
4295
+ ],
4296
+ [
4297
+ "catalog",
4298
+ "models",
4299
+ () => modelCatalog?.status?.()
4300
+ ],
4301
+ [
4302
+ "gaps",
4303
+ "models",
4304
+ () => modelCatalog?.capabilityGaps?.()
4305
+ ],
4306
+ [
4307
+ "requests",
4308
+ "transport",
4309
+ () => network?.snapshot ? safeRequests(network) : void 0
4310
+ ],
4311
+ [
4312
+ "history",
4313
+ "transport",
4314
+ () => safeHistory(network?.history?.())
4315
+ ],
4316
+ [
4317
+ "websocket",
4318
+ "transport",
4319
+ () => connection ? safeCounters(connection, [
4320
+ "requests",
4321
+ "connectionsCreated",
4322
+ "connectionsReused",
4323
+ "deltaRequests",
4324
+ "websocketFailures",
4325
+ "sseFallbacks"
4326
+ ]) : void 0
4327
+ ],
4328
+ [
4329
+ "compaction",
4330
+ "compaction",
4331
+ () => compaction ? safeCounters(compaction, [
4332
+ "requests",
4333
+ "checkpointsSaved",
4334
+ "checkpointsReused"
4335
+ ]) : void 0
4336
+ ],
4337
+ [
4338
+ "operations",
4339
+ "transport",
4340
+ () => safeOperations(operations?.snapshot?.())
4341
+ ],
4342
+ [
4343
+ "runtime",
4344
+ "subagents",
4345
+ async () => {
4346
+ const value = await runtimeManagement?.status?.();
4347
+ return value ? Object.fromEntries([
4348
+ "available",
4349
+ "installed",
4350
+ "restartRequired"
4351
+ ].filter((key) => typeof value[key] === "boolean").map((key) => [key, value[key]])) : void 0;
4352
+ }
4353
+ ],
4354
+ [
4355
+ "storage",
4356
+ "storage",
4357
+ async () => typeof storage === "function" ? (await storage(), { readable: true }) : void 0
4358
+ ],
4359
+ [
4360
+ "tools",
4361
+ "host-ui",
4362
+ () => typeof tools?.get === "function" ? {
4363
+ images: !!tools.get("codex_image_generate"),
4364
+ sketch: !!tools.get("codex_sketch")
4365
+ } : void 0
4366
+ ]
4367
+ ].map(([id, capability, read]) => ({
4368
+ id,
4369
+ capability,
4370
+ run: read ? async (signal) => {
4371
+ const value = await read();
4372
+ if (value === void 0) return {
4373
+ status: "unknown",
4374
+ reason: "not-instrumented"
4375
+ };
4376
+ if (!signal.aborted) collected[id] = value;
4377
+ return {
4378
+ status: "pass",
4379
+ reason: "inspection-completed"
4380
+ };
4381
+ } : void 0
4382
+ })), inspectionOptions);
3930
4383
  let account = { status: "unknown" };
3931
4384
  const issues = [];
3932
- try {
3933
- account = { status: (await auth.status()).authenticated === true ? "signed-in" : "signed-out" };
3934
- } catch {
3935
- issues.push({ code: "account-status-unavailable" });
3936
- }
3937
- const preference = preferences.status();
3938
- const catalog = modelCatalog?.status?.();
3939
- const gaps = (modelCatalog?.capabilityGaps?.() ?? []).slice(0, 20).flatMap((value) => {
4385
+ if (collected.account) account = { status: collected.account.authenticated === true ? "signed-in" : "signed-out" };
4386
+ else issues.push({ code: "account-status-unavailable" });
4387
+ const preference = collected.preferences ?? {};
4388
+ const catalog = collected.catalog;
4389
+ const gaps = (Array.isArray(collected.gaps) ? collected.gaps : []).slice(0, 20).flatMap((value) => {
3940
4390
  if (!value || typeof value.model !== "string" || !/^[a-z][a-z0-9._-]{0,79}$/u.test(value.model)) return [];
3941
4391
  const fields = Object.fromEntries([
3942
4392
  "reasoning",
@@ -3954,6 +4404,22 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
3954
4404
  if (gaps.length) issues.push({ code: "catalog-capabilities-not-adapted" });
3955
4405
  return {
3956
4406
  schemaVersion: 3,
4407
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4408
+ inspection: {
4409
+ schemaVersion: 1,
4410
+ meaning: "collection-success-is-not-feature-success",
4411
+ checks,
4412
+ capabilities: capabilityCoverage({
4413
+ account,
4414
+ preference,
4415
+ runtime: collected.runtime,
4416
+ catalog,
4417
+ tools: collected.tools,
4418
+ storage: collected.storage,
4419
+ checks,
4420
+ operations: collected.operations
4421
+ })
4422
+ },
3957
4423
  package: "dsh-codex-subscription",
3958
4424
  version: PACKAGE_VERSION,
3959
4425
  runtime: {
@@ -3963,7 +4429,11 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
3963
4429
  },
3964
4430
  account,
3965
4431
  login,
3966
- requests: safeRequests(network),
4432
+ requests: collected.requests ?? {},
4433
+ ...collected.history ? { requestHistory: collected.history } : {},
4434
+ ...collected.operations ? { operations: collected.operations } : {},
4435
+ ...collected.websocket ? { websocket: collected.websocket } : {},
4436
+ ...collected.compaction ? { compaction: collected.compaction } : {},
3967
4437
  ...catalog && ["fallback", "online"].includes(catalog.source) && [
3968
4438
  "idle",
3969
4439
  "refreshing",
@@ -3981,11 +4451,15 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
3981
4451
  ...typeof preference.outputVerbosity === "string" ? { outputVerbosity: preference.outputVerbosity } : {},
3982
4452
  searchProvider: preference.searchProvider,
3983
4453
  speedMode: preference.speedMode,
3984
- writable: preference.writable === true
4454
+ ...typeof preference.writable === "boolean" ? { writable: preference.writable } : {}
3985
4455
  },
3986
4456
  issues
3987
4457
  };
3988
4458
  }
4459
+ function safeCounters(source, keys) {
4460
+ const values = source.snapshot();
4461
+ return Object.fromEntries(keys.filter((key) => Number.isSafeInteger(values?.[key]) && values[key] >= 0).map((key) => [key, values[key]]));
4462
+ }
3989
4463
  //#endregion
3990
4464
  //#region src/usage.js
3991
4465
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
@@ -4144,10 +4618,7 @@ function createCodexUsageReader(options) {
4144
4618
  let inFlight;
4145
4619
  let generation = 0;
4146
4620
  const load = async (signal) => {
4147
- const auth = await getAuth({ signal });
4148
- const credential = await readCredential({ signal });
4149
- const access = auth?.auth?.apiKey;
4150
- const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
4621
+ const { access, accountId } = await readSubscriptionCredentials(getAuth, readCredential, signal);
4151
4622
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
4152
4623
  const response = await fetchUsage(CODEX_USAGE_URL, {
4153
4624
  method: "GET",
@@ -4683,6 +5154,7 @@ function createQuotaForecastReader({ reader, enabled, now = Date.now, scope = ()
4683
5154
  return current === generation ? forecast.usage : usage;
4684
5155
  },
4685
5156
  clear: () => clearHistory(),
5157
+ storage: () => stateStore.usage(),
4686
5158
  clearCache() {
4687
5159
  generation += 1;
4688
5160
  reader.clear();
@@ -4768,6 +5240,22 @@ var QuotaForecastStateStore = class {
4768
5240
  async clear() {
4769
5241
  await rm(this.filename, { force: true });
4770
5242
  }
5243
+ async usage() {
5244
+ try {
5245
+ const info = await lstat(this.filename);
5246
+ if (!info.isFile() || info.isSymbolicLink()) throw Error("Unexpected forecast cache entry");
5247
+ return {
5248
+ bytes: info.size,
5249
+ limit: this.maxBytes
5250
+ };
5251
+ } catch (error) {
5252
+ if (error.code === "ENOENT") return {
5253
+ bytes: 0,
5254
+ limit: this.maxBytes
5255
+ };
5256
+ throw error;
5257
+ }
5258
+ }
4771
5259
  };
4772
5260
  //#endregion
4773
5261
  //#region src/reset-credits.js
@@ -4786,9 +5274,7 @@ const requestSignal = (signal, timeoutMs) => {
4786
5274
  function safeCopy(value) {
4787
5275
  return typeof value === "string" && value.length > 0 ? value.slice(0, MAX_COPY_LENGTH) : void 0;
4788
5276
  }
4789
- function credentialsOf(auth, credential) {
4790
- const access = auth?.auth?.apiKey;
4791
- const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
5277
+ function credentialsOf({ access, accountId }) {
4792
5278
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
4793
5279
  return {
4794
5280
  access,
@@ -4873,7 +5359,7 @@ function createCodexResetCreditService(options) {
4873
5359
  ...credit.title === void 0 ? {} : { name: credit.title },
4874
5360
  ...credit.creditExpiresAt === void 0 ? {} : { expiresAt: credit.creditExpiresAt }
4875
5361
  });
4876
- const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
5362
+ const resolveCredentials = async (signal) => credentialsOf(await readSubscriptionCredentials(getAuth, readCredential, signal));
4877
5363
  const readDetails = async (signal, credentials) => {
4878
5364
  const { access, accountId } = credentials ?? await resolveCredentials(signal);
4879
5365
  const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
@@ -5064,8 +5550,19 @@ const publicError = (code, message) => ({
5064
5550
  details: { issues: [] }
5065
5551
  }
5066
5552
  });
5067
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged }) {
5553
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
5068
5554
  return async (endpoint, payload, signal) => {
5555
+ if (endpoint === "storage/status" || endpoint === "storage/clear-forecast") try {
5556
+ signal.throwIfAborted();
5557
+ if (endpoint === "storage/clear-forecast") await usageReader.clear();
5558
+ return {
5559
+ ok: true,
5560
+ value: await usageReader.storage()
5561
+ };
5562
+ } catch (error) {
5563
+ if (signal.aborted) throw error;
5564
+ return publicError("operation-error", "Could not manage forecast cache");
5565
+ }
5069
5566
  if ([
5070
5567
  "runtime/status",
5071
5568
  "runtime/install",
@@ -5211,23 +5708,20 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
5211
5708
  return publicError("internal", message);
5212
5709
  }
5213
5710
  const result = await authHandler(endpoint, payload, signal);
5214
- if (result.ok === true && [
5215
- "logout",
5216
- "account/select",
5217
- "account/remove"
5218
- ].includes(endpoint)) closeConnections?.();
5219
- if (endpoint === "account/remove" && result.ok === true && typeof payload?.id === "string") await usageReader.clearScope(payload.id);
5220
- if (endpoint === "logout" && result.ok === true) {
5221
- await usageReader.clear();
5222
- resetCreditService.clear();
5223
- modelCatalog?.clear();
5224
- onAccountChanged?.();
5225
- } else if (result.ok === true && (endpoint === "account/select" || endpoint === "account/remove" || endpoint === "login/status" && result.value?.authenticated === true)) {
5226
- usageReader.clearCache();
5227
- resetCreditService.clear();
5228
- modelCatalog?.clear();
5229
- onAccountChanged?.();
5230
- modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
5711
+ if (result.ok === true && (endpoint === "logout" || endpoint === "account/select" || endpoint === "account/remove" || endpoint === "login/status" && result.value?.authenticated === true)) {
5712
+ const tasks = [
5713
+ () => closeConnections?.(),
5714
+ () => usageReader.clearCache(),
5715
+ () => resetCreditService.clear(),
5716
+ () => modelCatalog?.clear(),
5717
+ () => onAccountChanged?.()
5718
+ ];
5719
+ if (endpoint === "logout") tasks.push(() => usageReader.clear());
5720
+ if (endpoint === "account/remove" && typeof payload?.id === "string") tasks.push(() => usageReader.clearScope(payload.id));
5721
+ if ((await Promise.allSettled(tasks.map((task) => Promise.resolve().then(task)))).some((value) => value.status === "rejected")) try {
5722
+ onCleanupFailure?.();
5723
+ } catch {}
5724
+ if (endpoint !== "logout") Promise.resolve().then(() => modelCatalog?.refresh({ signal: void 0 })).catch(() => {});
5231
5725
  } else if (result.ok === true && (endpoint === "status" || result.value?.authenticated === true)) modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
5232
5726
  return result;
5233
5727
  };
@@ -5305,6 +5799,8 @@ function apply(ctx, config = {}) {
5305
5799
  const settings = createSettingsAdapter(ctx, z.object(settingsFields), config, SETTINGS_NAMESPACE);
5306
5800
  const searchProvider = createSearchProviderSwitcher(ctx.loader);
5307
5801
  const network = createCodexNetworkTransport();
5802
+ const diagnosticOperations = createDiagnosticOperations();
5803
+ ctx.effect(() => ctx.on?.("tools/result", (exec, result) => diagnosticOperations.tool(exec, result)), "codex-subscription: operation evidence");
5308
5804
  const originalImages = new OriginalImageStore();
5309
5805
  const accountVault = ACCOUNT_VAULT_KEY !== void 0 && typeof ctx.credentials.readRecord === "function" && typeof ctx.credentials.modifyRecord === "function" && typeof ctx.credentials.deleteRecord === "function" ? new DshOAuthAccountVault(ctx.credentials, {
5310
5806
  key: ACCOUNT_VAULT_KEY,
@@ -5329,6 +5825,7 @@ function apply(ctx, config = {}) {
5329
5825
  });
5330
5826
  ctx.effect(() => ctx.on?.("plugin-manager/install-state", (value) => runtimeManagement.progress(value)));
5331
5827
  const modelCatalog = createOfficialModelCatalog({
5828
+ onUpdated: () => ctx.llm.emitAdaptersUpdated?.(),
5332
5829
  getAuth: (options) => resolveAuth(options),
5333
5830
  readCredential: (options) => store.read(PROVIDER, options),
5334
5831
  baseModels: () => baseProvider.getModels(),
@@ -5635,14 +6132,21 @@ function apply(ctx, config = {}) {
5635
6132
  preferences,
5636
6133
  login: coordinator.supportState(),
5637
6134
  network,
5638
- modelCatalog
6135
+ modelCatalog,
6136
+ connection,
6137
+ compaction,
6138
+ operations: diagnosticOperations,
6139
+ runtimeManagement,
6140
+ storage: () => usageReader.storage(),
6141
+ tools: ctx.tools
5639
6142
  }),
6143
+ onCleanupFailure: () => diagnosticOperations.record("account/cleanup", "failed"),
5640
6144
  modelCatalog,
5641
6145
  closeConnections: () => connection.dispose(),
5642
6146
  originalImages,
5643
6147
  resolveInheritedOriginal: (sessionId, assetId) => inheritedOriginalImageRef(ctx.get?.("sessions")?.get?.(sessionId), assetId)
5644
6148
  });
5645
- const handler = (endpoint, payload, signal) => endpoint.startsWith("sketch/") ? sketchBridge.rpc(endpoint, payload) : subscriptionHandler(endpoint, payload, signal);
6149
+ const handler = diagnosticOperations.wrap((endpoint, payload, signal) => endpoint.startsWith("sketch/") ? sketchBridge.rpc(endpoint, payload) : subscriptionHandler(endpoint, payload, signal));
5646
6150
  ctx.effect(() => {
5647
6151
  modelCatalog.refresh().catch((error) => ctx.logger?.debug?.("could not refresh Codex model catalog: %s", error.message));
5648
6152
  }, "codex-subscription: official model catalog");