@eddyskywalker/dsh-chatgpt-subscription 0.10.11 → 0.10.13

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +176 -42
  3. package/lib/client.js +133 -26
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +1145 -561
  6. package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -1
  7. package/lib/types/client/claude/locales.d.ts +17 -2
  8. package/lib/types/client/claude/locales.d.ts.map +1 -1
  9. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  10. package/lib/types/client/kimi-code/locales.d.ts +17 -2
  11. package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
  12. package/lib/types/compat.d.ts +35 -2
  13. package/lib/types/compat.d.ts.map +1 -1
  14. package/lib/types/host/adapter.d.ts +20 -1
  15. package/lib/types/host/adapter.d.ts.map +1 -1
  16. package/lib/types/host/claude/account-pool.d.ts +4 -21
  17. package/lib/types/host/claude/account-pool.d.ts.map +1 -1
  18. package/lib/types/host/claude/adapter.d.ts +11 -0
  19. package/lib/types/host/claude/adapter.d.ts.map +1 -1
  20. package/lib/types/host/claude/client.d.ts +7 -2
  21. package/lib/types/host/claude/client.d.ts.map +1 -1
  22. package/lib/types/host/claude/mapper.d.ts +13 -1
  23. package/lib/types/host/claude/mapper.d.ts.map +1 -1
  24. package/lib/types/host/claude/routes.d.ts.map +1 -1
  25. package/lib/types/host/claude/token-store.d.ts +9 -0
  26. package/lib/types/host/claude/token-store.d.ts.map +1 -1
  27. package/lib/types/host/claude/types.d.ts +35 -0
  28. package/lib/types/host/claude/types.d.ts.map +1 -1
  29. package/lib/types/host/codex-account-pool.d.ts.map +1 -1
  30. package/lib/types/host/codex-catalog.d.ts +54 -0
  31. package/lib/types/host/codex-catalog.d.ts.map +1 -0
  32. package/lib/types/host/codex-images.d.ts.map +1 -1
  33. package/lib/types/host/codex-search.d.ts.map +1 -1
  34. package/lib/types/host/common/account-pool.d.ts +93 -6
  35. package/lib/types/host/common/account-pool.d.ts.map +1 -1
  36. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  37. package/lib/types/host/kimi-code/mapper.d.ts +13 -7
  38. package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
  39. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  40. package/lib/types/host/kimi-code/token-store.d.ts +11 -1
  41. package/lib/types/host/kimi-code/token-store.d.ts.map +1 -1
  42. package/lib/types/host/kimi-code/types.d.ts +13 -0
  43. package/lib/types/host/kimi-code/types.d.ts.map +1 -1
  44. package/lib/types/host/minimax-code/account-pool.d.ts +2 -2
  45. package/lib/types/host/minimax-code/account-pool.d.ts.map +1 -1
  46. package/lib/types/host/model-catalog.d.ts +3 -2
  47. package/lib/types/host/model-catalog.d.ts.map +1 -1
  48. package/lib/types/host/oauth-service.d.ts.map +1 -1
  49. package/lib/types/host/reasoning-collapse-guard/index.d.ts +26 -11
  50. package/lib/types/host/reasoning-collapse-guard/index.d.ts.map +1 -1
  51. package/lib/types/host/responses-client.d.ts +24 -0
  52. package/lib/types/host/responses-client.d.ts.map +1 -1
  53. package/lib/types/host/responses-mapper.d.ts +5 -1
  54. package/lib/types/host/responses-mapper.d.ts.map +1 -1
  55. package/lib/types/host/wire-auth.d.ts +14 -1
  56. package/lib/types/host/wire-auth.d.ts.map +1 -1
  57. package/lib/types/host/workbuddy/account-pool.d.ts +8 -8
  58. package/lib/types/host/workbuddy/account-pool.d.ts.map +1 -1
  59. package/lib/types/index.d.ts.map +1 -1
  60. package/lib/types/shared/claude-contracts.d.ts +19 -0
  61. package/lib/types/shared/claude-contracts.d.ts.map +1 -1
  62. package/lib/types/shared/kimi-code-contracts.d.ts +16 -0
  63. package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
  64. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -2,13 +2,13 @@ import { createRequire } from "node:module";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import * as LlmModule from "@deepseek-ai/dsh-llm";
4
4
  import { HarnessError, LlmAdapter, LlmError, LlmError as LlmError$1, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
+ import fs, { chmod, lstat, mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
5
6
  import path, { basename, dirname, join, relative, resolve } from "node:path";
6
7
  import os, { homedir } from "node:os";
7
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
9
  import { isDeepStrictEqual } from "node:util";
9
10
  import { execSync, spawn } from "node:child_process";
10
- import fsPromises, { chmod, lstat, mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
11
- import fs, { appendFileSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
11
+ import fs$1, { appendFileSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
12
12
  import http, { createServer } from "node:http";
13
13
  import { WebError } from "@deepseek-ai/dsh-web";
14
14
  import { lookup } from "node:dns/promises";
@@ -60,7 +60,23 @@ const OAUTH_CALLBACK_HOST = "localhost";
60
60
  const OAUTH_CALLBACK_PORT = 1455;
61
61
  const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}/auth/callback`;
62
62
  const OAUTH_SCOPE = "openid profile email offline_access";
63
- const OAUTH_ORIGINATOR = "opencode";
63
+ /**
64
+ * The single `originator` this line presents to the Codex backend on every
65
+ * request, and the one OAuth sends during sign-in.
66
+ *
67
+ * This used to be three different values - `opencode` for chat and sign-in,
68
+ * `pi` for the image and search endpoints - which was an archaeological record
69
+ * of when each endpoint was reverse-engineered, not a decision. The Codex
70
+ * backend keys behaviour off this value (`codex_cli_rs` is what the official
71
+ * CLI sends) and treats it as one client identity, so splitting it across three
72
+ * files meant three unrelated failure signatures for one account. Both spellings
73
+ * are known-good against the backend; `opencode` is kept because it is the value
74
+ * the OAuth flow has always presented, and changing the sign-in originator is a
75
+ * different risk from changing the request originator.
76
+ */
77
+ const CODEX_ORIGINATOR = "opencode";
78
+ /** OAuth presents the same identity the request path does (see above). */
79
+ const OAUTH_ORIGINATOR = CODEX_ORIGINATOR;
64
80
  const TOKEN_REFRESH_MARGIN_MS = 6e4;
65
81
  const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
66
82
  const PLUGIN_VERSION = "0.1.0-alpha.0";
@@ -70,9 +86,18 @@ const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
70
86
  const CODEX_IMAGE_GENERATION_URL = `${CODEX_API_BASE}/images/generations`;
71
87
  const CODEX_SEARCH_URL = `${CODEX_API_BASE}/alpha/search`;
72
88
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
89
+ const CODEX_MODELS_URL = `${CODEX_API_BASE}/models?client_version=0.99.0`;
90
+ /**
91
+ * Beta gate the Codex backend expects on every responses request.
92
+ *
93
+ * The subscription backend is served under a beta flag, not as stable API:
94
+ * without this header the endpoint is a different (and, on older builds,
95
+ * rejected) surface. Kept as a single constant so the value is reviewable in
96
+ * one place with the rest of the wire contract in `wire-auth.ts`.
97
+ */
98
+ const CODEX_OPENAI_BETA = "responses=experimental";
73
99
  const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
74
100
  const CODEX_RESET_CREDITS_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
75
- const CODEX_ORIGINATOR = "opencode";
76
101
  const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
77
102
  const CODEX_IMAGE_MODEL = "gpt-image-2";
78
103
  const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
@@ -227,6 +252,9 @@ function reasoningEffortsForModel(model) {
227
252
  if (profile === "gpt-6") return GPT_6_REASONING_EFFORTS;
228
253
  return profile === "gpt-5.6" ? GPT_56_REASONING_EFFORTS : STANDARD_REASONING_EFFORTS;
229
254
  }
255
+ function isCodexReasoningEffort(value) {
256
+ return typeof value === "string" && GPT_56_REASONING_EFFORTS.some((effort) => effort === value);
257
+ }
230
258
  function isCodexModelId(model) {
231
259
  return typeof model === "string" && CODEX_MODEL_CATALOG.some((entry) => entry.id === model);
232
260
  }
@@ -270,28 +298,40 @@ function resolveCodexFallbackModel(model) {
270
298
  //#region src/host/model-catalog.ts
271
299
  const PROVIDER_ID$6 = CODEX_CHATGPT_PROVIDER_ID;
272
300
  const PROVIDER_NAME$6 = "Codex(ChatGPT 订阅)";
273
- function listCodexModels(preferences) {
301
+ function listCodexModels(preferences, live) {
274
302
  const status = preferences?.status();
275
303
  if (status?.enabled === false) return [];
276
- const visible = new Set(status?.visibleModelIds ?? CODEX_MODEL_CATALOG.map((entry) => entry.id));
277
- return CODEX_MODEL_CATALOG.filter((entry) => visible.has(entry.id)).map((entry) => ({
304
+ const source = live !== void 0 && live.length > 0 ? live.map((entry) => ({
305
+ id: entry.id,
306
+ name: entry.name,
307
+ inputModalities: entry.inputModalities
308
+ })) : CODEX_MODEL_CATALOG.map((entry) => ({
309
+ id: entry.id,
310
+ name: entry.name,
311
+ inputModalities: entry.inputModalities
312
+ }));
313
+ const visible = new Set(status?.visibleModelIds ?? source.map((entry) => entry.id));
314
+ return source.filter((entry) => visible.has(entry.id)).map((entry) => ({
278
315
  provider: PROVIDER_ID$6,
279
316
  id: entry.id,
280
317
  name: entry.name,
281
318
  inputModalities: [...entry.inputModalities]
282
319
  }));
283
320
  }
284
- function resolveCodexModel(model, preferences) {
321
+ function resolveCodexModel(model, preferences, live) {
285
322
  const entry = resolveCodexCatalogEntry(model);
323
+ const liveEntry = live?.find((candidate) => candidate.id === model);
286
324
  const configuredContextWindow = (preferences?.status())?.contextWindowOverrides[model];
287
- const efforts = reasoningEffortsForModel(model);
288
- const defaultEffort = efforts.includes(entry.defaultReasoningEffort) ? entry.defaultReasoningEffort : efforts[0];
325
+ const listedEfforts = liveEntry?.reasoningEfforts?.filter(isCodexReasoningEffort);
326
+ const efforts = listedEfforts !== void 0 && listedEfforts.length > 0 ? listedEfforts : reasoningEffortsForModel(model);
327
+ const statedDefault = liveEntry?.defaultReasoningEffort ?? entry.defaultReasoningEffort;
328
+ const defaultEffort = isCodexReasoningEffort(statedDefault) && efforts.includes(statedDefault) ? statedDefault : efforts[0];
289
329
  return {
290
330
  provider: PROVIDER_ID$6,
291
331
  id: model,
292
- name: entry.id === model ? entry.name : model,
293
- inputModalities: [...entry.inputModalities],
294
- context: { contextWindow: configuredContextWindow ?? entry.contextWindow },
332
+ name: liveEntry?.name ?? (entry.id === model ? entry.name : model),
333
+ inputModalities: [...liveEntry?.inputModalities ?? entry.inputModalities],
334
+ context: { contextWindow: configuredContextWindow ?? liveEntry?.contextWindow ?? entry.contextWindow },
295
335
  defaultMaxTokens: codexModelMaxTokens(model),
296
336
  reasoning: {
297
337
  efforts: efforts.map((effort) => ({
@@ -324,10 +364,12 @@ const RETRY_POLICY$5 = resolveRetryPolicy({
324
364
  var CodexChatGptAdapter = class extends LlmAdapter {
325
365
  client;
326
366
  preferences;
327
- constructor(client, preferences) {
367
+ loadCatalog;
368
+ constructor(client, preferences, loadCatalog) {
328
369
  super();
329
370
  this.client = client;
330
371
  this.preferences = preferences;
372
+ this.loadCatalog = loadCatalog;
331
373
  }
332
374
  providerInfo(provider) {
333
375
  return {
@@ -340,10 +382,21 @@ var CodexChatGptAdapter = class extends LlmAdapter {
340
382
  }
341
383
  imageRequestPricing(_provider, _model) {}
342
384
  async listModels() {
343
- return listCodexModels(this.preferences);
385
+ return listCodexModels(this.preferences, await this.catalog());
344
386
  }
345
387
  async resolveModel(_provider, model, signal) {
346
- return resolveCodexModel(model, this.preferences);
388
+ return resolveCodexModel(model, this.preferences, await this.catalog(signal));
389
+ }
390
+ /**
391
+ * The live listing, or undefined when this line has no way to fetch one.
392
+ *
393
+ * Never throws: a listing failure must not become a failed picker or a
394
+ * rejected model resolution. `loadCodexCatalog` already answers with the
395
+ * shipped table on failure, so the catch here only covers a throwing loader.
396
+ */
397
+ async catalog(signal) {
398
+ if (this.loadCatalog === void 0) return void 0;
399
+ return this.loadCatalog({ signal }).catch(() => void 0);
347
400
  }
348
401
  async prepareCall(provider, model, signal) {
349
402
  return {
@@ -367,6 +420,347 @@ function dshHomeDir() {
367
420
  return process.env.DSH_HOME?.trim() || path.join(os.homedir(), ".dsh");
368
421
  }
369
422
  //#endregion
423
+ //#region src/host/common/catalog-snapshot.ts
424
+ /**
425
+ * Persistent snapshot for one provider's live model catalog.
426
+ *
427
+ * The in-memory catalog caches keep a process alive between requests, but they
428
+ * start cold: on the first call after a restart (or a plugin reinstall) the
429
+ * picker and the settings page both wait on the upstream listing before they
430
+ * can render anything, and an unreachable endpoint turns that wait into the
431
+ * full discovery timeout — "the model server is unavailable" on a fresh
432
+ * install. The catalog is not fast-moving data, so the last successful listing
433
+ * is persisted to `<DSH_HOME>/storages/` and rehydrated into the in-memory
434
+ * cache on first use. A snapshot the caller can read is therefore available
435
+ * from the very first call, with no network in the path.
436
+ *
437
+ * Writes are best-effort: persistence is an optimization, never a capability.
438
+ * A read-only home or a locked file must not fail the fetch that produced the
439
+ * snapshot.
440
+ */
441
+ /** File name of the persisted snapshot for one catalog. */
442
+ function catalogSnapshotPath(name) {
443
+ return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
444
+ }
445
+ /**
446
+ * Snapshot name for one catalog *and* one credential scope.
447
+ *
448
+ * A persisted snapshot answers before any credential is read — that is the whole
449
+ * point of it — so the file itself has to carry the scope it was fetched under.
450
+ * Otherwise the first call after a restart, for any account, is answered by
451
+ * whatever listing some other scope happened to persist last: a Kimi listing
452
+ * fetched for the mainland-cn endpoint would answer a global caller with models
453
+ * and context windows that region does not serve, and a Command Code listing
454
+ * fetched against one API environment would answer the other.
455
+ *
456
+ * The scope is part of the file name rather than of the payload because a caller
457
+ * that cannot name the account — the harness resolves this catalog with no token
458
+ * at all — still has to find the snapshot its own scope owns.
459
+ *
460
+ * Everything outside `[a-z0-9]` is folded to `_`, so no scope can escape the
461
+ * `storages` directory.
462
+ */
463
+ function catalogSnapshotName(base, scope) {
464
+ const normalized = scope.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
465
+ return normalized === "" ? base : `${base}-${normalized}`;
466
+ }
467
+ /**
468
+ * Read the persisted snapshot for one catalog.
469
+ *
470
+ * Shape errors and unreadable files both return `undefined` — the caller keeps
471
+ * its in-memory state, and the next successful fetch overwrites the file.
472
+ */
473
+ async function readCatalogSnapshot(name, parseModels) {
474
+ try {
475
+ const content = await fs.readFile(catalogSnapshotPath(name), "utf8");
476
+ const parsed = JSON.parse(content);
477
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
478
+ const record = parsed;
479
+ const fetchedAt = record.fetchedAt;
480
+ if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
481
+ const models = parseModels(record.models);
482
+ if (models === void 0 || models.length === 0) return void 0;
483
+ return {
484
+ fetchedAt,
485
+ models
486
+ };
487
+ } catch {
488
+ return;
489
+ }
490
+ }
491
+ /**
492
+ * Snapshot writes still in flight.
493
+ *
494
+ * Callers fire-and-forget these writes, so nothing in production ever awaits
495
+ * one. A test that deletes its private `storages` directory between cases
496
+ * cannot know when that is safe, and a write that lands after the delete
497
+ * recreates the file the next case asserts is absent — which is how a stale
498
+ * listing leaked into a later test and failed it on CI. Registering each write
499
+ * gives {@link flushCatalogSnapshots} a deterministic point to wait on.
500
+ */
501
+ const pendingSnapshotWrites = /* @__PURE__ */ new Set();
502
+ /** Persist one snapshot; a failure is logged nowhere and never thrown. */
503
+ function writeCatalogSnapshot(name, models, fetchedAt) {
504
+ const write = (async () => {
505
+ try {
506
+ const filePath = catalogSnapshotPath(name);
507
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
508
+ const tmp = `${filePath}.tmp.${Date.now()}`;
509
+ await fs.writeFile(tmp, JSON.stringify({
510
+ fetchedAt,
511
+ models
512
+ }, null, 2), "utf8");
513
+ await fs.rename(tmp, filePath);
514
+ } catch {}
515
+ })();
516
+ pendingSnapshotWrites.add(write);
517
+ write.finally(() => {
518
+ pendingSnapshotWrites.delete(write);
519
+ });
520
+ return write;
521
+ }
522
+ /**
523
+ * Warm an in-memory cache from the persisted snapshot when it is fresher than
524
+ * what the cache currently holds. Called lazily on the first cache miss of a
525
+ * process, so a restart serves the previous listing without waiting on the
526
+ * network.
527
+ *
528
+ * @returns the snapshot's `fetchedAt`, or `0` when no usable snapshot exists —
529
+ * the caller then fetches as before.
530
+ */
531
+ async function rehydrateCatalogCache(name, parseModels, accept, currentFetchedAt) {
532
+ const snapshot = await readCatalogSnapshot(name, parseModels);
533
+ if (snapshot === void 0 || snapshot.fetchedAt <= currentFetchedAt) return 0;
534
+ accept(snapshot.fetchedAt, snapshot.models);
535
+ return snapshot.fetchedAt;
536
+ }
537
+ //#endregion
538
+ //#region src/host/wire-auth.ts
539
+ /**
540
+ * Turn-state header the Codex backend hands back on a responses response.
541
+ *
542
+ * The official CLI reads it and echoes it on the next request of the same
543
+ * conversation; the backend uses it to resume instead of re-ingesting the
544
+ * whole history. It is opaque, short-lived and account-scoped, so it is
545
+ * threaded through rather than computed (see `responses-client.ts`).
546
+ */
547
+ const CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
548
+ function codexHeaders(credentials, sessionId, options = {}) {
549
+ const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
550
+ return {
551
+ authorization: `Bearer ${credentials.accessToken}`,
552
+ ...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
553
+ "openai-beta": CODEX_OPENAI_BETA,
554
+ originator: CODEX_ORIGINATOR,
555
+ "user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
556
+ ...sessionId ? { "session-id": sessionId } : {},
557
+ ...options.turnState ? { [CODEX_TURN_STATE_HEADER]: options.turnState } : {}
558
+ };
559
+ }
560
+ function stableSessionId(value) {
561
+ const source = value === void 0 || value === "" ? randomUUID() : value;
562
+ return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
563
+ }
564
+ function retryAfterMs$1(headers) {
565
+ const raw = headers.get("retry-after");
566
+ if (raw === null) return void 0;
567
+ const seconds = Number(raw);
568
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
569
+ const timestamp = Date.parse(raw);
570
+ if (!Number.isFinite(timestamp)) return void 0;
571
+ return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
572
+ }
573
+ //#endregion
574
+ //#region src/host/codex-catalog.ts
575
+ /** How long a live listing is reused before the server is asked again. */
576
+ const CATALOG_TTL_MS = 15 * 6e4;
577
+ /**
578
+ * Snapshot scope.
579
+ *
580
+ * The Codex listing is account-scoped (each plan sees its own models), so the
581
+ * persisted snapshot is filed under the account it was fetched for. Without the
582
+ * scope a rehydrated listing from one account would answer another's picker.
583
+ */
584
+ const SNAPSHOT_SCOPE = "chatgpt-codex";
585
+ let catalogCache$2 = null;
586
+ const catalogInFlight$3 = /* @__PURE__ */ new Map();
587
+ let snapshotRehydrated = null;
588
+ /**
589
+ * The listing currently held, or undefined before the first load.
590
+ *
591
+ * Callers that need a catalog with no credential read the shipped table
592
+ * directly; returning it here under a different entry type would force every
593
+ * one of them to convert it.
594
+ */
595
+ function getCachedCatalog() {
596
+ return catalogCache$2?.models;
597
+ }
598
+ /**
599
+ * Read the models this subscription can call.
600
+ *
601
+ * The shipped table in `shared/model-catalog.ts` stays the floor: it is what
602
+ * answers a picker before the first sign-in, and what a failed listing falls
603
+ * back to. The subscription listing is the authority once one is available,
604
+ * because only the backend knows which models a plan currently serves and at
605
+ * what context window — the reason subscription-only models exist at all.
606
+ *
607
+ * Single-flighted per account, because the harness resolves every model of every
608
+ * provider when it builds the picker; without that, one picker build is one
609
+ * round trip per model.
610
+ */
611
+ function loadCodexCatalog(credentials, options = {}) {
612
+ const key = accountKeyFor$1(credentials);
613
+ if (options.force !== true) {
614
+ const cached = catalogCache$2;
615
+ if (cached !== null && cached.key === key && Date.now() - cached.at < CATALOG_TTL_MS) return Promise.resolve(cached.models);
616
+ }
617
+ const pending = catalogInFlight$3.get(key);
618
+ if (pending !== void 0) return pending;
619
+ const run = performCatalogLoad$1(credentials, options).then((models) => {
620
+ catalogInFlight$3.delete(key);
621
+ return models;
622
+ }, (error) => {
623
+ catalogInFlight$3.delete(key);
624
+ throw error;
625
+ });
626
+ catalogInFlight$3.set(key, run);
627
+ return run;
628
+ }
629
+ async function performCatalogLoad$1(credentials, options) {
630
+ const key = accountKeyFor$1(credentials);
631
+ const fetchFn = options.fetchFn ?? fetch;
632
+ snapshotRehydrated ??= rehydrateSnapshot(key);
633
+ await snapshotRehydrated;
634
+ const cached = catalogCache$2;
635
+ if (cached !== null && cached.key === key && Date.now() - cached.at < CATALOG_TTL_MS) return cached.models;
636
+ let models;
637
+ let live = false;
638
+ try {
639
+ const response = await fetchFn(CODEX_MODELS_URL, {
640
+ method: "GET",
641
+ headers: {
642
+ ...codexHeaders(credentials),
643
+ accept: "application/json"
644
+ },
645
+ signal: options.signal
646
+ });
647
+ if (!response.ok) throw new Error(`Codex model listing failed (${response.status}).`);
648
+ const listing = parseListing(await response.json().catch(() => void 0));
649
+ if (listing.length === 0) throw new Error("Codex model listing named no models this line understands.");
650
+ models = listing;
651
+ live = true;
652
+ writeCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), [...models], Date.now());
653
+ } catch {
654
+ models = getCachedCatalog() ?? [];
655
+ live = false;
656
+ }
657
+ catalogCache$2 = {
658
+ at: Date.now(),
659
+ models,
660
+ key,
661
+ live
662
+ };
663
+ return models;
664
+ }
665
+ /**
666
+ * Seed the in-memory cache from the last persisted listing.
667
+ *
668
+ * Best effort by construction: a missing or malformed snapshot leaves the cache
669
+ * cold, and the caller then simply makes the network call.
670
+ */
671
+ async function rehydrateSnapshot(key) {
672
+ const snapshot = await readCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), (value) => Array.isArray(value) ? value.flatMap((item) => parseListingEntry$1(item)) : void 0).catch(() => void 0);
673
+ if (snapshot === void 0 || snapshot.models.length === 0) return;
674
+ if (catalogCache$2 !== null) return;
675
+ catalogCache$2 = {
676
+ at: snapshot.fetchedAt,
677
+ models: snapshot.models,
678
+ key,
679
+ live: true
680
+ };
681
+ }
682
+ /**
683
+ * Read the listing payload.
684
+ *
685
+ * The documented shape is `{ models: [...] }`, but a bare array is accepted too
686
+ * so a backend that flattens the envelope does not read as an empty catalog.
687
+ */
688
+ function parseListing(payload) {
689
+ const record = asRecord$10(payload);
690
+ const models = Array.isArray(payload) ? payload : record?.models;
691
+ if (!Array.isArray(models)) return [];
692
+ return models.flatMap((item) => parseListingEntry$1(item));
693
+ }
694
+ /**
695
+ * One listing entry.
696
+ *
697
+ * Only fields the backend actually documents are read. A model id this line has
698
+ * never heard of still produces an entry, because the listing is the authority
699
+ * on what the account may call — but every capability it does not state falls
700
+ * back to a conservative answer rather than to a guess.
701
+ */
702
+ function parseListingEntry$1(value) {
703
+ const record = asRecord$10(value);
704
+ if (record === void 0) return [];
705
+ const id = asString$14(record.slug) ?? asString$14(record.id);
706
+ if (id === void 0) return [];
707
+ const contextWindow = asNumber$4(record.context_window) ?? asNumber$4(record.contextWindow);
708
+ const modalities = parseModalities(record.input_modalities);
709
+ const efforts = parseEfforts(record.supported_reasoning_levels);
710
+ const fallback = CODEX_MODEL_CATALOG.find((entry) => entry.id === id);
711
+ return [{
712
+ id,
713
+ name: asString$14(record.display_name) ?? asString$14(record.displayName) ?? fallback?.name ?? id,
714
+ contextWindow: contextWindow !== void 0 && contextWindow > 0 ? contextWindow : null,
715
+ inputModalities: modalities ?? (fallback ? [...fallback.inputModalities] : ["text"]),
716
+ ...efforts !== null ? { reasoningEfforts: efforts } : {},
717
+ defaultReasoningEffort: asString$14(record.default_reasoning_level) ?? fallback?.defaultReasoningEffort ?? null
718
+ }];
719
+ }
720
+ function parseModalities(value) {
721
+ if (!Array.isArray(value)) return null;
722
+ const out = [];
723
+ for (const item of value) {
724
+ const name = asString$14(item);
725
+ if (name === "text" || name === "image") out.push(name);
726
+ }
727
+ return out.length > 0 ? out : null;
728
+ }
729
+ /**
730
+ * Reasoning levels the listing states, in the order it stated them.
731
+ *
732
+ * The backend writes each level as `{ "effort": "high" }`; a bare string is
733
+ * accepted too, since that is the other spelling seen in the wild.
734
+ */
735
+ function parseEfforts(value) {
736
+ if (!Array.isArray(value)) return null;
737
+ const out = [];
738
+ for (const item of value) {
739
+ const effort = asString$14(item) ?? asString$14(asRecord$10(item)?.effort);
740
+ if (effort !== void 0 && !out.includes(effort)) out.push(effort);
741
+ }
742
+ return out.length > 0 ? out : null;
743
+ }
744
+ /**
745
+ * Cache identity for one credential.
746
+ *
747
+ * The account id is the entitlement boundary: two accounts of the same user can
748
+ * see different listings, and a pool rotates between them, so the key has to
749
+ * follow the account rather than the person.
750
+ */
751
+ function accountKeyFor$1(credentials) {
752
+ return credentials.accountId || "default";
753
+ }
754
+ function asRecord$10(value) {
755
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
756
+ }
757
+ function asString$14(value) {
758
+ return typeof value === "string" && value !== "" ? value : void 0;
759
+ }
760
+ function asNumber$4(value) {
761
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
762
+ }
763
+ //#endregion
370
764
  //#region src/host/common/credential-read-cache.ts
371
765
  /**
372
766
  * Identity a file-backed credential store reports when nothing is stored.
@@ -905,6 +1299,16 @@ function poolCredentialAccount$1(filePath) {
905
1299
  return createHash("sha256").update(path.resolve(filePath)).digest("hex");
906
1300
  }
907
1301
  const poolOperations = /* @__PURE__ */ new Map();
1302
+ const poolIdentityRevisions = /* @__PURE__ */ new Map();
1303
+ const poolRefreshes = /* @__PURE__ */ new Map();
1304
+ /**
1305
+ * A selected row was deleted, or became unusable, while a caller awaited it.
1306
+ *
1307
+ * Distinct from every other credential error because a MODEL REQUEST can still
1308
+ * be served by another account: routing excludes the row and tries the next
1309
+ * one, while a caller that named the account still gets the plain refusal.
1310
+ */
1311
+ var PoolAccountUnavailableError = class extends LlmError {};
908
1312
  /**
909
1313
  * The shared multi-account pool: encrypted storage, eligibility filtering,
910
1314
  * rotation strategy, 429 cooldowns and per-account auth failures.
@@ -926,11 +1330,14 @@ var AccountPoolCore = class {
926
1330
  * can therefore invalidate on identity change without paying a credential read
927
1331
  * to discover it.
928
1332
  */
929
- identityRevision = 0;
1333
+ /** Pool-file identity of this instance: locks, revisions and flights key on it. */
1334
+ operationKey;
930
1335
  constructor(hooks) {
931
1336
  this.hooks = hooks;
932
1337
  this.filePath = hooks.poolFile;
933
1338
  this.backend = hooks.backend ?? createPoolBackend(hooks);
1339
+ const resolved = path.resolve(this.filePath);
1340
+ this.operationKey = process.platform === "win32" ? resolved.toLowerCase() : resolved;
934
1341
  }
935
1342
  /**
936
1343
  * A cheap, in-memory revision of the pool's serving identity.
@@ -946,15 +1353,21 @@ var AccountPoolCore = class {
946
1353
  * would report one account's usage under another account's name.
947
1354
  */
948
1355
  currentIdentityRevision() {
949
- return this.identityRevision;
1356
+ return poolIdentityRevisions.get(this.operationKey) ?? 0;
950
1357
  }
951
1358
  /** Human description of where this pool's credentials live. */
952
1359
  path() {
953
1360
  if (process.platform === "win32") return `${this.filePath}.dpapi`;
954
1361
  return `${process.platform === "darwin" ? "Keychain" : "Secret Service"}: ${this.hooks.keychainService}/${poolCredentialAccount$1(this.filePath)}`;
955
1362
  }
1363
+ /**
1364
+ * Queue one storage transaction for this pool file.
1365
+ *
1366
+ * NOT reentrant: a transaction must use {@link loadPoolData} and
1367
+ * {@link updatePool} rather than calling the public read/write methods.
1368
+ */
956
1369
  serialize(operation) {
957
- const key = path.resolve(this.filePath);
1370
+ const key = this.operationKey;
958
1371
  const result = (poolOperations.get(key) || Promise.resolve()).then(operation);
959
1372
  const settled = result.then(() => void 0, () => void 0);
960
1373
  poolOperations.set(key, settled);
@@ -978,31 +1391,74 @@ var AccountPoolCore = class {
978
1391
  * getter write to disk.
979
1392
  */
980
1393
  read() {
981
- return this.serialize(async () => {
982
- let current = null;
983
- try {
984
- current = await this.backend.load();
985
- } catch {}
986
- if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
987
- try {
988
- const legacy = await this.hooks.legacyAccount?.();
989
- if (legacy) return {
990
- version: 1,
991
- activeAccountId: legacy.id,
992
- rotationStrategy: current?.rotationStrategy || "sequential",
993
- accounts: [legacy]
994
- };
995
- } catch {}
996
- return current || {
1394
+ return this.serialize(() => this.loadPoolData());
1395
+ }
1396
+ /**
1397
+ * Load the pool without holding the file lock.
1398
+ *
1399
+ * Providers override this to maintain their own in-memory indexes, so every
1400
+ * load — including one inside {@link updatePool} — refreshes them.
1401
+ */
1402
+ async loadPoolData() {
1403
+ let current = null;
1404
+ try {
1405
+ current = await this.backend.load();
1406
+ } catch {}
1407
+ if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
1408
+ try {
1409
+ const legacy = await this.hooks.legacyAccount?.();
1410
+ if (legacy) return {
997
1411
  version: 1,
998
- rotationStrategy: "sequential",
999
- accounts: []
1412
+ activeAccountId: legacy.id,
1413
+ rotationStrategy: current?.rotationStrategy || "sequential",
1414
+ accounts: [legacy]
1000
1415
  };
1001
- });
1416
+ } catch {}
1417
+ return current || {
1418
+ version: 1,
1419
+ rotationStrategy: "sequential",
1420
+ accounts: []
1421
+ };
1422
+ }
1423
+ async persistPoolData(data) {
1424
+ poolIdentityRevisions.set(this.operationKey, this.currentIdentityRevision() + 1);
1425
+ await this.saveVerified(data);
1002
1426
  }
1427
+ /**
1428
+ * Replace the whole document (initialization, import), NOT a read-modify-write.
1429
+ * Prefer {@link updatePool} for mutations so the draft is read under the lock.
1430
+ */
1003
1431
  write(data) {
1004
- this.identityRevision += 1;
1005
- return this.serialize(() => this.saveVerified(this.hooks.parsePoolData(data)));
1432
+ return this.serialize(() => this.persistPoolData(data));
1433
+ }
1434
+ /**
1435
+ * Read the LATEST document, mutate it and persist it as one file transaction.
1436
+ *
1437
+ * This is the whole fix for the read-modify-write race: the old shape read a
1438
+ * snapshot, awaited (a token refresh, a settings write, an encrypted round
1439
+ * trip) and then wrote that whole stale snapshot back, erasing whatever landed
1440
+ * in between — including another account's freshly rotated token.
1441
+ *
1442
+ * The callback runs under the file lock, so it must not call read, write or
1443
+ * updatePool (the lock is not reentrant) and must not do network I/O.
1444
+ * Provider-local stores belong in `afterCommit`, which runs only once the
1445
+ * pool write is verified, and must not re-enter the pool either.
1446
+ */
1447
+ updatePool(mutate, afterCommit) {
1448
+ return this.serialize(async () => {
1449
+ const data = await this.loadPoolData();
1450
+ const before = structuredClone(data);
1451
+ const result = await mutate(data);
1452
+ if (!isDeepStrictEqual(before, data)) {
1453
+ await this.persistPoolData(data);
1454
+ await afterCommit?.(data, result).catch(() => void 0);
1455
+ for (const account of data.accounts) if (!isDeepStrictEqual(before.accounts.find((entry) => entry.id === account.id)?.credentials, account.credentials)) await this.hooks.credentialsCommitted?.(account).catch(() => void 0);
1456
+ const previousPrimary = before.accounts.find((account) => account.isPrimary);
1457
+ const primary = data.accounts.find((account) => account.isPrimary);
1458
+ if (previousPrimary?.id !== primary?.id || !isDeepStrictEqual(previousPrimary?.credentials, primary?.credentials)) await this.hooks.mirrorPrimary?.(primary?.credentials ?? null).catch(() => void 0);
1459
+ }
1460
+ return result;
1461
+ });
1006
1462
  }
1007
1463
  /** Public summaries, with cooldowns that already expired reported as absent. */
1008
1464
  async listAccounts() {
@@ -1031,8 +1487,17 @@ var AccountPoolCore = class {
1031
1487
  return this.hooks.extendSummary ? this.hooks.extendSummary(account, summary, now) : summary;
1032
1488
  }
1033
1489
  /** Add or re-authorize one account; an existing dedupe key updates in place. */
1034
- async addAccount(credentials, alias) {
1035
- const data = await this.read();
1490
+ addAccount(credentials, alias) {
1491
+ return this.updatePool((data) => this.addAccountToPool(data, credentials, alias));
1492
+ }
1493
+ /**
1494
+ * Add or re-authorize inside an existing transaction.
1495
+ *
1496
+ * Capacity, dedupe and the primary decision are all taken against the live
1497
+ * document, so two simultaneous sign-ins cannot each pass a check the other
1498
+ * has already invalidated.
1499
+ */
1500
+ addAccountToPool(data, credentials, alias) {
1036
1501
  const key = this.hooks.dedupeKey?.(credentials);
1037
1502
  const existingIndex = key === void 0 ? -1 : data.accounts.findIndex((account) => this.hooks.dedupeKey?.(account.credentials) === key);
1038
1503
  const maxAccounts = this.hooks.maxAccounts ?? 20;
@@ -1058,77 +1523,79 @@ var AccountPoolCore = class {
1058
1523
  if (existingIndex >= 0) data.accounts[existingIndex] = account;
1059
1524
  else data.accounts.push(account);
1060
1525
  if (!data.activeAccountId || account.isPrimary) data.activeAccountId = account.id;
1061
- await this.write(data);
1062
- if (account.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
1063
1526
  return account;
1064
1527
  }
1065
- async setPrimary(accountId) {
1066
- const data = await this.read();
1067
- for (const account of data.accounts) account.isPrimary = account.id === accountId;
1068
- const primary = data.accounts.find((account) => account.isPrimary);
1069
- if (primary) {
1070
- data.activeAccountId = primary.id;
1071
- this.hooks.mirrorPrimary?.(primary.credentials).catch(() => void 0);
1072
- }
1073
- await this.write(data);
1528
+ setPrimary(accountId) {
1529
+ return this.updatePool((data) => {
1530
+ for (const account of data.accounts) account.isPrimary = account.id === accountId;
1531
+ const primary = data.accounts.find((account) => account.isPrimary);
1532
+ if (primary) data.activeAccountId = primary.id;
1533
+ });
1074
1534
  }
1075
1535
  /**
1076
1536
  * Persist a refreshed credential for one account.
1077
1537
  *
1538
+ * `expected` makes this a conditional write: when the stored credential is
1539
+ * no longer the one the caller started from, something newer won the race (a
1540
+ * re-login, a rotation committed while this call was in flight) and the
1541
+ * latest row is returned WITHOUT being overwritten. A deleted account is
1542
+ * never brought back.
1543
+ *
1078
1544
  * Used by a forced refresh (a 401 mid-request) rather than by the rotating
1079
- * getEffectiveAccount path, which already writes back what it refreshed.
1545
+ * getEffectiveAccount path, which refreshes through its own single-flight.
1080
1546
  */
1081
- async updateAccountCredentials(accountId, credentials) {
1082
- const data = await this.read();
1083
- const target = data.accounts.find((account) => account.id === accountId);
1084
- if (target === void 0) return void 0;
1085
- target.credentials = credentials;
1086
- await this.write(data);
1087
- if (target.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
1088
- return target;
1547
+ updateAccountCredentials(accountId, credentials, expected) {
1548
+ return this.updatePool((data) => {
1549
+ const target = data.accounts.find((account) => account.id === accountId);
1550
+ if (target && (expected === void 0 || isDeepStrictEqual(target.credentials, expected))) target.credentials = credentials;
1551
+ return target;
1552
+ });
1089
1553
  }
1090
- async setAlias(accountId, alias) {
1091
- const data = await this.read();
1092
- const target = data.accounts.find((account) => account.id === accountId);
1093
- if (target && alias.trim()) {
1094
- target.alias = alias.trim();
1095
- await this.write(data);
1096
- }
1554
+ setAlias(accountId, alias) {
1555
+ return this.updatePool((data) => {
1556
+ const target = data.accounts.find((account) => account.id === accountId);
1557
+ if (target && alias.trim()) target.alias = alias.trim();
1558
+ });
1097
1559
  }
1098
- async deleteAccount(accountId) {
1099
- const data = await this.read();
1560
+ deleteAccount(accountId) {
1561
+ return this.updatePool((data) => this.deleteAccountFromPool(data, accountId));
1562
+ }
1563
+ /**
1564
+ * Remove one account inside a transaction.
1565
+ *
1566
+ * Provider refusal rules (a snapshot this plugin does not own, a desktop
1567
+ * account it may only hide) belong in an override of this, so the check and
1568
+ * the removal cannot be separated by a concurrent change.
1569
+ */
1570
+ deleteAccountFromPool(data, accountId) {
1100
1571
  const wasPrimary = data.accounts.find((account) => account.id === accountId)?.isPrimary;
1101
1572
  data.accounts = data.accounts.filter((account) => account.id !== accountId);
1102
- if (wasPrimary && data.accounts.length > 0) {
1103
- data.accounts[0].isPrimary = true;
1104
- this.hooks.mirrorPrimary?.(data.accounts[0].credentials).catch(() => void 0);
1105
- } else if (data.accounts.length === 0) this.hooks.mirrorPrimary?.(null).catch(() => void 0);
1573
+ if (wasPrimary && data.accounts.length > 0) data.accounts[0].isPrimary = true;
1106
1574
  if (data.activeAccountId === accountId) data.activeAccountId = data.accounts.find((account) => account.isPrimary)?.id ?? data.accounts[0]?.id;
1107
- await this.write(data);
1108
1575
  }
1109
- async setStrategy(strategy) {
1110
- const data = await this.read();
1111
- data.rotationStrategy = strategy;
1112
- await this.write(data);
1576
+ setStrategy(strategy) {
1577
+ return this.updatePool((data) => {
1578
+ data.rotationStrategy = strategy;
1579
+ });
1113
1580
  }
1114
1581
  /** Cool one account down after an upstream 429. */
1115
- async markCooldown(accountId, durationMs, reason) {
1116
- const data = await this.read();
1117
- const target = data.accounts.find((account) => account.id === accountId);
1118
- if (target) {
1119
- target.cooldownUntil = Date.now() + Math.max(MIN_COOLDOWN_MS, durationMs);
1120
- target.cooldownReason = reason;
1121
- await this.write(data);
1122
- }
1582
+ markCooldown(accountId, durationMs, reason) {
1583
+ return this.updatePool((data) => {
1584
+ const target = data.accounts.find((account) => account.id === accountId);
1585
+ if (target) {
1586
+ target.cooldownUntil = Date.now() + Math.max(MIN_COOLDOWN_MS, durationMs);
1587
+ target.cooldownReason = reason;
1588
+ }
1589
+ });
1123
1590
  }
1124
- async clearCooldown(accountId) {
1125
- const data = await this.read();
1126
- const target = data.accounts.find((account) => account.id === accountId);
1127
- if (target) {
1128
- target.cooldownUntil = void 0;
1129
- target.cooldownReason = void 0;
1130
- await this.write(data);
1131
- }
1591
+ clearCooldown(accountId) {
1592
+ return this.updatePool((data) => {
1593
+ const target = data.accounts.find((account) => account.id === accountId);
1594
+ if (target) {
1595
+ target.cooldownUntil = void 0;
1596
+ target.cooldownReason = void 0;
1597
+ }
1598
+ });
1132
1599
  }
1133
1600
  /**
1134
1601
  * Record that an account can no longer authenticate.
@@ -1136,23 +1603,23 @@ var AccountPoolCore = class {
1136
1603
  * The account stays in the pool: signing in again is what restores it, and a
1137
1604
  * deleted account would take the user's alias and ordering with it.
1138
1605
  */
1139
- async markAuthFailed(accountId, reason, status = "expired") {
1140
- const data = await this.read();
1141
- const target = data.accounts.find((account) => account.id === accountId);
1142
- if (target) {
1143
- target.authStatus = status;
1144
- target.authFailedReason = reason;
1145
- await this.write(data);
1146
- }
1606
+ markAuthFailed(accountId, reason, status = "expired") {
1607
+ return this.updatePool((data) => {
1608
+ const target = data.accounts.find((account) => account.id === accountId);
1609
+ if (target) {
1610
+ target.authStatus = status;
1611
+ target.authFailedReason = reason;
1612
+ }
1613
+ });
1147
1614
  }
1148
- async clearAuthFailed(accountId) {
1149
- const data = await this.read();
1150
- const target = data.accounts.find((account) => account.id === accountId);
1151
- if (target) {
1152
- target.authStatus = void 0;
1153
- target.authFailedReason = void 0;
1154
- await this.write(data);
1155
- }
1615
+ clearAuthFailed(accountId) {
1616
+ return this.updatePool((data) => {
1617
+ const target = data.accounts.find((account) => account.id === accountId);
1618
+ if (target) {
1619
+ target.authStatus = void 0;
1620
+ target.authFailedReason = void 0;
1621
+ }
1622
+ });
1156
1623
  }
1157
1624
  /** Whether an account may serve a request right now. */
1158
1625
  isEligible(account, now, triedAccountIds) {
@@ -1192,24 +1659,16 @@ var AccountPoolCore = class {
1192
1659
  *
1193
1660
  * @param fetchFn - fetch used if the credential is close to expiry.
1194
1661
  */
1195
- async getCredentialAccount(fetchFn = fetch) {
1662
+ async getCredentialAccount(fetchFn = fetch, forceRefresh = false) {
1196
1663
  const data = await this.read();
1197
1664
  if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
1198
- const now = Date.now();
1199
1665
  const usable = data.accounts.filter((account) => this.isCredentialUsable(account));
1200
1666
  if (usable.length === 0) throw new LlmError(`全部 ${data.accounts.length} 个 ${this.hooks.displayName} 账号均需要重新登录。`, "AUTH", { status: 401 });
1201
1667
  const selected = this.selectAccount(usable, data);
1202
- if (this.shouldRefreshCredential(selected, now) && this.hooks.refresh) {
1203
- const refreshed = await this.hooks.refresh(selected.credentials, fetchFn);
1204
- await this.updateAccountCredentials(selected.id, refreshed);
1205
- return {
1206
- account: selected,
1207
- credentials: refreshed
1208
- };
1209
- }
1668
+ const account = await this.freshAccount(selected, fetchFn, forceRefresh);
1210
1669
  return {
1211
- account: selected,
1212
- credentials: selected.credentials
1670
+ account,
1671
+ credentials: account.credentials
1213
1672
  };
1214
1673
  }
1215
1674
  /**
@@ -1244,8 +1703,8 @@ var AccountPoolCore = class {
1244
1703
  * looks unexpired, and only the service's own refusal says so. Renewing it is
1245
1704
  * what separates that from a credential that genuinely needs a new sign-in.
1246
1705
  */
1247
- async renewCredential(accountId, fetchFn = fetch) {
1248
- return this.credentialFor(accountId, fetchFn, true);
1706
+ async renewCredential(accountId, fetchFn = fetch, expected) {
1707
+ return this.credentialFor(accountId, fetchFn, true, expected);
1249
1708
  }
1250
1709
  /**
1251
1710
  * Resolve, and if needed renew, one account's credential.
@@ -1255,29 +1714,106 @@ var AccountPoolCore = class {
1255
1714
  * not name an account (the catalog loader) still has to present the
1256
1715
  * credential that is actually in play.
1257
1716
  */
1258
- async credentialFor(accountId, fetchFn, force) {
1717
+ async credentialFor(accountId, fetchFn, force, expected) {
1259
1718
  const data = await this.read();
1260
1719
  const target = accountId === void 0 ? data.accounts.find((account) => account.id === data.activeAccountId) ?? data.accounts.find((account) => account.isPrimary) ?? data.accounts[0] : data.accounts.find((account) => account.id === accountId);
1261
1720
  if (target === void 0) throw new LlmError(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`, "AUTH");
1262
1721
  if (!this.isCredentialUsable(target)) throw new LlmError(`${this.hooks.displayName} 账号的凭据已被拒绝,需要重新登录。`, "AUTH", { status: 401 });
1263
- if (!force && (!this.shouldRefreshCredential(target, Date.now()) || !this.hooks.refresh)) return target.credentials;
1264
- if (!this.hooks.refresh) return target.credentials;
1722
+ if (expected !== void 0 && !isDeepStrictEqual(expected, target.credentials)) return target.credentials;
1723
+ return (await this.freshAccount(target, fetchFn, force)).credentials;
1724
+ }
1725
+ /**
1726
+ * Return one account's current row, refreshing it at most once per generation.
1727
+ *
1728
+ * Shared by every entry point — routing, auxiliary tools, the settings card
1729
+ * and forced renewals — across all instances of the pool file, because a
1730
+ * rotating refresh token may be spent once: two parallel exchanges both fail,
1731
+ * and the losing one gets recorded as a dead sign-in.
1732
+ *
1733
+ * Reading the current generation and registering/joining its flight happen in
1734
+ * one short transaction, so a commit cannot slip between the two decisions.
1735
+ * The network call is fired AFTER the lock is released, and the plan returned
1736
+ * from the lock is a plain value: awaiting a flight while holding the lock
1737
+ * would deadlock that flight's own commit.
1738
+ */
1739
+ async freshAccount(target, fetchFn, force = false) {
1740
+ let launch;
1741
+ const plan = await this.serialize(async () => {
1742
+ const current = (await this.loadPoolData()).accounts.find((account) => account.id === target.id);
1743
+ if (current === void 0 || !this.isCredentialUsable(current)) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
1744
+ if (!isDeepStrictEqual(current.credentials, target.credentials)) return { account: current };
1745
+ if (!this.hooks.refresh || !force && !this.shouldRefreshCredential(current, Date.now())) return { account: current };
1746
+ const key = JSON.stringify([this.operationKey, current.id]);
1747
+ const pending = poolRefreshes.get(key);
1748
+ if (pending !== void 0 && isDeepStrictEqual(pending.credentials, current.credentials)) return { result: pending.result };
1749
+ const result = new Promise((resolve, reject) => {
1750
+ launch = () => {
1751
+ this.refreshAccount(current, fetchFn).then(resolve, reject);
1752
+ };
1753
+ });
1754
+ const flight = {
1755
+ credentials: structuredClone(current.credentials),
1756
+ result
1757
+ };
1758
+ poolRefreshes.set(key, flight);
1759
+ const release = () => {
1760
+ if (poolRefreshes.get(key) === flight) poolRefreshes.delete(key);
1761
+ };
1762
+ result.then(release, release);
1763
+ return { result };
1764
+ });
1765
+ launch?.();
1766
+ return plan.result ?? plan.account;
1767
+ }
1768
+ /**
1769
+ * Refresh one account: network outside the lock, then a conditional commit.
1770
+ *
1771
+ * Every write-back is checked against the credential the refresh started from,
1772
+ * which is what keeps a late response from restoring a spent token over a
1773
+ * re-login, and keeps a deleted account from coming back.
1774
+ */
1775
+ async refreshAccount(target, fetchFn) {
1776
+ const expected = structuredClone(target.credentials);
1265
1777
  const live = await this.hooks.liveCredentialsFor?.(target).catch(() => null);
1266
1778
  if (live !== null && live !== void 0 && live.advanced) {
1267
- target.credentials = live.credentials;
1268
- await this.write(data).catch(() => void 0);
1269
- return live.credentials;
1779
+ const account = await this.updateAccountCredentials(target.id, live.credentials, expected).catch(() => ({
1780
+ ...target,
1781
+ credentials: live.credentials
1782
+ }));
1783
+ this.assertUsableAccount(account);
1784
+ return account;
1270
1785
  }
1271
1786
  let refreshed;
1272
1787
  try {
1273
1788
  refreshed = await this.hooks.refresh(target.credentials, fetchFn);
1274
1789
  } catch (error) {
1275
1790
  const status = this.hooks.refreshFailureStatus?.(error);
1276
- if (status !== void 0) await this.markAuthFailed(target.id, error instanceof Error ? error.message : String(error), status).catch(() => void 0);
1791
+ let superseded;
1792
+ await this.updatePool((data) => {
1793
+ const current = data.accounts.find((account) => account.id === target.id);
1794
+ if (current === void 0) return;
1795
+ if (!isDeepStrictEqual(current.credentials, expected)) {
1796
+ superseded = current;
1797
+ return;
1798
+ }
1799
+ if (status !== void 0) {
1800
+ current.authStatus = status;
1801
+ current.authFailedReason = error instanceof Error ? error.message : String(error);
1802
+ }
1803
+ }).catch(() => void 0);
1804
+ if (superseded !== void 0) {
1805
+ this.assertUsableAccount(superseded);
1806
+ return superseded;
1807
+ }
1277
1808
  throw error;
1278
1809
  }
1279
- await this.updateAccountCredentials(target.id, refreshed);
1280
- return refreshed;
1810
+ const account = await this.updateAccountCredentials(target.id, refreshed, expected);
1811
+ this.assertUsableAccount(account);
1812
+ return account;
1813
+ }
1814
+ assertUsableAccount(account) {
1815
+ if (account === void 0) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
1816
+ if (!this.isCredentialUsable(account)) throw new PoolAccountUnavailableError(this.hooks.displayName + " 账号的凭据已被拒绝,需要重新登录。", "AUTH", { status: 401 });
1281
1817
  }
1282
1818
  /** Whether one account's credential must be refreshed before it is handed out. */
1283
1819
  shouldRefreshCredential(account, now) {
@@ -1321,37 +1857,34 @@ var AccountPoolCore = class {
1321
1857
  * @param fetchFn - fetch used by the provider's token refresh.
1322
1858
  * @throws LlmError with `RATE_LIMIT` when every account is cooling down.
1323
1859
  */
1324
- async getEffectiveAccount(excludeIds, fetchFn = fetch) {
1860
+ async getEffectiveAccount(excludeIds, fetchFn = fetch, forceRefresh = false) {
1325
1861
  const data = await this.read();
1326
1862
  if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
1327
1863
  const now = Date.now();
1328
1864
  const eligible = data.accounts.filter((account) => this.isEligible(account, now, excludeIds));
1329
1865
  if (eligible.length === 0) throw this.allUnavailableError(data, now);
1330
1866
  const selected = this.selectAccount(eligible, data);
1331
- if (this.shouldRefreshCredential(selected, now) && this.hooks.refresh) {
1332
- let refreshed;
1333
- try {
1334
- refreshed = await this.hooks.refresh(selected.credentials, fetchFn);
1335
- } catch (error) {
1336
- const status = this.hooks.refreshFailureStatus?.(error);
1337
- if (status === void 0) throw error;
1338
- await this.markAuthFailed(selected.id, error instanceof Error ? error.message : String(error), status);
1339
- const nextExclude = new Set(excludeIds ?? []);
1340
- nextExclude.add(selected.id);
1341
- if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn);
1342
- throw error;
1343
- }
1344
- selected.credentials = refreshed;
1345
- const index = data.accounts.findIndex((account) => account.id === selected.id);
1346
- if (index >= 0) data.accounts[index] = selected;
1347
- if (selected.isPrimary) this.hooks.mirrorPrimary?.(refreshed).catch(() => void 0);
1348
- }
1349
- selected.lastUsedAt = now;
1350
- data.activeAccountId = selected.id;
1351
- await this.write(data).catch(() => void 0);
1867
+ let refreshed;
1868
+ try {
1869
+ refreshed = await this.freshAccount(selected, fetchFn, forceRefresh);
1870
+ } catch (error) {
1871
+ if (this.hooks.refreshFailureStatus?.(error) === void 0 && !(error instanceof PoolAccountUnavailableError)) throw error;
1872
+ const nextExclude = new Set(excludeIds ?? []);
1873
+ nextExclude.add(selected.id);
1874
+ if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn, forceRefresh);
1875
+ throw error;
1876
+ }
1877
+ const account = await this.updatePool((current) => {
1878
+ const latest = current.accounts.find((entry) => entry.id === refreshed.id);
1879
+ if (latest === void 0 || !isDeepStrictEqual(latest.credentials, refreshed.credentials)) return latest;
1880
+ latest.lastUsedAt = now;
1881
+ if (current.activeAccountId === data.activeAccountId) current.activeAccountId = latest.id;
1882
+ return latest;
1883
+ }).catch(() => refreshed);
1884
+ this.assertUsableAccount(account);
1352
1885
  return {
1353
- account: selected,
1354
- credentials: selected.credentials
1886
+ account,
1887
+ credentials: account.credentials
1355
1888
  };
1356
1889
  }
1357
1890
  };
@@ -1724,8 +2257,7 @@ var OAuthService = class {
1724
2257
  async refresh() {
1725
2258
  this.assertAvailable();
1726
2259
  if (this.pool !== null) {
1727
- const { account, credentials } = await this.pool.getEffectiveAccount(void 0, this.fetchFn);
1728
- await this.refreshAccount(credentials).then((refreshed) => this.pool.updateAccountCredentials(account.id, refreshed));
2260
+ await this.pool.getEffectiveAccount(void 0, this.fetchFn, true);
1729
2261
  return this.status();
1730
2262
  }
1731
2263
  const stored = await this.loadAuthenticated();
@@ -1787,18 +2319,8 @@ var OAuthService = class {
1787
2319
  */
1788
2320
  async credentials(forceRefresh = false, access = {}) {
1789
2321
  if (this.pool !== null) {
1790
- if (access.purpose === "tool") {
1791
- const { account, credentials } = await this.pool.getCredentialAccount(this.fetchFn);
1792
- if (!forceRefresh) return credentials;
1793
- const refreshed = await this.refreshAccount(credentials, this.fetchFn);
1794
- await this.pool.updateAccountCredentials(account.id, refreshed);
1795
- return refreshed;
1796
- }
1797
- const { account, credentials } = await this.pool.getEffectiveAccount(void 0, this.fetchFn);
1798
- if (!forceRefresh) return credentials;
1799
- const refreshed = await this.refreshAccount(credentials, this.fetchFn);
1800
- await this.pool.updateAccountCredentials(account.id, refreshed);
1801
- return refreshed;
2322
+ if (access.purpose === "tool") return (await this.pool.getCredentialAccount(this.fetchFn, forceRefresh)).credentials;
2323
+ return (await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh)).credentials;
1802
2324
  }
1803
2325
  const stored = await this.loadAuthenticated();
1804
2326
  if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return this.refreshCredentials(stored);
@@ -2251,11 +2773,8 @@ var CodexAccountPool = class extends AccountPoolCore {
2251
2773
  async refreshAccountNow(accountId) {
2252
2774
  const account = (await this.read()).accounts.find((entry) => entry.id === accountId);
2253
2775
  if (account === void 0) throw new Error("ChatGPT 账号已不在号池中");
2254
- const refresher = this.refresherRef.current;
2255
- if (refresher === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
2256
- const refreshed = await refresher.refreshAccount(account.credentials);
2257
- await this.updateAccountCredentials(accountId, refreshed);
2258
- return refreshed;
2776
+ if (this.refresherRef.current === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
2777
+ return this.renewCredential(accountId, fetch, account.credentials);
2259
2778
  }
2260
2779
  /** The single-credential store this pool mirrors its primary account into. */
2261
2780
  mirrorStore() {
@@ -2627,31 +3146,6 @@ function normalizeGenerateOptions(options) {
2627
3146
  };
2628
3147
  }
2629
3148
  //#endregion
2630
- //#region src/host/wire-auth.ts
2631
- function codexHeaders(credentials, sessionId) {
2632
- const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
2633
- return {
2634
- authorization: `Bearer ${credentials.accessToken}`,
2635
- ...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
2636
- originator: CODEX_ORIGINATOR,
2637
- "user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
2638
- ...sessionId ? { "session-id": sessionId } : {}
2639
- };
2640
- }
2641
- function stableSessionId(value) {
2642
- const source = value === void 0 || value === "" ? randomUUID() : value;
2643
- return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
2644
- }
2645
- function retryAfterMs$1(headers) {
2646
- const raw = headers.get("retry-after");
2647
- if (raw === null) return void 0;
2648
- const seconds = Number(raw);
2649
- if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
2650
- const timestamp = Date.parse(raw);
2651
- if (!Number.isFinite(timestamp)) return void 0;
2652
- return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
2653
- }
2654
- //#endregion
2655
3149
  //#region src/host/codex-images.ts
2656
3150
  const PNG_SIGNATURE = [
2657
3151
  137,
@@ -2805,7 +3299,6 @@ function requestImage(fetchFn, credentials, prompt, turnId, signal) {
2805
3299
  method: "POST",
2806
3300
  headers: {
2807
3301
  ...codexHeaders(credentials),
2808
- originator: "pi",
2809
3302
  accept: "application/json",
2810
3303
  "content-type": "application/json",
2811
3304
  "x-codex-image-turn-id": turnId
@@ -2905,7 +3398,6 @@ function sendSearch(fetchFn, credentials, query, model, id, signal) {
2905
3398
  method: "POST",
2906
3399
  headers: {
2907
3400
  ...codexHeaders(credentials),
2908
- originator: "pi",
2909
3401
  accept: "application/json",
2910
3402
  "content-type": "application/json"
2911
3403
  },
@@ -3058,8 +3550,8 @@ function parseEnvProxy(env = process.env, options = {}) {
3058
3550
  try {
3059
3551
  const dshHome = env.DSH_HOME || path.join(os.homedir(), ".dsh");
3060
3552
  const envFile = options.envFile ?? path.join(dshHome, ".env");
3061
- if (fs.existsSync(envFile)) {
3062
- const match = fs.readFileSync(envFile, "utf8").match(/^(?:export\s+)?(?:HTTPS_PROXY|https_proxy|HTTP_PROXY|http_proxy|ALL_PROXY|all_proxy)\s*=\s*["']?([^"'\r\n]+)["']?/m);
3553
+ if (fs$1.existsSync(envFile)) {
3554
+ const match = fs$1.readFileSync(envFile, "utf8").match(/^(?:export\s+)?(?:HTTPS_PROXY|https_proxy|HTTP_PROXY|http_proxy|ALL_PROXY|all_proxy)\s*=\s*["']?([^"'\r\n]+)["']?/m);
3063
3555
  if (match && match[1]?.trim()) return normalizeProxyUrl(match[1].trim());
3064
3556
  }
3065
3557
  } catch {}
@@ -3680,7 +4172,7 @@ var FileCredentialStore$2 = class {
3680
4172
  }
3681
4173
  async removeLegacy() {
3682
4174
  try {
3683
- await fsPromises.unlink(this.filePath);
4175
+ await fs.unlink(this.filePath);
3684
4176
  } catch (error) {
3685
4177
  if (error.code !== "ENOENT") throw new Error("Antigravity legacy credential removal failed");
3686
4178
  }
@@ -3699,11 +4191,11 @@ var FileCredentialStore$2 = class {
3699
4191
  }
3700
4192
  let legacy;
3701
4193
  try {
3702
- const stats = await fsPromises.lstat(this.filePath);
4194
+ const stats = await fs.lstat(this.filePath);
3703
4195
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
3704
4196
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
3705
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
3706
- legacy = await fsPromises.readFile(this.filePath, "utf8");
4197
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
4198
+ legacy = await fs.readFile(this.filePath, "utf8");
3707
4199
  } catch (error) {
3708
4200
  if (error.code === "ENOENT") return null;
3709
4201
  throw new Error("Antigravity legacy credential read failed");
@@ -3738,7 +4230,7 @@ var FileModelSettingsStore$2 = class {
3738
4230
  }
3739
4231
  async read() {
3740
4232
  try {
3741
- const content = await fsPromises.readFile(this.filePath, "utf8");
4233
+ const content = await fs.readFile(this.filePath, "utf8");
3742
4234
  const parsed = JSON.parse(content);
3743
4235
  if (typeof parsed === "object" && parsed !== null) {
3744
4236
  const record = parsed;
@@ -3764,10 +4256,10 @@ var FileModelSettingsStore$2 = class {
3764
4256
  };
3765
4257
  }
3766
4258
  async write(settings) {
3767
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
4259
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
3768
4260
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
3769
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
3770
- await fsPromises.rename(tmp, this.filePath);
4261
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
4262
+ await fs.rename(tmp, this.filePath);
3771
4263
  }
3772
4264
  async updateSettings(patch) {
3773
4265
  const current = await this.read();
@@ -3893,7 +4385,7 @@ var FilePreferencesStore = class {
3893
4385
  async readDocument() {
3894
4386
  let parsed;
3895
4387
  try {
3896
- parsed = JSON.parse(await fsPromises.readFile(this.filePath, "utf8"));
4388
+ parsed = JSON.parse(await fs.readFile(this.filePath, "utf8"));
3897
4389
  } catch {
3898
4390
  return;
3899
4391
  }
@@ -3914,17 +4406,17 @@ var FilePreferencesStore = class {
3914
4406
  for (const listener of [...this.listeners]) listener(next, prev);
3915
4407
  }
3916
4408
  async write(value) {
3917
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
4409
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
3918
4410
  writeSequence += 1;
3919
4411
  const tmp = `${this.filePath}.tmp.${process.pid}.${writeSequence}`;
3920
4412
  try {
3921
- await fsPromises.writeFile(tmp, JSON.stringify(value, null, 2), {
4413
+ await fs.writeFile(tmp, JSON.stringify(value, null, 2), {
3922
4414
  encoding: "utf8",
3923
4415
  mode: 384
3924
4416
  });
3925
- await fsPromises.rename(tmp, this.filePath);
4417
+ await fs.rename(tmp, this.filePath);
3926
4418
  } finally {
3927
- await fsPromises.unlink(tmp).catch(() => void 0);
4419
+ await fs.unlink(tmp).catch(() => void 0);
3928
4420
  }
3929
4421
  }
3930
4422
  };
@@ -3989,7 +4481,7 @@ const LEGACY_DOCUMENTS = ["settings.yaml", "settings.yaml.imported"];
3989
4481
  */
3990
4482
  async function readLegacyPreferences(homeDir = dshHomeDir$2()) {
3991
4483
  for (const name of LEGACY_DOCUMENTS) {
3992
- const document = await fsPromises.readFile(path.join(homeDir, name), "utf8").catch(() => void 0);
4484
+ const document = await fs.readFile(path.join(homeDir, name), "utf8").catch(() => void 0);
3993
4485
  if (document === void 0) continue;
3994
4486
  const section = sectionOf(document, PREFERENCES_NAMESPACE);
3995
4487
  if (section !== void 0 && Object.keys(section).length > 0) return section;
@@ -4297,7 +4789,7 @@ function normalizeReplayCallIds(items, normalizeCallId) {
4297
4789
  call_id: normalizeCallId(item.call_id)
4298
4790
  } : item);
4299
4791
  }
4300
- async function buildResponsesPayload(options, attachments, localRawImages = {}, outputVerbosity = null, fastMode = false, reasoningSummary = null) {
4792
+ async function buildResponsesPayload(options, attachments, localRawImages = {}, outputVerbosity = null, fastMode = false, reasoningSummary = null, promptCacheKey) {
4301
4793
  const sandboxRetryTools = recentSandboxRetryToolNames(options.messages);
4302
4794
  const resolveLocalRawImages = supportsImageInput(options);
4303
4795
  const normalizeCallId = createCallIdNormalizer();
@@ -4365,7 +4857,8 @@ async function buildResponsesPayload(options, attachments, localRawImages = {},
4365
4857
  input,
4366
4858
  stream: true,
4367
4859
  store: false,
4368
- include: ["reasoning.encrypted_content"]
4860
+ include: ["reasoning.encrypted_content"],
4861
+ ...promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}
4369
4862
  };
4370
4863
  const instructions = [...instructionParts, localRawImageInstruction(localImageStats)].filter((value) => Boolean(value));
4371
4864
  if (instructions.length > 0) payload.instructions = instructions.join("\n\n");
@@ -4381,6 +4874,8 @@ async function buildResponsesPayload(options, attachments, localRawImages = {},
4381
4874
  }
4382
4875
  if (outputVerbosity !== null) payload.text = { verbosity: outputVerbosity };
4383
4876
  if (fastMode) payload.service_tier = "priority";
4877
+ const modelMaxTokens = codexModelMaxTokens(options.model);
4878
+ payload.max_output_tokens = options.maxTokens === void 0 ? modelMaxTokens : Math.min(options.maxTokens, modelMaxTokens);
4384
4879
  if (options.reasoningEffort !== void 0) {
4385
4880
  const effort = codexWireReasoningEffort(options.model, options.reasoningEffort);
4386
4881
  payload.reasoning = codexModelSupportsReasoningSummary(options.model) ? {
@@ -4698,6 +5193,8 @@ const MAX_VISIBLE_REASONING_CHARS = 12e3;
4698
5193
  const DEFAULT_POOL_COOLDOWN_MS = 15 * 6e4;
4699
5194
  const REASONING_DELTA_FLUSH_CHARS = 768;
4700
5195
  const REASONING_TRUNCATED_NOTICE = "\n\n[Reasoning summary truncated to keep the DSH web UI responsive.]";
5196
+ /** Cap on conversations whose turn state is remembered at once. */
5197
+ const MAX_TRACKED_TURN_STATES = 200;
4701
5198
  var ResponsesClient = class {
4702
5199
  oauth;
4703
5200
  attachments;
@@ -4707,6 +5204,16 @@ var ResponsesClient = class {
4707
5204
  fastMode;
4708
5205
  reasoningSummary;
4709
5206
  accountPool;
5207
+ /**
5208
+ * Opaque backend turn state, keyed by conversation session id.
5209
+ *
5210
+ * The Codex backend hands `x-codex-turn-state` back on each response and
5211
+ * expects it on the next request of the same conversation, which lets it
5212
+ * resume the turn instead of re-ingesting the whole history. The value is
5213
+ * short-lived and account-scoped, so it is stored per session and simply
5214
+ * dropped when the backend stops sending it — never a correctness input.
5215
+ */
5216
+ turnStates = /* @__PURE__ */ new Map();
4710
5217
  constructor(oauth, attachments, options = {}) {
4711
5218
  this.oauth = oauth;
4712
5219
  this.attachments = attachments;
@@ -4727,7 +5234,7 @@ var ResponsesClient = class {
4727
5234
  let attemptOptions = options;
4728
5235
  let response;
4729
5236
  while (true) {
4730
- const payload = await buildResponsesPayload(attemptOptions, this.attachments, this.localRawImages, this.outputVerbosity(), this.fastMode(), this.reasoningSummary());
5237
+ const payload = await buildResponsesPayload(attemptOptions, this.attachments, this.localRawImages, this.outputVerbosity(), this.fastMode(), this.reasoningSummary(), promptCacheKeyFor(sessionId));
4731
5238
  try {
4732
5239
  response = await this.send(payload, sessionId, options.signal);
4733
5240
  break;
@@ -4762,9 +5269,36 @@ var ResponsesClient = class {
4762
5269
  response = await this.request(payload, credentials, sessionId, signal);
4763
5270
  }
4764
5271
  if (!response.ok) throw await responseError(response);
5272
+ this.rememberTurnState(response, sessionId);
4765
5273
  return response;
4766
5274
  }
4767
5275
  /**
5276
+ * Record the turn state a response carried, for the next turn of this session.
5277
+ *
5278
+ * A response that carries none clears any stored value: a backend that stops
5279
+ * sending the header has stopped honouring it, and replaying a stale value
5280
+ * would be guessing. An empty header is treated the same as absent.
5281
+ */
5282
+ rememberTurnState(response, sessionId) {
5283
+ const next = response.headers.get(CODEX_TURN_STATE_HEADER)?.trim();
5284
+ if (next === void 0 || next === "") this.turnStates.delete(sessionId);
5285
+ else this.storeTurnState(sessionId, next);
5286
+ }
5287
+ /**
5288
+ * Bounded store: a long-lived host serves many sessions, and nothing here ever
5289
+ * signals that a session ended, so the map is capped and evicts the oldest
5290
+ * entry. The value is an optimization, so a miss only costs a full resend.
5291
+ */
5292
+ storeTurnState(sessionId, value) {
5293
+ this.turnStates.delete(sessionId);
5294
+ this.turnStates.set(sessionId, value);
5295
+ while (this.turnStates.size > MAX_TRACKED_TURN_STATES) {
5296
+ const oldest = this.turnStates.keys().next();
5297
+ if (oldest.done === true) break;
5298
+ this.turnStates.delete(oldest.value);
5299
+ }
5300
+ }
5301
+ /**
4768
5302
  * Send one payload with account rotation.
4769
5303
  *
4770
5304
  * The pool picks the account — skipping cooling, quota-exhausted and failed
@@ -4806,6 +5340,7 @@ var ResponsesClient = class {
4806
5340
  });
4807
5341
  }
4808
5342
  if (!response.ok) throw await responseError(response);
5343
+ this.rememberTurnState(response, sessionId);
4809
5344
  return response;
4810
5345
  }
4811
5346
  }
@@ -4814,7 +5349,7 @@ var ResponsesClient = class {
4814
5349
  return await this.fetchFn(CODEX_RESPONSES_URL, {
4815
5350
  method: "POST",
4816
5351
  headers: {
4817
- ...codexHeaders(credentials, sessionId),
5352
+ ...codexHeaders(credentials, sessionId, { turnState: this.turnStates.get(sessionId) }),
4818
5353
  "content-type": "application/json",
4819
5354
  accept: "text/event-stream"
4820
5355
  },
@@ -4827,6 +5362,17 @@ var ResponsesClient = class {
4827
5362
  }
4828
5363
  }
4829
5364
  };
5365
+ /**
5366
+ * Prompt cache key for one conversation.
5367
+ *
5368
+ * The backend keys its prefix cache on this string, so it must be stable for a
5369
+ * conversation and distinct between conversations — otherwise one session would
5370
+ * be served another session's cached prefix. The session id is already a hashed
5371
+ * opaque string, so it is reused directly rather than hashed again.
5372
+ */
5373
+ function promptCacheKeyFor(sessionId) {
5374
+ return sessionId;
5375
+ }
4830
5376
  async function* parseResponsesStream(response, signal, hiddenSandboxControls = /* @__PURE__ */ new Set()) {
4831
5377
  if (response.body === null) throw new LlmError("Codex returned no response stream.", "PROVIDER_ERROR");
4832
5378
  const reader = response.body.getReader();
@@ -5818,7 +6364,7 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
5818
6364
  "Content-Type": "application/javascript; charset=utf-8",
5819
6365
  "Cache-Control": "public, max-age=86400"
5820
6366
  });
5821
- fs.createReadStream(mermaidPath).pipe(response);
6367
+ fs$1.createReadStream(mermaidPath).pipe(response);
5822
6368
  } catch {
5823
6369
  response.writeHead(404, { "Content-Type": "text/plain" });
5824
6370
  response.end("Not found");
@@ -9384,7 +9930,7 @@ var FileCredentialStore$1 = class {
9384
9930
  }
9385
9931
  async removeLegacy() {
9386
9932
  try {
9387
- await fsPromises.unlink(this.filePath);
9933
+ await fs.unlink(this.filePath);
9388
9934
  } catch (error) {
9389
9935
  if (error.code !== "ENOENT") throw new Error("Command Code legacy credential removal failed");
9390
9936
  }
@@ -9403,11 +9949,11 @@ var FileCredentialStore$1 = class {
9403
9949
  }
9404
9950
  let legacy;
9405
9951
  try {
9406
- const stats = await fsPromises.lstat(this.filePath);
9952
+ const stats = await fs.lstat(this.filePath);
9407
9953
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
9408
9954
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
9409
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
9410
- legacy = await fsPromises.readFile(this.filePath, "utf8");
9955
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
9956
+ legacy = await fs.readFile(this.filePath, "utf8");
9411
9957
  } catch (error) {
9412
9958
  if (error.code === "ENOENT") return null;
9413
9959
  throw new Error("Command Code legacy credential read failed");
@@ -9443,7 +9989,7 @@ var FileModelSettingsStore$1 = class {
9443
9989
  }
9444
9990
  async read() {
9445
9991
  try {
9446
- const content = await fsPromises.readFile(this.filePath, "utf8");
9992
+ const content = await fs.readFile(this.filePath, "utf8");
9447
9993
  const parsed = JSON.parse(content);
9448
9994
  if (typeof parsed === "object" && parsed !== null) {
9449
9995
  const record = parsed;
@@ -9469,10 +10015,10 @@ var FileModelSettingsStore$1 = class {
9469
10015
  };
9470
10016
  }
9471
10017
  async write(settings) {
9472
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
10018
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
9473
10019
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
9474
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
9475
- await fsPromises.rename(tmp, this.filePath);
10020
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
10021
+ await fs.rename(tmp, this.filePath);
9476
10022
  }
9477
10023
  async updateSettings(patch) {
9478
10024
  const current = await this.read();
@@ -9571,121 +10117,6 @@ function commandCodePlanLabel(planId) {
9571
10117
  return resolveCommandCodePlan(planId)?.name ?? planId;
9572
10118
  }
9573
10119
  //#endregion
9574
- //#region src/host/common/catalog-snapshot.ts
9575
- /**
9576
- * Persistent snapshot for one provider's live model catalog.
9577
- *
9578
- * The in-memory catalog caches keep a process alive between requests, but they
9579
- * start cold: on the first call after a restart (or a plugin reinstall) the
9580
- * picker and the settings page both wait on the upstream listing before they
9581
- * can render anything, and an unreachable endpoint turns that wait into the
9582
- * full discovery timeout — "the model server is unavailable" on a fresh
9583
- * install. The catalog is not fast-moving data, so the last successful listing
9584
- * is persisted to `<DSH_HOME>/storages/` and rehydrated into the in-memory
9585
- * cache on first use. A snapshot the caller can read is therefore available
9586
- * from the very first call, with no network in the path.
9587
- *
9588
- * Writes are best-effort: persistence is an optimization, never a capability.
9589
- * A read-only home or a locked file must not fail the fetch that produced the
9590
- * snapshot.
9591
- */
9592
- /** File name of the persisted snapshot for one catalog. */
9593
- function catalogSnapshotPath(name) {
9594
- return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
9595
- }
9596
- /**
9597
- * Snapshot name for one catalog *and* one credential scope.
9598
- *
9599
- * A persisted snapshot answers before any credential is read — that is the whole
9600
- * point of it — so the file itself has to carry the scope it was fetched under.
9601
- * Otherwise the first call after a restart, for any account, is answered by
9602
- * whatever listing some other scope happened to persist last: a Kimi listing
9603
- * fetched for the mainland-cn endpoint would answer a global caller with models
9604
- * and context windows that region does not serve, and a Command Code listing
9605
- * fetched against one API environment would answer the other.
9606
- *
9607
- * The scope is part of the file name rather than of the payload because a caller
9608
- * that cannot name the account — the harness resolves this catalog with no token
9609
- * at all — still has to find the snapshot its own scope owns.
9610
- *
9611
- * Everything outside `[a-z0-9]` is folded to `_`, so no scope can escape the
9612
- * `storages` directory.
9613
- */
9614
- function catalogSnapshotName(base, scope) {
9615
- const normalized = scope.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
9616
- return normalized === "" ? base : `${base}-${normalized}`;
9617
- }
9618
- /**
9619
- * Read the persisted snapshot for one catalog.
9620
- *
9621
- * Shape errors and unreadable files both return `undefined` — the caller keeps
9622
- * its in-memory state, and the next successful fetch overwrites the file.
9623
- */
9624
- async function readCatalogSnapshot(name, parseModels) {
9625
- try {
9626
- const content = await fsPromises.readFile(catalogSnapshotPath(name), "utf8");
9627
- const parsed = JSON.parse(content);
9628
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
9629
- const record = parsed;
9630
- const fetchedAt = record.fetchedAt;
9631
- if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
9632
- const models = parseModels(record.models);
9633
- if (models === void 0 || models.length === 0) return void 0;
9634
- return {
9635
- fetchedAt,
9636
- models
9637
- };
9638
- } catch {
9639
- return;
9640
- }
9641
- }
9642
- /**
9643
- * Snapshot writes still in flight.
9644
- *
9645
- * Callers fire-and-forget these writes, so nothing in production ever awaits
9646
- * one. A test that deletes its private `storages` directory between cases
9647
- * cannot know when that is safe, and a write that lands after the delete
9648
- * recreates the file the next case asserts is absent — which is how a stale
9649
- * listing leaked into a later test and failed it on CI. Registering each write
9650
- * gives {@link flushCatalogSnapshots} a deterministic point to wait on.
9651
- */
9652
- const pendingSnapshotWrites = /* @__PURE__ */ new Set();
9653
- /** Persist one snapshot; a failure is logged nowhere and never thrown. */
9654
- function writeCatalogSnapshot(name, models, fetchedAt) {
9655
- const write = (async () => {
9656
- try {
9657
- const filePath = catalogSnapshotPath(name);
9658
- await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
9659
- const tmp = `${filePath}.tmp.${Date.now()}`;
9660
- await fsPromises.writeFile(tmp, JSON.stringify({
9661
- fetchedAt,
9662
- models
9663
- }, null, 2), "utf8");
9664
- await fsPromises.rename(tmp, filePath);
9665
- } catch {}
9666
- })();
9667
- pendingSnapshotWrites.add(write);
9668
- write.finally(() => {
9669
- pendingSnapshotWrites.delete(write);
9670
- });
9671
- return write;
9672
- }
9673
- /**
9674
- * Warm an in-memory cache from the persisted snapshot when it is fresher than
9675
- * what the cache currently holds. Called lazily on the first cache miss of a
9676
- * process, so a restart serves the previous listing without waiting on the
9677
- * network.
9678
- *
9679
- * @returns the snapshot's `fetchedAt`, or `0` when no usable snapshot exists —
9680
- * the caller then fetches as before.
9681
- */
9682
- async function rehydrateCatalogCache(name, parseModels, accept, currentFetchedAt) {
9683
- const snapshot = await readCatalogSnapshot(name, parseModels);
9684
- if (snapshot === void 0 || snapshot.fetchedAt <= currentFetchedAt) return 0;
9685
- accept(snapshot.fetchedAt, snapshot.models);
9686
- return snapshot.fetchedAt;
9687
- }
9688
- //#endregion
9689
10120
  //#region src/host/command-code/client.ts
9690
10121
  const CLI_VERSION = "1.0.0";
9691
10122
  /**
@@ -12955,6 +13386,10 @@ function maxOutputTokensFor$3(modelId, contextWindow) {
12955
13386
  const capped = (contextWindow ?? kimiCodeModelDef(modelId)?.contextWindow ?? 262144) - CONTEXT_HEADROOM_TOKENS$1;
12956
13387
  return Math.max(declared, capped);
12957
13388
  }
13389
+ /** Whether a stored preference names a tier this line can send. */
13390
+ function isKimiCacheTtl(value) {
13391
+ return value === "5m" || value === "1h";
13392
+ }
12958
13393
  /**
12959
13394
  * Reduce the requested output cap so prompt + output fit the window.
12960
13395
  *
@@ -13004,7 +13439,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13004
13439
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$3],
13005
13440
  catalogModels: [],
13006
13441
  contextWindowOverrides: {},
13007
- defaultReasoningEffort: null
13442
+ defaultReasoningEffort: null,
13443
+ cacheTtl: null
13008
13444
  };
13009
13445
  fallbackStore.read().then((stored) => {
13010
13446
  snapshot = stored;
@@ -13021,7 +13457,12 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13021
13457
  enabled: z.boolean().default(true),
13022
13458
  enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS$3]),
13023
13459
  contextWindowOverrides: z.dict(z.number()).default({}),
13024
- defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null)
13460
+ defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null),
13461
+ cacheTtl: z.union([
13462
+ z.const("5m"),
13463
+ z.const("1h"),
13464
+ z.const(null)
13465
+ ]).default(null)
13025
13466
  }));
13026
13467
  return {
13027
13468
  status: () => {
@@ -13031,7 +13472,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13031
13472
  enabledModelIds: value.enabledModelIds,
13032
13473
  catalogModels: [],
13033
13474
  contextWindowOverrides: value.contextWindowOverrides,
13034
- defaultReasoningEffort: value.defaultReasoningEffort
13475
+ defaultReasoningEffort: value.defaultReasoningEffort,
13476
+ cacheTtl: isKimiCacheTtl(value.cacheTtl) ? value.cacheTtl : null
13035
13477
  };
13036
13478
  },
13037
13479
  update: async (patch) => {
@@ -13040,7 +13482,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13040
13482
  enabled: patch.enabled !== void 0 ? patch.enabled : current.enabled !== false,
13041
13483
  enabledModelIds: patch.enabledModelIds ?? current.enabledModelIds,
13042
13484
  contextWindowOverrides: patch.contextWindowOverrides ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
13043
- defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort
13485
+ defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort,
13486
+ cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl
13044
13487
  };
13045
13488
  await scope.update(normalized);
13046
13489
  fallbackStore.updateSettings(patch).catch(() => void 0);
@@ -13162,7 +13605,7 @@ var FileCredentialStore$3 = class {
13162
13605
  }
13163
13606
  async removeLegacy() {
13164
13607
  try {
13165
- await fsPromises.unlink(this.filePath);
13608
+ await fs.unlink(this.filePath);
13166
13609
  } catch (error) {
13167
13610
  if (error.code !== "ENOENT") throw new Error("Kimi Code legacy credential removal failed");
13168
13611
  }
@@ -13181,11 +13624,11 @@ var FileCredentialStore$3 = class {
13181
13624
  }
13182
13625
  let legacy;
13183
13626
  try {
13184
- const stats = await fsPromises.lstat(this.filePath);
13627
+ const stats = await fs.lstat(this.filePath);
13185
13628
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
13186
13629
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
13187
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
13188
- legacy = await fsPromises.readFile(this.filePath, "utf8");
13630
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
13631
+ legacy = await fs.readFile(this.filePath, "utf8");
13189
13632
  } catch (error) {
13190
13633
  if (error.code === "ENOENT") return null;
13191
13634
  throw new Error("Kimi Code legacy credential read failed");
@@ -13221,7 +13664,7 @@ var FileModelSettingsStore$3 = class {
13221
13664
  }
13222
13665
  async read() {
13223
13666
  try {
13224
- const content = await fsPromises.readFile(this.filePath, "utf8");
13667
+ const content = await fs.readFile(this.filePath, "utf8");
13225
13668
  const parsed = JSON.parse(content);
13226
13669
  if (typeof parsed === "object" && parsed !== null) {
13227
13670
  const record = parsed;
@@ -13234,7 +13677,8 @@ var FileModelSettingsStore$3 = class {
13234
13677
  enabledModelIds,
13235
13678
  catalogModels,
13236
13679
  contextWindowOverrides,
13237
- defaultReasoningEffort
13680
+ defaultReasoningEffort,
13681
+ cacheTtl: isKimiCacheTtl(record.cacheTtl) ? record.cacheTtl : null
13238
13682
  };
13239
13683
  }
13240
13684
  } catch {}
@@ -13243,14 +13687,15 @@ var FileModelSettingsStore$3 = class {
13243
13687
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$3],
13244
13688
  catalogModels: [],
13245
13689
  contextWindowOverrides: {},
13246
- defaultReasoningEffort: null
13690
+ defaultReasoningEffort: null,
13691
+ cacheTtl: null
13247
13692
  };
13248
13693
  }
13249
13694
  async write(settings) {
13250
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
13695
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
13251
13696
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
13252
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
13253
- await fsPromises.rename(tmp, this.filePath);
13697
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
13698
+ await fs.rename(tmp, this.filePath);
13254
13699
  }
13255
13700
  async updateSettings(patch) {
13256
13701
  const current = await this.read();
@@ -13259,7 +13704,8 @@ var FileModelSettingsStore$3 = class {
13259
13704
  ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {},
13260
13705
  ...patch.enabledModelIds !== void 0 ? { enabledModelIds: patch.enabledModelIds } : {},
13261
13706
  ...patch.contextWindowOverrides !== void 0 ? { contextWindowOverrides: mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) } : {},
13262
- ...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {}
13707
+ ...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {},
13708
+ ...patch.cacheTtl !== void 0 && patch.cacheTtl !== null ? { cacheTtl: isKimiCacheTtl(patch.cacheTtl) ? patch.cacheTtl : current.cacheTtl } : patch.cacheTtl === null ? { cacheTtl: null } : {}
13263
13709
  };
13264
13710
  await this.write(next);
13265
13711
  return next;
@@ -13289,7 +13735,7 @@ async function resolveRegion(env = process.env) {
13289
13735
  return pinned.includes(".ai") ? "global" : "mainland-cn";
13290
13736
  }
13291
13737
  try {
13292
- const marker = (await fsPromises.readFile(regionMarkerPath(), "utf8")).trim();
13738
+ const marker = (await fs.readFile(regionMarkerPath(), "utf8")).trim();
13293
13739
  if (isRegion$4(marker)) return marker;
13294
13740
  } catch {}
13295
13741
  return "mainland-cn";
@@ -13298,8 +13744,8 @@ async function resolveRegion(env = process.env) {
13298
13744
  async function persistRegion(region) {
13299
13745
  try {
13300
13746
  const file = regionMarkerPath();
13301
- await fsPromises.mkdir(path.dirname(file), { recursive: true });
13302
- await fsPromises.writeFile(file, region, "utf8");
13747
+ await fs.mkdir(path.dirname(file), { recursive: true });
13748
+ await fs.writeFile(file, region, "utf8");
13303
13749
  } catch {}
13304
13750
  }
13305
13751
  //#endregion
@@ -13401,13 +13847,13 @@ function deviceModel() {
13401
13847
  async function getDeviceId() {
13402
13848
  const file = deviceIdPath();
13403
13849
  try {
13404
- const existing = (await fsPromises.readFile(file, "utf8")).trim();
13850
+ const existing = (await fs.readFile(file, "utf8")).trim();
13405
13851
  if (existing !== "") return existing;
13406
13852
  } catch {}
13407
13853
  const created = randomUUID();
13408
13854
  try {
13409
- await fsPromises.mkdir(path.dirname(file), { recursive: true });
13410
- await fsPromises.writeFile(file, created, {
13855
+ await fs.mkdir(path.dirname(file), { recursive: true });
13856
+ await fs.writeFile(file, created, {
13411
13857
  encoding: "utf8",
13412
13858
  mode: 384
13413
13859
  });
@@ -15223,6 +15669,39 @@ function imageBlockToInline$2(block, images) {
15223
15669
  data: resolved.data
15224
15670
  } : void 0;
15225
15671
  }
15672
+ /**
15673
+ * Media a single request build may carry.
15674
+ *
15675
+ * Passed as one object so adding a media kind never grows a positional
15676
+ * signature the existing callers already bind.
15677
+ */
15678
+ /**
15679
+ * The cache field for the Anthropic-compatible wire.
15680
+ *
15681
+ * TOP LEVEL, not per block: the service documents that a `cache_control` inside
15682
+ * a message is ignored, and omitting the field means "read a 5m entry but do
15683
+ * not write one" — which is why this is only attached when the caller asked
15684
+ * for a tier.
15685
+ */
15686
+ function kimiAnthropicCacheControl(ttl) {
15687
+ return { cache_control: {
15688
+ type: "ephemeral",
15689
+ ttl
15690
+ } };
15691
+ }
15692
+ /**
15693
+ * The cache field for the OpenAI-compatible wire.
15694
+ *
15695
+ * `mode: 'implicit'` is the only mode the service supports — it identifies the
15696
+ * prefix itself, so this is not a request to break the cache but a request to
15697
+ * write it at a chosen TTL.
15698
+ */
15699
+ function kimiOpenAICacheOptions(ttl) {
15700
+ return { prompt_cache_options: {
15701
+ mode: "implicit",
15702
+ ttl
15703
+ } };
15704
+ }
15226
15705
  function textOf$3(content) {
15227
15706
  if (typeof content === "string") return sanitizeText$3(content);
15228
15707
  if (!Array.isArray(content)) return "";
@@ -15758,6 +16237,7 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
15758
16237
  stream: true,
15759
16238
  stream_options: { include_usage: true },
15760
16239
  max_completion_tokens: maxTokens,
16240
+ ...media.cacheTtl === null || media.cacheTtl === void 0 ? {} : kimiOpenAICacheOptions(media.cacheTtl),
15761
16241
  ...stopSequences(options.stop).length > 0 ? { stop: stopSequences(options.stop) } : {},
15762
16242
  ...options.tools && options.tools.length > 0 ? {
15763
16243
  tools: options.tools.map((tool) => ({
@@ -15891,7 +16371,8 @@ function mergeAnthropicMessages$1(entries) {
15891
16371
  return merged;
15892
16372
  }
15893
16373
  /** Build one `/v1/messages` body. */
15894
- function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, _media = {}) {
16374
+ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {}) {
16375
+ const cacheTtl = media.cacheTtl ?? null;
15895
16376
  const carriers = options.messages.filter((message) => message.role === "system" && messageToolsOf(message) !== void 0);
15896
16377
  const unsentDeclarations = carriers.reduce((total, message) => total + (messageToolsOf(message)?.length ?? 0), 0);
15897
16378
  const carrierText = carriers.map((message) => textOf$3(message.content)).filter((text) => text !== "");
@@ -15922,6 +16403,7 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, _media =
15922
16403
  model: options.model,
15923
16404
  max_tokens: maxTokens,
15924
16405
  messages: mergeAnthropicMessages$1(entries),
16406
+ ...cacheTtl === null || cacheTtl === void 0 ? {} : kimiAnthropicCacheControl(cacheTtl),
15925
16407
  ...system === void 0 ? {} : { system },
15926
16408
  ...options.temperature === void 0 || budget !== void 0 ? {} : { temperature: options.temperature },
15927
16409
  ...options.stop && options.stop.length > 0 ? { stop_sequences: options.stop } : {},
@@ -16724,12 +17206,13 @@ var KimiCodeAdapter = class extends LlmAdapter {
16724
17206
  const entry = catalog.find((model) => model.id === options.model);
16725
17207
  const requestOptions = offloadOldestRequestVideos(offloadOldestRequestImages$2(normalizeGenerateOptions(options)));
16726
17208
  const [images, videos] = await Promise.all([resolveRequestImages$2(requestOptions, this.options.attachments, signal), resolveRequestVideos(requestOptions, this.options.videos, signal)]);
17209
+ const settings = await this.settings();
16727
17210
  const media = {
16728
17211
  videos,
16729
17212
  videoAccepted: inputModalitiesForEntry(options.model, catalog).includes("video"),
16730
- messageTools: dynamicToolsForEntry(options.model, catalog)
17213
+ messageTools: dynamicToolsForEntry(options.model, catalog),
17214
+ cacheTtl: settings.cacheTtl
16731
17215
  };
16732
- const settings = await this.settings();
16733
17216
  const contextWindow = this.contextWindowFor(options.model, entry, settings.contextWindowOverrides);
16734
17217
  const requestedMax = options.maxTokens ?? maxOutputTokensFor$3(options.model, contextWindow);
16735
17218
  const body = assertRequestBodyFits$1(buildRequest({
@@ -17108,6 +17591,7 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
17108
17591
  models,
17109
17592
  contextWindowOverrides: settings.contextWindowOverrides,
17110
17593
  defaultReasoningEffort: settings.defaultReasoningEffort,
17594
+ cacheTtl: settings.cacheTtl,
17111
17595
  loginRegion: region,
17112
17596
  serving: readOption$3(options.serving, true),
17113
17597
  conflict: readOption$3(options.conflict, null)
@@ -17333,6 +17817,10 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
17333
17817
  const effort = body.defaultReasoningEffort;
17334
17818
  if (effort === null || isKimiCodeEffort(effort)) patch.defaultReasoningEffort = effort;
17335
17819
  }
17820
+ if (body.cacheTtl !== void 0) {
17821
+ const ttl = body.cacheTtl;
17822
+ if (ttl === null || isKimiCacheTtl(ttl)) patch.cacheTtl = ttl;
17823
+ }
17336
17824
  if (preferences) await preferences.update(patch);
17337
17825
  else await modelSettings.updateSettings(patch);
17338
17826
  if (patch.enabledModelIds !== void 0 || patch.enabled !== void 0) ctx.emit?.("llm/adapters-updated");
@@ -17542,15 +18030,15 @@ async function saveVideo(input) {
17542
18030
  const digest = createHash("sha256").update(input.data).digest("hex");
17543
18031
  const attachmentId = `sha256:${digest}`;
17544
18032
  const dir = videoStoreDir();
17545
- await fsPromises.mkdir(dir, {
18033
+ await fs.mkdir(dir, {
17546
18034
  recursive: true,
17547
18035
  mode: 448
17548
18036
  });
17549
18037
  const target = path.join(dir, digest + extensionFor(input.declaredType));
17550
- if (!await fsPromises.stat(target).then(() => true).catch(() => false)) {
18038
+ if (!await fs.stat(target).then(() => true).catch(() => false)) {
17551
18039
  const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
17552
- await fsPromises.writeFile(tmp, input.data);
17553
- await fsPromises.rename(tmp, target);
18040
+ await fs.writeFile(tmp, input.data);
18041
+ await fs.rename(tmp, target);
17554
18042
  }
17555
18043
  pruneVideoStore().catch(() => void 0);
17556
18044
  return {
@@ -17580,14 +18068,14 @@ async function pruneVideoStore(budgetBytes = VIDEO_STORE_BUDGET_BYTES) {
17580
18068
  const dir = videoStoreDir();
17581
18069
  let entries;
17582
18070
  try {
17583
- const names = await fsPromises.readdir(dir);
18071
+ const names = await fs.readdir(dir);
17584
18072
  entries = [];
17585
18073
  for (const name of names) {
17586
18074
  if (name.includes(".tmp.")) {
17587
- await fsPromises.rm(path.join(dir, name), { force: true }).catch(() => void 0);
18075
+ await fs.rm(path.join(dir, name), { force: true }).catch(() => void 0);
17588
18076
  continue;
17589
18077
  }
17590
- const stats = await fsPromises.stat(path.join(dir, name)).catch(() => void 0);
18078
+ const stats = await fs.stat(path.join(dir, name)).catch(() => void 0);
17591
18079
  if (stats === void 0 || !stats.isFile()) continue;
17592
18080
  entries.push({
17593
18081
  path: path.join(dir, name),
@@ -17604,7 +18092,7 @@ async function pruneVideoStore(budgetBytes = VIDEO_STORE_BUDGET_BYTES) {
17604
18092
  let removed = 0;
17605
18093
  for (const entry of entries) {
17606
18094
  if (total <= budgetBytes) break;
17607
- await fsPromises.rm(entry.path, { force: true }).catch(() => void 0);
18095
+ await fs.rm(entry.path, { force: true }).catch(() => void 0);
17608
18096
  total -= entry.size;
17609
18097
  removed += 1;
17610
18098
  }
@@ -17628,7 +18116,7 @@ async function readVideoBytes(ref) {
17628
18116
  if (stored === void 0) throw new VideoIngestError("video reference is not one this store issued", "VIDEO_REFERENCE_INVALID");
17629
18117
  let data;
17630
18118
  try {
17631
- data = await fsPromises.readFile(stored);
18119
+ data = await fs.readFile(stored);
17632
18120
  } catch {
17633
18121
  throw new VideoIngestError("stored video is missing; attach it again", "VIDEO_MISSING");
17634
18122
  }
@@ -17689,14 +18177,14 @@ function humanBytes(bytes) {
17689
18177
  /** Read one local file, refusing anything that is not a regular file. */
17690
18178
  async function readLocalFile(input) {
17691
18179
  if (!path.isAbsolute(input)) throw new HarnessError(`video path must be absolute: ${input}`, "KIMI_VIDEO_PATH_NOT_ABSOLUTE");
17692
- const stats = await fsPromises.stat(input).catch(() => void 0);
18180
+ const stats = await fs.stat(input).catch(() => void 0);
17693
18181
  if (stats === void 0) throw new HarnessError(`video file not found: ${input}`, "KIMI_VIDEO_NOT_FOUND");
17694
18182
  if (!stats.isFile()) throw new HarnessError(`video path is not a regular file: ${input}`, "KIMI_VIDEO_NOT_A_FILE");
17695
18183
  if (stats.size > 31457280) throw new HarnessError(`video is ${humanBytes(stats.size)}, above the ${humanBytes(MAX_VIDEO_FILE_BYTES)} limit this route sends`, "KIMI_VIDEO_TOO_LARGE");
17696
18184
  const type = mediaTypeForPath(input);
17697
18185
  if (type === void 0) throw new HarnessError(`unsupported video extension "${path.extname(input)}"; supported: .mp4 .m4v .webm .mov .avi .mpeg .mpg .flv .wmv .3gp`, "KIMI_VIDEO_TYPE_UNSUPPORTED");
17698
18186
  return {
17699
- data: await fsPromises.readFile(input),
18187
+ data: await fs.readFile(input),
17700
18188
  name: path.basename(input),
17701
18189
  type
17702
18190
  };
@@ -19177,16 +19665,16 @@ function credentialNeedsRefresh(credentials, now = Date.now()) {
19177
19665
  * leaves the destination untouched.
19178
19666
  */
19179
19667
  async function writeFileAtomic(filePath, contents) {
19180
- await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
19668
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
19181
19669
  const temporary = filePath + ".dsh-tmp-" + process.pid + "-" + Date.now();
19182
19670
  try {
19183
- await fsPromises.writeFile(temporary, contents, {
19671
+ await fs.writeFile(temporary, contents, {
19184
19672
  encoding: "utf8",
19185
19673
  mode: 384
19186
19674
  });
19187
- await fsPromises.rename(temporary, filePath);
19675
+ await fs.rename(temporary, filePath);
19188
19676
  } catch (error) {
19189
- await fsPromises.rm(temporary, { force: true }).catch(() => void 0);
19677
+ await fs.rm(temporary, { force: true }).catch(() => void 0);
19190
19678
  throw new Error("MiniMax Code credential write failed; the previous file was left untouched (" + (error instanceof Error ? error.message : String(error)) + ")");
19191
19679
  }
19192
19680
  }
@@ -19396,7 +19884,7 @@ async function readNativeDocument(region) {
19396
19884
  const filePath = authJsonPath(region);
19397
19885
  let raw;
19398
19886
  try {
19399
- raw = await fsPromises.readFile(filePath, "utf8");
19887
+ raw = await fs.readFile(filePath, "utf8");
19400
19888
  } catch {
19401
19889
  return null;
19402
19890
  }
@@ -19433,7 +19921,7 @@ async function syncAuthState(region, options) {
19433
19921
  const filePath = authStateJsonPath(region);
19434
19922
  let parsed;
19435
19923
  try {
19436
- parsed = JSON.parse(await fsPromises.readFile(filePath, "utf8"));
19924
+ parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
19437
19925
  } catch {
19438
19926
  return;
19439
19927
  }
@@ -19481,7 +19969,7 @@ var MinimaxCodeCredentialStore = class {
19481
19969
  async readPluginFile() {
19482
19970
  let raw;
19483
19971
  try {
19484
- raw = await fsPromises.readFile(pluginCredentialPath(), "utf8");
19972
+ raw = await fs.readFile(pluginCredentialPath(), "utf8");
19485
19973
  } catch {
19486
19974
  return null;
19487
19975
  }
@@ -19623,7 +20111,7 @@ var MinimaxCodeCredentialStore = class {
19623
20111
  */
19624
20112
  delete() {
19625
20113
  return serialize(pluginCredentialPath(), async () => {
19626
- await fsPromises.rm(pluginCredentialPath(), { force: true });
20114
+ await fs.rm(pluginCredentialPath(), { force: true });
19627
20115
  recentRead = null;
19628
20116
  });
19629
20117
  }
@@ -19722,17 +20210,17 @@ var MinimaxCodeModelSettingsStore = class {
19722
20210
  }
19723
20211
  async read() {
19724
20212
  try {
19725
- return parseMinimaxCodeModelSettings(JSON.parse(await fsPromises.readFile(this.filePath, "utf8")));
20213
+ return parseMinimaxCodeModelSettings(JSON.parse(await fs.readFile(this.filePath, "utf8")));
19726
20214
  } catch {
19727
20215
  return defaultMinimaxCodeSettings();
19728
20216
  }
19729
20217
  }
19730
20218
  async write(settings) {
19731
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
20219
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
19732
20220
  const temporary = this.filePath + ".tmp." + Date.now();
19733
- await fsPromises.writeFile(temporary, JSON.stringify(settings, null, 2) + "\n", "utf8");
19734
- await fsPromises.rename(temporary, this.filePath);
19735
- if (!isDeepStrictEqual(parseMinimaxCodeModelSettings(JSON.parse(await fsPromises.readFile(this.filePath, "utf8"))), settings)) throw new Error("MiniMax Code model settings write failed; the stored selection does not match what was written");
20221
+ await fs.writeFile(temporary, JSON.stringify(settings, null, 2) + "\n", "utf8");
20222
+ await fs.rename(temporary, this.filePath);
20223
+ if (!isDeepStrictEqual(parseMinimaxCodeModelSettings(JSON.parse(await fs.readFile(this.filePath, "utf8"))), settings)) throw new Error("MiniMax Code model settings write failed; the stored selection does not match what was written");
19736
20224
  }
19737
20225
  /**
19738
20226
  * Read-modify-write under the same per-path lock the credential writes use.
@@ -21552,23 +22040,30 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21552
22040
  * because that one genuinely needs a sign-in.
21553
22041
  */
21554
22042
  async healStaleAuthFailures(current) {
21555
- const data = current ?? await this.read();
21556
- if (!data.accounts.some(isMarkedFailed)) return false;
22043
+ const failedAccounts = (current ?? await this.read()).accounts.filter(isMarkedFailed);
22044
+ if (failedAccounts.length === 0) return false;
22045
+ const targets = new Map(failedAccounts.map((account) => [account.id, {
22046
+ credentials: account.credentials,
22047
+ authStatus: account.authStatus
22048
+ }]));
21557
22049
  const live = await this.store.read().catch(() => null);
21558
22050
  if (live === null) return false;
21559
- let changed = false;
21560
- for (const account of data.accounts) {
21561
- if (account.authStatus === void 0 || account.authStatus === "ok") continue;
21562
- if (!minimaxCodeSameCredentialSession(live, account.credentials)) continue;
21563
- if (!minimaxCodeCredentialAdvancedPast(live, account.credentials)) continue;
21564
- account.authStatus = void 0;
21565
- account.authFailedReason = void 0;
21566
- account.credentials = live;
21567
- changed = true;
21568
- }
21569
- if (!changed) return false;
21570
- await this.write(data).catch(() => void 0);
21571
- return true;
22051
+ return await this.updatePool((draft) => {
22052
+ let changed = false;
22053
+ for (const account of draft.accounts) {
22054
+ const expected = targets.get(account.id);
22055
+ if (!expected) continue;
22056
+ if (account.authStatus !== expected.authStatus) continue;
22057
+ if (!isDeepStrictEqual(account.credentials, expected.credentials)) continue;
22058
+ if (!minimaxCodeSameCredentialSession(live, account.credentials)) continue;
22059
+ if (!minimaxCodeCredentialAdvancedPast(live, account.credentials)) continue;
22060
+ account.authStatus = void 0;
22061
+ account.authFailedReason = void 0;
22062
+ account.credentials = live;
22063
+ changed = true;
22064
+ }
22065
+ return changed;
22066
+ });
21572
22067
  }
21573
22068
  /**
21574
22069
  * Pick the account for the next request.
@@ -21582,7 +22077,13 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21582
22077
  const effective = await this.getEffectiveAccount(excludeIds, fetchFn);
21583
22078
  const live = await this.liveNativeCredential(effective.account);
21584
22079
  if (live === null) return effective;
21585
- if (live.accessToken !== effective.credentials.accessToken) await this.updateAccountCredentials(effective.account.id, live).catch(() => void 0);
22080
+ if (live.accessToken !== effective.credentials.accessToken) {
22081
+ const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
22082
+ if (updated) return {
22083
+ account: updated,
22084
+ credentials: updated.credentials
22085
+ };
22086
+ }
21586
22087
  return {
21587
22088
  account: effective.account,
21588
22089
  credentials: live
@@ -21644,7 +22145,7 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21644
22145
  return this.addAccount(credentials, alias);
21645
22146
  }
21646
22147
  /**
21647
- * Delete one account.
22148
+ * Remove one account inside a transaction.
21648
22149
  *
21649
22150
  * An account whose credential is the desktop app's own \`auth.json\` is refused:
21650
22151
  * this plugin does not own that file, and removing the account would either
@@ -21652,9 +22153,9 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21652
22153
  * file — sign the user out of MiniMax Code itself. Signing out there is the
21653
22154
  * supported way to end it, and the card is told so by \`removable: false\`.
21654
22155
  */
21655
- async deleteAccount(accountId) {
21656
- if ((await this.read()).accounts.find((account) => account.id === accountId)?.source === "minimax-native") throw new Error("该账号来自 MiniMax Code 桌面端(~/.minimax/auth),本插件只能读取和续期,不能删除。请在 MiniMax Code 应用中退出登录。");
21657
- await super.deleteAccount(accountId);
22156
+ deleteAccountFromPool(data, accountId) {
22157
+ if (data.accounts.find((account) => account.id === accountId)?.source === "minimax-native") throw new Error("该账号来自 MiniMax Code 桌面端(~/.minimax/auth),本插件只能读取和续期,不能删除。请在 MiniMax Code 应用中退出登录。");
22158
+ super.deleteAccountFromPool(data, accountId);
21658
22159
  }
21659
22160
  /** The credential store this pool mirrors its primary account into. */
21660
22161
  mirrorStore() {
@@ -23434,7 +23935,7 @@ function isCanonicalFile(name) {
23434
23935
  async function scanCredentials(dir = codeBuddyAuthDir()) {
23435
23936
  let names;
23436
23937
  try {
23437
- names = await fsPromises.readdir(dir);
23938
+ names = await fs.readdir(dir);
23438
23939
  } catch (error) {
23439
23940
  if (error.code === "ENOENT") return [];
23440
23941
  throw new Error(`WorkBuddy credential directory could not be read: ${dir}`);
@@ -23444,9 +23945,9 @@ async function scanCredentials(dir = codeBuddyAuthDir()) {
23444
23945
  if (!name.endsWith(".info")) continue;
23445
23946
  const file = path.join(dir, name);
23446
23947
  try {
23447
- const stats = await fsPromises.stat(file);
23948
+ const stats = await fs.stat(file);
23448
23949
  if (!stats.isFile()) continue;
23449
- const raw = await fsPromises.readFile(file, "utf8");
23950
+ const raw = await fs.readFile(file, "utf8");
23450
23951
  found.push(parseCredentialFile(JSON.parse(raw), file, stats.mtimeMs));
23451
23952
  } catch {
23452
23953
  continue;
@@ -23583,7 +24084,7 @@ var FileCredentialStore$4 = class {
23583
24084
  async isStillCurrent(credentials) {
23584
24085
  if (credentials.source === "managed") return true;
23585
24086
  try {
23586
- return (await fsPromises.stat(credentials.sourceFile)).mtimeMs === credentials.sourceMtimeMs;
24087
+ return (await fs.stat(credentials.sourceFile)).mtimeMs === credentials.sourceMtimeMs;
23587
24088
  } catch {
23588
24089
  return false;
23589
24090
  }
@@ -23623,9 +24124,9 @@ var FileCredentialStore$4 = class {
23623
24124
  */
23624
24125
  async readDesktopFile(filePath) {
23625
24126
  try {
23626
- const stats = await fsPromises.stat(filePath);
24127
+ const stats = await fs.stat(filePath);
23627
24128
  if (!stats.isFile()) return null;
23628
- const raw = await fsPromises.readFile(filePath, "utf8");
24129
+ const raw = await fs.readFile(filePath, "utf8");
23629
24130
  return parseCredentialFile(JSON.parse(raw), filePath, stats.mtimeMs);
23630
24131
  } catch {
23631
24132
  return null;
@@ -23684,25 +24185,25 @@ var FileCredentialStore$4 = class {
23684
24185
  */
23685
24186
  async writeBack(credentials) {
23686
24187
  try {
23687
- const raw = await fsPromises.readFile(credentials.sourceFile, "utf8");
24188
+ const raw = await fs.readFile(credentials.sourceFile, "utf8");
23688
24189
  const parsed = JSON.parse(raw);
23689
24190
  if (!isRecord$11(parsed) || !isRecord$11(parsed.auth)) return;
23690
24191
  parsed.auth.accessToken = credentials.accessToken;
23691
24192
  if (credentials.refreshToken !== "") parsed.auth.refreshToken = credentials.refreshToken;
23692
24193
  parsed.auth.expiresAt = credentials.expiresAt;
23693
24194
  parsed.auth.lastRefreshTime = Date.now();
23694
- const sourceMode = process.platform === "win32" ? void 0 : (await fsPromises.stat(credentials.sourceFile)).mode & 511;
24195
+ const sourceMode = process.platform === "win32" ? void 0 : (await fs.stat(credentials.sourceFile)).mode & 511;
23695
24196
  const tmp = `${credentials.sourceFile}.tmp.${process.pid}`;
23696
24197
  try {
23697
- await fsPromises.writeFile(tmp, JSON.stringify(parsed, null, 2), {
24198
+ await fs.writeFile(tmp, JSON.stringify(parsed, null, 2), {
23698
24199
  encoding: "utf8",
23699
24200
  ...sourceMode === void 0 ? {} : { mode: sourceMode }
23700
24201
  });
23701
- await fsPromises.rename(tmp, credentials.sourceFile);
24202
+ await fs.rename(tmp, credentials.sourceFile);
23702
24203
  } finally {
23703
- await fsPromises.unlink(tmp).catch(() => void 0);
24204
+ await fs.unlink(tmp).catch(() => void 0);
23704
24205
  }
23705
- credentials.sourceMtimeMs = (await fsPromises.stat(credentials.sourceFile)).mtimeMs;
24206
+ credentials.sourceMtimeMs = (await fs.stat(credentials.sourceFile)).mtimeMs;
23706
24207
  } catch {}
23707
24208
  }
23708
24209
  };
@@ -23717,7 +24218,7 @@ var FileModelSettingsStore$4 = class {
23717
24218
  }
23718
24219
  async read() {
23719
24220
  try {
23720
- const content = await fsPromises.readFile(this.filePath, "utf8");
24221
+ const content = await fs.readFile(this.filePath, "utf8");
23721
24222
  const parsed = JSON.parse(content);
23722
24223
  if (isRecord$11(parsed)) {
23723
24224
  const enabledModelIds = Array.isArray(parsed.enabledModelIds) ? parsed.enabledModelIds.filter((id) => typeof id === "string") : [...DEFAULT_ENABLED_MODEL_IDS$1];
@@ -23745,10 +24246,10 @@ var FileModelSettingsStore$4 = class {
23745
24246
  };
23746
24247
  }
23747
24248
  async write(settings) {
23748
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
24249
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
23749
24250
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
23750
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
23751
- await fsPromises.rename(tmp, this.filePath);
24251
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
24252
+ await fs.rename(tmp, this.filePath);
23752
24253
  }
23753
24254
  async updateSettings(patch) {
23754
24255
  const current = await this.read();
@@ -26157,15 +26658,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26157
26658
  this.options_selection = options.selection;
26158
26659
  }
26159
26660
  /**
26160
- * Read the pool, refreshing the legacy-key index on the way.
26661
+ * Load the pool, refreshing the legacy-key index on the way.
26161
26662
  *
26162
- * Every routing decision goes through this method before it consults the
26163
- * settings selection, so building the index here is what keeps a pin written
26164
- * before the identity fix resolvable when {@link resolveSelectionId} is asked
26165
- * for it.
26663
+ * Every routing decision and mutation goes through this method before it
26664
+ * consults the settings selection, so building the index here is what keeps
26665
+ * a pin written before the identity fix resolvable when
26666
+ * {@link resolveSelectionId} is asked for it.
26166
26667
  */
26167
- async read() {
26168
- const data = await super.read();
26668
+ async loadPoolData() {
26669
+ const data = await super.loadPoolData();
26169
26670
  this.legacyIds.clear();
26170
26671
  for (const account of data.accounts) for (const alias of workBuddyAccountIdAliases(account.credentials)) if (alias !== account.id) this.legacyIds.set(alias, account.id);
26171
26672
  return data;
@@ -26185,20 +26686,34 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26185
26686
  */
26186
26687
  async syncDesktopAccounts() {
26187
26688
  const discovered = await this.store.discoverDesktopCredentials();
26188
- if (discovered.length > 0) {
26189
- const data = await this.read();
26190
- const known = new Set(data.accounts.map((account) => workBuddyAccountId(account.credentials)));
26191
- const seen = /* @__PURE__ */ new Set();
26192
- for (const credentials of discovered) {
26193
- const id = workBuddyAccountId(credentials);
26194
- if (known.has(id) || seen.has(id)) continue;
26195
- seen.add(id);
26196
- await this.addAccount({
26197
- ...credentials,
26198
- source: "desktop"
26199
- });
26689
+ const candidates = [];
26690
+ const seen = /* @__PURE__ */ new Set();
26691
+ for (const raw of discovered) {
26692
+ const credentials = withResolvedIdentity({
26693
+ ...raw,
26694
+ source: "desktop"
26695
+ });
26696
+ const id = workBuddyAccountId(credentials);
26697
+ if (seen.has(id)) continue;
26698
+ seen.add(id);
26699
+ candidates.push(credentials);
26700
+ }
26701
+ if (candidates.length > 0) await this.updatePool((data) => {
26702
+ const existingKeys = /* @__PURE__ */ new Set();
26703
+ for (const account of data.accounts) {
26704
+ existingKeys.add(account.id);
26705
+ const key = this.hooks.dedupeKey?.(account.credentials);
26706
+ if (key !== void 0) existingKeys.add(key);
26200
26707
  }
26201
- }
26708
+ for (const credentials of candidates) {
26709
+ const key = this.hooks.dedupeKey?.(credentials);
26710
+ const id = this.hooks.accountId?.(credentials);
26711
+ if (key !== void 0 && existingKeys.has(key) || id !== void 0 && existingKeys.has(id)) continue;
26712
+ this.addAccountToPool(data, credentials);
26713
+ if (key !== void 0) existingKeys.add(key);
26714
+ if (id !== void 0) existingKeys.add(id);
26715
+ }
26716
+ });
26202
26717
  return this.listAccounts();
26203
26718
  }
26204
26719
  /**
@@ -26214,7 +26729,13 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26214
26729
  const live = await this.store.readDesktopFile(effective.account.sourceFile);
26215
26730
  if (live === null) return effective;
26216
26731
  if (isExpired(live)) return effective;
26217
- if (live.accessToken !== effective.credentials.accessToken) await this.updateAccountCredentials(effective.account.id, live).catch(() => void 0);
26732
+ if (live.accessToken !== effective.credentials.accessToken) {
26733
+ const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
26734
+ if (updated) return {
26735
+ account: updated,
26736
+ credentials: updated.credentials
26737
+ };
26738
+ }
26218
26739
  return {
26219
26740
  account: effective.account,
26220
26741
  credentials: live
@@ -26251,15 +26772,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26251
26772
  return super.addAccount(withResolvedIdentity(credentials), alias);
26252
26773
  }
26253
26774
  /**
26254
- * Delete one account.
26775
+ * Remove one account inside a transaction.
26255
26776
  *
26256
26777
  * A desktop account is refused: the plugin does not own that credential file,
26257
26778
  * so removing it from the pool would strand the IDE's session while telling
26258
26779
  * the user their account was deleted. Hiding is the supported action there.
26259
26780
  */
26260
- async deleteAccount(accountId) {
26261
- if ((await this.read()).accounts.find((account) => account.id === accountId)?.source === "desktop") throw new Error("桌面账号由 CodeBuddy 客户端所有,只能隐藏,不能删除。");
26262
- await super.deleteAccount(accountId);
26781
+ deleteAccountFromPool(data, accountId) {
26782
+ if (data.accounts.find((account) => account.id === accountId)?.source === "desktop") throw new Error("桌面账号由 CodeBuddy 客户端所有,只能隐藏,不能删除。");
26783
+ super.deleteAccountFromPool(data, accountId);
26263
26784
  }
26264
26785
  };
26265
26786
  //#endregion
@@ -26352,6 +26873,25 @@ const CLAUDE_CODE_BETA = "claude-code-20250219";
26352
26873
  /** Lets thinking blocks interleave with tool use across a multi-turn loop. */
26353
26874
  const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
26354
26875
  /**
26876
+ * Licenses the extended one-hour prompt-cache TTL.
26877
+ *
26878
+ * `cache_control: { type: 'ephemeral', ttl: '1h' }` is NOT accepted without this
26879
+ * beta: the one-hour tier is a separate licensed capability, and a body that
26880
+ * asks for it without the marker is refused the same way `block_binding` is.
26881
+ */
26882
+ const EXTENDED_CACHE_TTL_BETA = "extended-cache-ttl-2025-04-11";
26883
+ /** A cache marker for one tier. Absent `ttl` means the five-minute default. */
26884
+ function cacheControlFor(ttl) {
26885
+ return {
26886
+ type: "ephemeral",
26887
+ ttl
26888
+ };
26889
+ }
26890
+ /** Whether a stored preference names a tier this line can send. */
26891
+ function isClaudeCacheTtl$1(value) {
26892
+ return value === "5m" || value === "1h";
26893
+ }
26894
+ /**
26355
26895
  * Licenses `thinking.block_binding` in a request body.
26356
26896
  *
26357
26897
  * The field is not optional to license: the vendor documents that a body
@@ -27320,6 +27860,7 @@ function defaultClaudeSettings() {
27320
27860
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS],
27321
27861
  contextWindowOverrides: {},
27322
27862
  defaultReasoningEffort: null,
27863
+ cacheTtl: null,
27323
27864
  selectedAccountId: null
27324
27865
  };
27325
27866
  }
@@ -27555,12 +28096,14 @@ function parseClaudeModelSettings(value) {
27555
28096
  for (const [model, window] of Object.entries(record.contextWindowOverrides)) if (typeof window === "number" && Number.isFinite(window) && window > 0) contextWindowOverrides[model] = window;
27556
28097
  }
27557
28098
  const defaultReasoningEffort = nonEmptyString$3(record.defaultReasoningEffort) ?? null;
28099
+ const cacheTtl = isClaudeCacheTtl$1(record.cacheTtl) ? record.cacheTtl : null;
27558
28100
  const selectedAccountId = nonEmptyString$3(record.selectedAccountId) ?? null;
27559
28101
  return {
27560
28102
  enabled: record.enabled !== false,
27561
28103
  enabledModelIds,
27562
28104
  contextWindowOverrides,
27563
28105
  defaultReasoningEffort,
28106
+ cacheTtl,
27564
28107
  selectedAccountId
27565
28108
  };
27566
28109
  }
@@ -27571,6 +28114,7 @@ function mergeClaudeSettings(current, patch) {
27571
28114
  enabledModelIds: patch.enabledModelIds !== void 0 ? [...patch.enabledModelIds] : current.enabledModelIds,
27572
28115
  contextWindowOverrides: patch.contextWindowOverrides !== void 0 ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
27573
28116
  defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort,
28117
+ cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl,
27574
28118
  selectedAccountId: patch.selectedAccountId !== void 0 ? patch.selectedAccountId : current.selectedAccountId
27575
28119
  };
27576
28120
  }
@@ -27643,7 +28187,7 @@ var FileCredentialStore = class {
27643
28187
  }
27644
28188
  async removeLegacy() {
27645
28189
  try {
27646
- await fsPromises.unlink(this.filePath);
28190
+ await fs.unlink(this.filePath);
27647
28191
  } catch (error) {
27648
28192
  if (error.code !== "ENOENT") throw new Error("Claude legacy credential removal failed");
27649
28193
  }
@@ -27664,11 +28208,11 @@ var FileCredentialStore = class {
27664
28208
  }
27665
28209
  let legacy;
27666
28210
  try {
27667
- const stats = await fsPromises.lstat(this.filePath);
28211
+ const stats = await fs.lstat(this.filePath);
27668
28212
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
27669
28213
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
27670
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
27671
- legacy = await fsPromises.readFile(this.filePath, "utf8");
28214
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
28215
+ legacy = await fs.readFile(this.filePath, "utf8");
27672
28216
  } catch (error) {
27673
28217
  if (error.code === "ENOENT") return null;
27674
28218
  throw new Error("Claude legacy credential read failed");
@@ -27776,17 +28320,17 @@ var FileModelSettingsStore = class {
27776
28320
  }
27777
28321
  async read() {
27778
28322
  try {
27779
- const content = await fsPromises.readFile(this.filePath, "utf8");
28323
+ const content = await fs.readFile(this.filePath, "utf8");
27780
28324
  return parseClaudeModelSettings(JSON.parse(content));
27781
28325
  } catch {
27782
28326
  return defaultClaudeSettings();
27783
28327
  }
27784
28328
  }
27785
28329
  async write(settings) {
27786
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
28330
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
27787
28331
  const tmp = this.filePath + ".tmp." + Date.now();
27788
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
27789
- await fsPromises.rename(tmp, this.filePath);
28332
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
28333
+ await fs.rename(tmp, this.filePath);
27790
28334
  }
27791
28335
  /**
27792
28336
  * Read-modify-write under the same per-path lock the credentials use.
@@ -27828,6 +28372,11 @@ function registerClaudePreferenceStore(settings, fallbackStore = new FileModelSe
27828
28372
  enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS]),
27829
28373
  contextWindowOverrides: z.dict(z.number()).default({}),
27830
28374
  defaultReasoningEffort: z.union([z.string(), z.const(null)]).default(null),
28375
+ cacheTtl: z.union([
28376
+ z.const("5m"),
28377
+ z.const("1h"),
28378
+ z.const(null)
28379
+ ]).default(null),
27831
28380
  selectedAccountId: z.union([z.string(), z.const(null)]).default(null)
27832
28381
  }));
27833
28382
  return {
@@ -27838,6 +28387,7 @@ function registerClaudePreferenceStore(settings, fallbackStore = new FileModelSe
27838
28387
  enabledModelIds: value.enabledModelIds,
27839
28388
  contextWindowOverrides: value.contextWindowOverrides,
27840
28389
  defaultReasoningEffort: value.defaultReasoningEffort,
28390
+ cacheTtl: isClaudeCacheTtl$1(value.cacheTtl) ? value.cacheTtl : null,
27841
28391
  selectedAccountId: value.selectedAccountId ?? null
27842
28392
  };
27843
28393
  },
@@ -28033,6 +28583,7 @@ function claudeBetas(options = {}) {
28033
28583
  betas.push(OAUTH_BETA);
28034
28584
  if (options.thinking === true) betas.push(INTERLEAVED_THINKING_BETA);
28035
28585
  if (options.thinkingBinding === true) betas.push(THINKING_BINDING_CONTROLS_BETA);
28586
+ if (options.cacheTtl === "1h") betas.push(EXTENDED_CACHE_TTL_BETA);
28036
28587
  return betas;
28037
28588
  }
28038
28589
  /** The `User-Agent` value this line reports for one surface. */
@@ -29630,7 +30181,7 @@ function leadingSystemText(options) {
29630
30181
  *
29631
30182
  * @param cacheControl - false is the opt-OUT; the default requests caching.
29632
30183
  */
29633
- function buildClaudeSystemBlocks(options, cacheControl = true) {
30184
+ function buildClaudeSystemBlocks(options, cacheControl = true, ttl = "5m") {
29634
30185
  const blocks = [{
29635
30186
  type: "text",
29636
30187
  text: CLAUDE_CODE_IDENTITY_TEXT
@@ -29642,7 +30193,7 @@ function buildClaudeSystemBlocks(options, cacheControl = true) {
29642
30193
  });
29643
30194
  if (cacheControl) {
29644
30195
  const last = blocks[blocks.length - 1];
29645
- last.cache_control = { type: "ephemeral" };
30196
+ last.cache_control = cacheControlFor(ttl);
29646
30197
  }
29647
30198
  return blocks;
29648
30199
  }
@@ -29680,13 +30231,14 @@ function lastCacheableUserBlock(messages) {
29680
30231
  * and has not been handed to a caller yet.
29681
30232
  * @returns the number of breakpoints written.
29682
30233
  */
29683
- function markClaudeCacheBreakpoints(body) {
30234
+ function markClaudeCacheBreakpoints(body, ttl = "5m") {
29684
30235
  let marked = 0;
30236
+ const marker = cacheControlFor(ttl);
29685
30237
  const system = body.system;
29686
30238
  if (Array.isArray(system)) {
29687
30239
  const last = system[system.length - 1];
29688
30240
  if (last !== void 0) {
29689
- last.cache_control = { type: "ephemeral" };
30241
+ last.cache_control = { ...marker };
29690
30242
  marked += 1;
29691
30243
  }
29692
30244
  }
@@ -29694,7 +30246,7 @@ function markClaudeCacheBreakpoints(body) {
29694
30246
  if (Array.isArray(messages)) {
29695
30247
  const target = lastCacheableUserBlock(messages);
29696
30248
  if (target !== void 0) {
29697
- target.cache_control = { type: "ephemeral" };
30249
+ target.cache_control = { ...marker };
29698
30250
  marked += 1;
29699
30251
  }
29700
30252
  }
@@ -29702,7 +30254,7 @@ function markClaudeCacheBreakpoints(body) {
29702
30254
  if (Array.isArray(tools) && tools.length > 0) {
29703
30255
  const last = tools[tools.length - 1];
29704
30256
  if (last !== void 0) {
29705
- last.cache_control = { type: "ephemeral" };
30257
+ last.cache_control = { ...marker };
29706
30258
  marked += 1;
29707
30259
  }
29708
30260
  }
@@ -29936,11 +30488,12 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
29936
30488
  const thinkingEnabled = thinking !== void 0 && thinking.type !== "disabled";
29937
30489
  const temperatureAllowed = claudeModelSupportsTemperature(options.model) && !thinkingEnabled;
29938
30490
  const caching = request.cacheControl !== false;
30491
+ const cacheTtl = request.cacheTtl ?? "5m";
29939
30492
  const body = {
29940
30493
  model: options.model,
29941
30494
  max_tokens: maxTokens,
29942
30495
  stream: true,
29943
- system: buildClaudeSystemBlocks(options, caching),
30496
+ system: buildClaudeSystemBlocks(options, caching, cacheTtl),
29944
30497
  messages: mergeClaudeMessages(entries),
29945
30498
  ...options.tools !== void 0 && options.tools.length > 0 ? { tools: options.tools.map((tool) => wireTool(tool, names)) } : {},
29946
30499
  ...thinking === void 0 ? {} : { thinking },
@@ -29948,7 +30501,7 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
29948
30501
  ...temperatureAllowed && options.temperature !== void 0 ? { temperature: options.temperature } : {},
29949
30502
  ...options.stop !== void 0 && options.stop.length > 0 ? { stop_sequences: [...options.stop] } : {}
29950
30503
  };
29951
- if (caching) markClaudeCacheBreakpoints(body);
30504
+ if (caching) markClaudeCacheBreakpoints(body, cacheTtl);
29952
30505
  return body;
29953
30506
  }
29954
30507
  /**
@@ -31730,7 +32283,7 @@ var ClaudeAccountTokenStore = class {
31730
32283
  await this.store.saveAccount(credentials, { internalId: this.internalId });
31731
32284
  }
31732
32285
  };
31733
- var ClaudeAdapter = class extends LlmAdapter {
32286
+ var ClaudeAdapter = class ClaudeAdapter extends LlmAdapter {
31734
32287
  store;
31735
32288
  modelSettings;
31736
32289
  preferences;
@@ -31912,10 +32465,25 @@ var ClaudeAdapter = class extends LlmAdapter {
31912
32465
  * driven by {@link shouldRotateAccount}, which bypasses the pool ENTIRELY when
31913
32466
  * no credentials were resolved for it.
31914
32467
  */
31915
- async attemptRequest(credentials, requestOptions, images, toolNames, thinking, signal, fetchFn) {
32468
+ /**
32469
+ * The prompt-cache tier this line requests.
32470
+ *
32471
+ * A stored value wins; nothing stored falls back to the tier the OFFICIAL
32472
+ * client uses for a subscription's main conversation, so a user comparing
32473
+ * this plugin with Claude Code sees the same cache lifetime rather than half
32474
+ * of it.
32475
+ *
32476
+ * @param settings - the document the caller already read for this turn.
32477
+ */
32478
+ static cacheTtlFor(settings) {
32479
+ return settings?.cacheTtl ?? "1h";
32480
+ }
32481
+ async attemptRequest(credentials, requestOptions, images, toolNames, thinking, settings, signal, fetchFn) {
32482
+ const cacheTtl = ClaudeAdapter.cacheTtlFor(settings);
31916
32483
  const payload = buildClaudeRequestBody(requestOptions, images, {
31917
32484
  toolNames,
31918
- cacheControl: true
32485
+ cacheControl: true,
32486
+ cacheTtl
31919
32487
  });
31920
32488
  try {
31921
32489
  return await fetchFn("https://api.anthropic.com/v1/messages?beta=true", {
@@ -31924,6 +32492,7 @@ var ClaudeAdapter = class extends LlmAdapter {
31924
32492
  model: requestOptions.model,
31925
32493
  thinking,
31926
32494
  thinkingBinding: claudeBodyBindsThinking(payload),
32495
+ cacheTtl,
31927
32496
  method: "POST"
31928
32497
  }),
31929
32498
  body: JSON.stringify(payload),
@@ -31942,6 +32511,7 @@ var ClaudeAdapter = class extends LlmAdapter {
31942
32511
  assertClaudeCliVersionMeetsFloor(requestOptions.model, await this.catalogForRequest());
31943
32512
  const effort = requestOptions.reasoningEffort === void 0 || requestOptions.reasoningEffort === null ? void 0 : String(requestOptions.reasoningEffort);
31944
32513
  const thinking = claudeRequestThinks(requestOptions.model, effort);
32514
+ const settings = await this.settings().catch(() => null);
31945
32515
  const pool = this.accountPool;
31946
32516
  const tried = /* @__PURE__ */ new Set();
31947
32517
  let response;
@@ -31961,7 +32531,7 @@ var ClaudeAdapter = class extends LlmAdapter {
31961
32531
  let outputStarted = false;
31962
32532
  while (true) {
31963
32533
  const { credentials, accountId } = await this.resolveCredential(pool, tried, fetchFn, signal);
31964
- response = await this.attemptRequest(credentials, requestOptions, images, toolNames, thinking, signal, fetchFn);
32534
+ response = await this.attemptRequest(credentials, requestOptions, images, toolNames, thinking, settings, signal, fetchFn);
31965
32535
  if (response.ok) break;
31966
32536
  const detail = (await response.text().catch(() => "")).slice(0, 2e3);
31967
32537
  const failure = classifyFailure(response.status, detail, response.headers);
@@ -32577,7 +33147,7 @@ function isAdoptedCredentialExpired(credential, now = Date.now()) {
32577
33147
  * caller in the process, so with a pool it would hand the second account the
32578
33148
  * first account's freshly rotated credential — and, through the adapter above,
32579
33149
  * write it into the second account's record. The pool therefore keeps its OWN
32580
- * single-flight, per account (see the inFlight map), which preserves the property
33150
+ * single-flight, per pool file and account, which preserves the property
32581
33151
  * the frozen module wanted (one rotation per expiry, no two callers spending the
32582
33152
  * same rotating token) without the cross-account contamination.
32583
33153
  *
@@ -32963,7 +33533,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
32963
33533
  * marked expired immediately after a refresh that in fact succeeded. Keyed by
32964
33534
  * the record's id — never by a token — so it cannot mix two accounts up.
32965
33535
  */
32966
- inFlight = /* @__PURE__ */ new Map();
32967
33536
  constructor(options = {}) {
32968
33537
  const store = options.store ?? new FileCredentialStore();
32969
33538
  const refreshSeam = options.refreshCredentials ?? ((credentials, fetchFn) => refreshClaudeCredential(credentials, fetchFn));
@@ -33010,6 +33579,10 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33010
33579
  if (record === void 0) return null;
33011
33580
  return poolAccountForRecord(record, true);
33012
33581
  },
33582
+ credentialsCommitted: async (account) => {
33583
+ if (isAdoptedPoolCredential(account.credentials)) return;
33584
+ await store.saveAccount(account.credentials, { internalId: account.id });
33585
+ },
33013
33586
  mirrorPrimary: async (credentials) => {
33014
33587
  if (credentials === null) {
33015
33588
  await store.delete();
@@ -33071,8 +33644,8 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33071
33644
  * {@link recordIdsFor} reads is kept in step with the rows that actually exist.
33072
33645
  * See the recordIds field for why entries are not pruned.
33073
33646
  */
33074
- async read() {
33075
- const data = await super.read();
33647
+ async loadPoolData() {
33648
+ const data = await super.loadPoolData();
33076
33649
  for (const account of data.accounts) this.indexAccount(account);
33077
33650
  return data;
33078
33651
  }
@@ -33085,10 +33658,27 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33085
33658
  * 'one account, one key' section.
33086
33659
  */
33087
33660
  async addAccount(credentials, alias) {
33088
- const data = await this.read();
33661
+ return this.updatePool((data) => this.addManagedAccount(data, credentials, alias)).catch((error) => {
33662
+ this.recordIds.clear();
33663
+ throw error;
33664
+ });
33665
+ }
33666
+ async addManagedAccount(data, credentials, alias) {
33089
33667
  if (isAdoptedPoolCredential(credentials)) return this.addAdoptedAccount(credentials, data, alias);
33668
+ parseClaudeCredentials(credentials);
33090
33669
  const knownId = await this.recordIdFor(credentials);
33091
- const record = await this.store.saveAccount(credentials);
33670
+ const document = await this.store.read();
33671
+ const keys = identityKeysFor(credentials);
33672
+ const stored = document?.accounts.find((record) => record.identityKeys.some((key) => keys.includes(key)));
33673
+ const record = stored === void 0 ? {
33674
+ internalId: data.accounts.find((account) => account.id === knownId)?.adopted === false ? knownId ?? createInternalId() : createInternalId(),
33675
+ identityKeys: keys,
33676
+ credentials
33677
+ } : {
33678
+ ...stored,
33679
+ identityKeys: mergeIdentityKeys(stored.identityKeys, keys),
33680
+ credentials
33681
+ };
33092
33682
  this.indexRecord(record);
33093
33683
  if (knownId !== void 0 && knownId !== record.internalId) {
33094
33684
  const row = data.accounts.find((account) => account.id === knownId);
@@ -33100,10 +33690,9 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33100
33690
  delete row.sourcePath;
33101
33691
  row.identityKeys = mergeIdentityKeys(row.identityKeys, record.identityKeys);
33102
33692
  if (data.activeAccountId === knownId) data.activeAccountId = record.internalId;
33103
- await this.write(data);
33104
33693
  }
33105
33694
  }
33106
- return super.addAccount(credentials, alias);
33695
+ return this.addAccountToPool(data, credentials, alias);
33107
33696
  }
33108
33697
  /**
33109
33698
  * Add, or re-import, one borrowed Claude Code snapshot.
@@ -33132,9 +33721,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33132
33721
  const candidateId = byIdentity ?? this.recordIds.get(sourceKey);
33133
33722
  const existing = candidateId === void 0 ? void 0 : data.accounts.find((account) => account.id === candidateId);
33134
33723
  if (existing !== void 0 && existing.adopted !== true) {
33135
- const before = existing.identityKeys.length;
33136
33724
  existing.identityKeys = mergeIdentityKeys(existing.identityKeys, identityKeysFor(credentials));
33137
- if (existing.identityKeys.length !== before) await this.write(data);
33138
33725
  return existing;
33139
33726
  }
33140
33727
  if (candidateId === void 0) {
@@ -33145,7 +33732,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33145
33732
  this.recordIds.set(sourceKey, candidateId);
33146
33733
  this.indexCredential(credentials, candidateId);
33147
33734
  }
33148
- return super.addAccount(credentials, alias);
33735
+ return this.addAccountToPool(data, credentials, alias);
33149
33736
  }
33150
33737
  /**
33151
33738
  * Merge two rows that are one account.
@@ -33168,7 +33755,21 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33168
33755
  */
33169
33756
  async mergeAccounts(keepAccountId, dropAccountId) {
33170
33757
  if (keepAccountId === dropAccountId) throw new Error("A Claude pool merge needs two different accounts.");
33171
- const data = await this.read();
33758
+ let originals;
33759
+ return await this.updatePool(async (data) => {
33760
+ const keep = data.accounts.find((account) => account.id === keepAccountId);
33761
+ const drop = data.accounts.find((account) => account.id === dropAccountId);
33762
+ if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
33763
+ originals = {
33764
+ keep,
33765
+ drop
33766
+ };
33767
+ return this.mergeAccountsInPool(data, keepAccountId, dropAccountId);
33768
+ }, async (_data, result) => {
33769
+ if (originals !== void 0) await this.reconcileMergedDocument(originals.keep, originals.drop, result);
33770
+ });
33771
+ }
33772
+ async mergeAccountsInPool(data, keepAccountId, dropAccountId) {
33172
33773
  const keep = data.accounts.find((account) => account.id === keepAccountId);
33173
33774
  const drop = data.accounts.find((account) => account.id === dropAccountId);
33174
33775
  if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
@@ -33217,9 +33818,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33217
33818
  data.accounts[index] = merged;
33218
33819
  data.accounts = data.accounts.filter((account) => account.id !== dropAccountId);
33219
33820
  if (data.activeAccountId === dropAccountId) data.activeAccountId = merged.id;
33220
- await this.write(data);
33221
- if (merged.isPrimary) this.hooks.mirrorPrimary?.(merged.credentials).catch(() => void 0);
33222
- await this.reconcileMergedDocument(keep, drop, merged);
33223
33821
  return merged;
33224
33822
  }
33225
33823
  /**
@@ -33259,10 +33857,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33259
33857
  * neither path.
33260
33858
  */
33261
33859
  async removeImportedAccount(accountId) {
33262
- const target = (await this.read()).accounts.find((account) => account.id === accountId);
33263
- if (target === void 0) return;
33264
- if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
33265
- await super.deleteAccount(accountId);
33860
+ return this.updatePool((data) => {
33861
+ const target = data.accounts.find((account) => account.id === accountId);
33862
+ if (target === void 0) return;
33863
+ if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
33864
+ super.deleteAccountFromPool(data, accountId);
33865
+ });
33266
33866
  }
33267
33867
  /**
33268
33868
  * Delete one plugin-owned account.
@@ -33275,9 +33875,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33275
33875
  * back a credential the user just deleted.
33276
33876
  */
33277
33877
  async deleteAccount(accountId) {
33278
- if ((await this.read()).accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from Claude Code is not an account this plugin owns; remove the import instead.");
33279
- await super.deleteAccount(accountId);
33280
- await this.store.deleteAccount(accountId);
33878
+ return this.updatePool((data) => {
33879
+ if (data.accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from Claude Code is not an account this plugin owns; remove the import instead.");
33880
+ super.deleteAccountFromPool(data, accountId);
33881
+ }, async () => {
33882
+ await this.store.deleteAccount(accountId);
33883
+ });
33281
33884
  }
33282
33885
  /**
33283
33886
  * The immutable id of the record that owns a credential, or nothing.
@@ -33357,44 +33960,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33357
33960
  */
33358
33961
  refreshCredentials(credentials, fetchFn) {
33359
33962
  if (isAdoptedPoolCredential(credentials)) return Promise.reject(new ClaudeAdoptedCredentialError(adoptedCredentialExpired(credentials) ? ADOPTED_CREDENTIAL_EXPIRED_HINT : ADOPTED_NEVER_REFRESHED_MESSAGE));
33360
- return this.singleFlightRefresh(credentials, fetchFn);
33361
- }
33362
- /**
33363
- * The per-account single-flight, resolving the account's id first.
33364
- *
33365
- * Separated from the hook above only because resolving the id consults the
33366
- * credential document, and a hook cannot await: the guard stays synchronous so
33367
- * it can never be skipped by a caller that forgets to wait.
33368
- */
33369
- async singleFlightRefresh(credentials, fetchFn) {
33370
- const key = await this.recordIdFor(credentials);
33371
- if (key !== void 0) {
33372
- const shared = this.inFlight.get(key);
33373
- if (shared !== void 0) return shared;
33374
- }
33375
- const pending = this.performRefresh(credentials, fetchFn);
33376
- if (key !== void 0) {
33377
- this.inFlight.set(key, pending);
33378
- pending.then(() => void 0, () => void 0).then(() => {
33379
- if (this.inFlight.get(key) === pending) this.inFlight.delete(key);
33380
- });
33381
- }
33382
- return pending;
33383
- }
33384
- /**
33385
- * Rotate one managed credential and keep the credential document in step.
33386
- *
33387
- * The document write is what stops the pre-pool projection from handing out a
33388
- * token the rotation has already spent. Its failure is swallowed on purpose:
33389
- * the credential WAS refreshed, the pool's own record holds it, and a mirror
33390
- * that could not be written must not fail a request that can still be served —
33391
- * the same rule the core applies to its own bookkeeping writes.
33392
- */
33393
- async performRefresh(credentials, fetchFn) {
33394
- const recordId = await this.recordIdFor(credentials);
33395
- const refreshed = await this.refreshSeam(credentials, fetchFn);
33396
- if (recordId !== void 0) await new ClaudeCredentialStoreAdapter(this.store, recordId).write(refreshed).catch(() => void 0);
33397
- return refreshed;
33963
+ return this.refreshSeam(credentials, fetchFn);
33398
33964
  }
33399
33965
  };
33400
33966
  //#endregion
@@ -33435,6 +34001,16 @@ const CLAUDE_REASONING_EFFORTS = [
33435
34001
  function isClaudeReasoningEffort(value) {
33436
34002
  return typeof value === "string" && CLAUDE_REASONING_EFFORTS.includes(value);
33437
34003
  }
34004
+ /**
34005
+ * Whether a posted value names a tier this route can send.
34006
+ *
34007
+ * Membership is checked at the wire boundary for the same reason effort levels
34008
+ * are: the one-hour tier is a LICENSED capability, so a value that reached the
34009
+ * body without the matching beta would be refused by the service.
34010
+ */
34011
+ function isClaudeCacheTtl(value) {
34012
+ return value === "5m" || value === "1h";
34013
+ }
33438
34014
  //#endregion
33439
34015
  //#region src/host/claude/routes.ts
33440
34016
  /** Ceiling on one request body. Identical to every sibling line's. */
@@ -33626,6 +34202,7 @@ async function getClaudeWebStatus(store, modelSettings, preferences, options = {
33626
34202
  models,
33627
34203
  contextWindowOverrides: settings.contextWindowOverrides,
33628
34204
  defaultReasoningEffort: settings.defaultReasoningEffort,
34205
+ cacheTtl: settings.cacheTtl,
33629
34206
  selectedAccountId: settings.selectedAccountId,
33630
34207
  accounts: poolAccounts.map((summary) => summary),
33631
34208
  rotationStrategy: poolData?.rotationStrategy ?? "sequential",
@@ -33904,6 +34481,10 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
33904
34481
  const effort = body.defaultReasoningEffort;
33905
34482
  if (effort === null || isClaudeReasoningEffort(effort)) patch.defaultReasoningEffort = effort;
33906
34483
  }
34484
+ if (body.cacheTtl !== void 0) {
34485
+ const ttl = body.cacheTtl;
34486
+ if (ttl === null || isClaudeCacheTtl(ttl)) patch.cacheTtl = ttl;
34487
+ }
33907
34488
  if (body.selectedAccountId !== void 0) {
33908
34489
  const selected = body.selectedAccountId;
33909
34490
  if (selected !== null && typeof selected !== "string") return sendJson(response, 400, {
@@ -34439,7 +35020,7 @@ var WorkBuddyCheckinService = class {
34439
35020
  if (this.loaded) return;
34440
35021
  this.loaded = true;
34441
35022
  try {
34442
- const parsed = JSON.parse(await fsPromises.readFile(this.statePath, "utf8"));
35023
+ const parsed = JSON.parse(await fs.readFile(this.statePath, "utf8"));
34443
35024
  if (isRecord$2(parsed)) {
34444
35025
  const lastRunAt = asCount$1(parsed.lastRunAt);
34445
35026
  if (lastRunAt !== void 0) this.lastRunAt = lastRunAt;
@@ -34452,15 +35033,15 @@ var WorkBuddyCheckinService = class {
34452
35033
  }
34453
35034
  async save() {
34454
35035
  try {
34455
- await fsPromises.mkdir(path.dirname(this.statePath), { recursive: true });
35036
+ await fs.mkdir(path.dirname(this.statePath), { recursive: true });
34456
35037
  const tmp = `${this.statePath}.tmp.${process.pid}`;
34457
35038
  const document = {
34458
35039
  version: 2,
34459
35040
  lastRunAt: this.lastRunAt,
34460
35041
  accounts: this.state
34461
35042
  };
34462
- await fsPromises.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
34463
- await fsPromises.rename(tmp, this.statePath);
35043
+ await fs.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
35044
+ await fs.rename(tmp, this.statePath);
34464
35045
  } catch {}
34465
35046
  }
34466
35047
  async post(url, headers) {
@@ -35108,7 +35689,7 @@ var MinimaxCodeCheckinService = class {
35108
35689
  if (this.loaded) return;
35109
35690
  this.loaded = true;
35110
35691
  try {
35111
- const parsed = JSON.parse(await fsPromises.readFile(this.statePath, "utf8"));
35692
+ const parsed = JSON.parse(await fs.readFile(this.statePath, "utf8"));
35112
35693
  if (isRecord(parsed)) {
35113
35694
  const lastRunAt = asCount(parsed.lastRunAt);
35114
35695
  if (lastRunAt !== void 0) this.lastRunAt = lastRunAt;
@@ -35127,7 +35708,7 @@ var MinimaxCodeCheckinService = class {
35127
35708
  }
35128
35709
  async save() {
35129
35710
  try {
35130
- await fsPromises.mkdir(path.dirname(this.statePath), { recursive: true });
35711
+ await fs.mkdir(path.dirname(this.statePath), { recursive: true });
35131
35712
  const tmp = `${this.statePath}.tmp.${process.pid}`;
35132
35713
  const document = {
35133
35714
  version: 1,
@@ -35135,8 +35716,8 @@ var MinimaxCodeCheckinService = class {
35135
35716
  accounts: this.state,
35136
35717
  identities: this.identities
35137
35718
  };
35138
- await fsPromises.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
35139
- await fsPromises.rename(tmp, this.statePath);
35719
+ await fs.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
35720
+ await fs.rename(tmp, this.statePath);
35140
35721
  } catch {}
35141
35722
  }
35142
35723
  };
@@ -37112,9 +37693,9 @@ function appendWindow(current, addition, limit) {
37112
37693
  /**
37113
37694
  * Install the reasoning-collapse guard on a Context.
37114
37695
  *
37115
- * The guard is inert unless the host exposes the `llm/stream` seam, so a
37116
- * harness without it keeps the built-in behaviour instead of failing this
37117
- * plugin's load.
37696
+ * The guard is inert unless the host exposes the event bus, so a harness
37697
+ * without it keeps the built-in behaviour instead of failing this plugin's
37698
+ * load.
37118
37699
  *
37119
37700
  * Resuming needs the live Agent for `cancel` and `steer`, and no supported
37120
37701
  * generation hands the agent to `llm/stream`. `agent/created` reports it on
@@ -37124,19 +37705,19 @@ function appendWindow(current, addition, limit) {
37124
37705
  *
37125
37706
  * @param ctx - plugin context.
37126
37707
  * @param options - see {@link GuardOptions}; validated fail-loud.
37127
- * @returns a disposer, or undefined when the seam is unavailable.
37708
+ * @returns a disposer removing both listeners, or undefined when the host
37709
+ * exposes no event bus.
37128
37710
  */
37129
37711
  function installReasoningCollapseGuard(ctx, options = {}) {
37130
37712
  const resolved = resolveGuardOptions(options);
37131
- const runtime = ctx.llm;
37132
- if (runtime === void 0 || typeof runtime.stream !== "function") return void 0;
37133
- const original = runtime.stream;
37713
+ if (typeof ctx.on !== "function") return void 0;
37134
37714
  const watched = new Set(resolved.includeModels);
37135
37715
  const breakers = /* @__PURE__ */ new WeakMap();
37136
37716
  let currentAgent;
37137
- const releaseAgent = typeof ctx.on === "function" ? ctx.on("agent/created", ({ agent }) => {
37138
- currentAgent = agent;
37139
- }) : void 0;
37717
+ const releaseAgent = ctx.on("agent/created", (payload) => {
37718
+ currentAgent = payload.agent;
37719
+ });
37720
+ const releaseStream = ctx.on("llm/stream", guardStream, { global: true });
37140
37721
  function breakerFor(key) {
37141
37722
  let state = breakers.get(key);
37142
37723
  if (state === void 0) {
@@ -37174,7 +37755,7 @@ function installReasoningCollapseGuard(ctx, options = {}) {
37174
37755
  agent.steer?.(createResumeMessage(text));
37175
37756
  });
37176
37757
  }
37177
- runtime.stream = function reasoningCollapseGuardStream(request, next) {
37758
+ function guardStream(request, next) {
37178
37759
  const state = breakerFor(request.signal ?? STATIC_KEY);
37179
37760
  if (state.exhausted || !watchedModel(request.model)) return next();
37180
37761
  async function* guarded() {
@@ -37196,9 +37777,9 @@ function installReasoningCollapseGuard(ctx, options = {}) {
37196
37777
  }
37197
37778
  }
37198
37779
  return guarded();
37199
- };
37780
+ }
37200
37781
  return () => {
37201
- runtime.stream = original;
37782
+ releaseListener(releaseStream);
37202
37783
  releaseListener(releaseAgent);
37203
37784
  };
37204
37785
  }
@@ -37290,7 +37871,7 @@ function apply(ctx, pluginConfig = {}) {
37290
37871
  ctx.effect(() => {
37291
37872
  const dispose = installReasoningCollapseGuard(ctx, guardOptions);
37292
37873
  if (dispose === void 0) {
37293
- ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no llm/stream seam");
37874
+ ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no event bus");
37294
37875
  return () => void 0;
37295
37876
  }
37296
37877
  return dispose;
@@ -37528,7 +38109,10 @@ function apply(ctx, pluginConfig = {}) {
37528
38109
  outputVerbosity: () => preferences.status().outputVerbosity,
37529
38110
  fastMode: () => preferences.status().fastMode,
37530
38111
  reasoningSummary: () => preferences.status().reasoningSummary
37531
- }), preferences);
38112
+ }), preferences, (options) => oauth.credentials(void 0, { purpose: "tool" }).then((credentials) => loadCodexCatalog(credentials, {
38113
+ fetchFn: proxyFetch,
38114
+ signal: options?.signal
38115
+ })).catch(() => []));
37532
38116
  const searchSwitcher = new SearchProviderSwitcher(ctx.loader);
37533
38117
  const applyWebProviders = (current = preferences.status()) => {
37534
38118
  const pluginFetch = proxyManager.resolveActiveProxyUrl() !== null;