dsh-connect-trae 3.8.1 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1012,6 +1012,49 @@ const TRAE_ACCOUNT_STATUS_PATH = "/plugins/dsh-connect-trae/account-status";
1012
1012
  */
1013
1013
  const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
1014
1014
  /**
1015
+ * Plugin-owned account-pool endpoint: one route for the pool's batch actions.
1016
+ *
1017
+ * 参考:`dsh-connect-workbuddy` 的 `WORKBUDDY_POOL_PATH`。本路由与该实现同形
1018
+ * (同一 `?action=` 风格、POST、loopback-only、per-row `accountId`),这样同时
1019
+ * 装了两个插件的用户面对的是同一套交互。
1020
+ *
1021
+ * `?action=checkin` and `?action=test` run over the whole member set; `remove`
1022
+ * addresses ONE account (the body carries `accountId`) and is the only way a
1023
+ * measurement of an account the user no longer wants can leave the store — a
1024
+ * deleted member otherwise keeps its row in the file forever, and a re-added
1025
+ * member would silently inherit an old exclusion.
1026
+ *
1027
+ * POST + loopback-only because both batches SPEND something: check-in sends a
1028
+ * claim, and a test is a real chat call. A page that is not the Host's own
1029
+ * loopback must not be able to trigger either.
1030
+ */
1031
+ const TRAE_POOL_PATH = "/plugins/dsh-connect-trae/pool";
1032
+ /** Query parameter selecting which pool batch action a request means. */
1033
+ const TRAE_POOL_ACTION_PARAM = "action";
1034
+ /** Query parameter naming ONE pool account a per-row request addresses. */
1035
+ const TRAE_POOL_ACCOUNT_PARAM = "accountId";
1036
+ /** Read the pool action off a request URL; unknown/absent means undefined. */
1037
+ function poolActionOf(url) {
1038
+ const at = url.indexOf("?");
1039
+ if (at === -1) return void 0;
1040
+ const value = new URLSearchParams(url.slice(at + 1)).get(TRAE_POOL_ACTION_PARAM);
1041
+ return value === "checkin" || value === "test" || value === "remove" ? value : void 0;
1042
+ }
1043
+ /**
1044
+ * Read the ONE account a per-row pool request addresses, from the QUERY string.
1045
+ *
1046
+ * Unlike the transfer route, the pool's `accountId` rides the URL rather than the
1047
+ * body, so a row action and a batch action are distinguishable before any bytes
1048
+ * are read — which is what lets the route refuse a malformed request without ever
1049
+ * touching the store.
1050
+ */
1051
+ function poolAccountIdOf(url) {
1052
+ const at = url.indexOf("?");
1053
+ if (at === -1) return void 0;
1054
+ const value = new URLSearchParams(url.slice(at + 1)).get(TRAE_POOL_ACCOUNT_PARAM);
1055
+ return value === null || value === "" ? void 0 : value;
1056
+ }
1057
+ /**
1015
1058
  * Plugin-owned account transfer endpoint: batch export and import of a region's
1016
1059
  * stored credentials (issue #37).
1017
1060
  *
@@ -1739,6 +1782,14 @@ const CLI_DEFAULT_HOST = "https://api.trae.cn";
1739
1782
  * working and upgrades itself the next time a credential is written; it is
1740
1783
  * never written again.
1741
1784
  */
1785
+ /**
1786
+ * How long a FAILED per-account refresh is remembered.
1787
+ *
1788
+ * Long enough that a revoked refresh token costs one exchange per five minutes
1789
+ * instead of two per card poll, short enough that a transient network failure is
1790
+ * retried on the next screen the user opens.
1791
+ */
1792
+ const REFRESH_RETRY_MS = 3e5;
1742
1793
  const OWN_VERSION = 2;
1743
1794
  /** The superseded single-credential layout, still read for migration. */
1744
1795
  const OWN_VERSION_SINGLE = 1;
@@ -1819,6 +1870,22 @@ function traeAccountId(credential) {
1819
1870
  const stable = `${credential.edition}\0${credential.userId || credential.accountName || "unknown"}`;
1820
1871
  return createHash("sha256").update(stable).digest("hex").slice(0, 24);
1821
1872
  }
1873
+ /**
1874
+ * The exact shape of an account id — sha256 hex sliced to 24 characters, which is
1875
+ * what {@link traeAccountId} emits and therefore the only string a request may
1876
+ * legitimately name.
1877
+ *
1878
+ * This regex is a traversal guard as much as a validator: an id becomes part of a
1879
+ * path (`$DSH_HOME/.trae-pool.<panel>.json` uses it as a key, and a credential
1880
+ * lookup receives it), so `../../etc/passwd` has to fail here rather than fail
1881
+ * somewhere closer to the filesystem. Exported so an HTTP route can answer a bad
1882
+ * id with 400 instead of letting an exception surface as a 500.
1883
+ */
1884
+ const TRAE_ACCOUNT_ID_RE = /^[0-9a-f]{24}$/;
1885
+ /** Whether a request-supplied value is a well-formed Trae account id. */
1886
+ function isTraeAccountId(value) {
1887
+ return typeof value === "string" && TRAE_ACCOUNT_ID_RE.test(value);
1888
+ }
1822
1889
  /** One stored credential, validated. Returns undefined for a malformed row. */
1823
1890
  function credentialOfStored(stored) {
1824
1891
  if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return void 0;
@@ -1897,6 +1964,37 @@ var TraeCredentialStore = class {
1897
1964
  refresh;
1898
1965
  refreshMarginMs;
1899
1966
  inflight;
1967
+ /**
1968
+ * In-flight refreshes of individual accounts, one per account id.
1969
+ *
1970
+ * The card reads every member's balance in one poll and each balance can
1971
+ * decide to refresh, so without this dedupe the same account would exchange
1972
+ * its token several times in a second and the last writer would win over a
1973
+ * copy another read already holds.
1974
+ */
1975
+ inflightByAccount = /* @__PURE__ */ new Map();
1976
+ /**
1977
+ * When a per-account refresh last FAILED, so a dead refresh token is not
1978
+ * exchanged again on every poll.
1979
+ *
1980
+ * The card reads every member's balance on each refresh, and a revoked refresh
1981
+ * token fails forever: without this memo each poll would spend two upstream
1982
+ * calls (the current contract and the superseded one) on an account no retry
1983
+ * can fix. Five minutes keeps a transient network failure cheap to retry while
1984
+ * a revoked sign-in costs almost nothing.
1985
+ */
1986
+ refreshFailedAtMs = /* @__PURE__ */ new Map();
1987
+ /**
1988
+ * The account the NEXT request should bill, when a pool — not the user's
1989
+ * binding — is choosing (issue #44).
1990
+ *
1991
+ * Runtime only, never persisted: the binding in settings is what a save
1992
+ * writes and what the card's account table reports as selected, while this is
1993
+ * "who serves right now". Keeping them apart is what lets the pool rotate
1994
+ * without silently rewriting the user's choice, and what makes turning the
1995
+ * pool off restore the bound account with no rebind at all.
1996
+ */
1997
+ rotatedAccountId;
1900
1998
  constructor(options) {
1901
1999
  this.storagePathOverride = options.storagePath;
1902
2000
  this.edition = options.edition ?? "auto";
@@ -2106,9 +2204,50 @@ var TraeCredentialStore = class {
2106
2204
  if (this.requireTenant && !isEnterpriseAccount(selected)) return this.preferred(offered);
2107
2205
  return selected;
2108
2206
  }
2207
+ /**
2208
+ * Point this store at one specific account for the NEXT request, without
2209
+ * touching the user's saved binding (issue #44).
2210
+ *
2211
+ * Called by the account pool before each chat request. `undefined` clears the
2212
+ * override, which is how switching the pool off immediately restores the
2213
+ * binding — no rebind, no settings write, nothing to undo.
2214
+ *
2215
+ * A rotation is a REQUEST, not an assertion: `current()` honours it only when
2216
+ * the account still exists locally and this store's own eligibility rules
2217
+ * allow it, and otherwise falls back to the binding. So a rotation can never
2218
+ * invent a credential that {@link current} would refuse.
2219
+ */
2220
+ setRotatedAccount(accountId) {
2221
+ if (this.rotatedAccountId === accountId) return;
2222
+ this.rotatedAccountId = accountId;
2223
+ this.inflight = void 0;
2224
+ }
2225
+ /** The rotation currently in effect, for diagnostics and the pool's own card. */
2226
+ rotatedAccount() {
2227
+ return this.rotatedAccountId;
2228
+ }
2229
+ /**
2230
+ * The credential a rotation points at, or `undefined` when it cannot be
2231
+ * honoured (the login was removed from this machine, or a tenant-scoped store
2232
+ * refuses a personal account).
2233
+ *
2234
+ * Never substitutes a different account here: the caller then applies the
2235
+ * user's binding, which is the honest answer when the pool's choice is gone.
2236
+ */
2237
+ rotated(discovered, offered) {
2238
+ const id = this.rotatedAccountId;
2239
+ if (id === void 0) return void 0;
2240
+ const credential = offered.find((item) => traeAccountId(item) === id) ?? discovered.find((item) => traeAccountId(item) === id);
2241
+ if (credential === void 0) return void 0;
2242
+ if (this.requireTenant && !isEnterpriseAccount(credential)) return void 0;
2243
+ return credential;
2244
+ }
2109
2245
  async current() {
2110
2246
  const discovered = await this.readAll();
2111
- return this.inUse(discovered, this.offered(discovered));
2247
+ const offered = this.offered(discovered);
2248
+ const rotated = this.rotated(discovered, offered);
2249
+ if (rotated !== void 0) return rotated;
2250
+ return this.inUse(discovered, offered);
2112
2251
  }
2113
2252
  /**
2114
2253
  * The stored credential of one specific account, without changing which
@@ -2130,6 +2269,41 @@ var TraeCredentialStore = class {
2130
2269
  return (await this.readAll()).find((credential) => traeAccountId(credential) === accountId);
2131
2270
  }
2132
2271
  /**
2272
+ * A SPECIFIC account's credential, refreshed when its access token is due.
2273
+ *
2274
+ * {@link credentialOf} is a pure lookup and hands back an expired token as-is,
2275
+ * which is right for the chat path (that request fails honestly) but wrong for
2276
+ * a READ of that account's own data: Trae's access token lives for days while
2277
+ * the refresh token lives for months, so a pooled account the user has not
2278
+ * used this week reads as "no balance" — the card showed a bare `—` for an
2279
+ * account whose refresh token was still good. Only the refresh margin decides
2280
+ * here, and a FAILED refresh still yields the stored credential so the caller
2281
+ * reports the failure it actually hit instead of inventing "not signed in".
2282
+ *
2283
+ * This never touches the binding or the rotation: refreshing account B must
2284
+ * not make B the account the next chat request bills.
2285
+ */
2286
+ async resolveCredentialOf(accountId) {
2287
+ const credential = await this.credentialOf(accountId);
2288
+ if (credential === void 0) return void 0;
2289
+ if (credential.expiresAtMs > Date.now() + this.refreshMarginMs) return credential;
2290
+ const pending = this.inflightByAccount.get(accountId);
2291
+ if (pending !== void 0) return pending;
2292
+ const failedAt = this.refreshFailedAtMs.get(accountId);
2293
+ if (failedAt !== void 0 && Date.now() - failedAt < REFRESH_RETRY_MS) return credential;
2294
+ const attempt = this.refreshNow(credential).then((refreshed) => {
2295
+ this.refreshFailedAtMs.delete(accountId);
2296
+ return refreshed;
2297
+ }).catch(() => {
2298
+ this.refreshFailedAtMs.set(accountId, Date.now());
2299
+ return credential;
2300
+ }).finally(() => {
2301
+ this.inflightByAccount.delete(accountId);
2302
+ });
2303
+ this.inflightByAccount.set(accountId, attempt);
2304
+ return attempt;
2305
+ }
2306
+ /**
2133
2307
  * Store imported credentials (issue #37), replacing any entry with the same
2134
2308
  * account id and leaving every other entry alone.
2135
2309
  *
@@ -2555,7 +2729,7 @@ async function refreshTraeCredential(credential, signal, device) {
2555
2729
  }
2556
2730
  //#endregion
2557
2731
  //#region src/identity.ts
2558
- function nonEmpty(value) {
2732
+ function nonEmpty$1(value) {
2559
2733
  return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
2560
2734
  }
2561
2735
  function deviceCenterId(storage) {
@@ -2580,8 +2754,8 @@ function deviceKeyPairOf(storage) {
2580
2754
  const plaintext = parseTraeAuthValue(encoded);
2581
2755
  if (typeof plaintext !== "object" || plaintext === null || Array.isArray(plaintext)) return void 0;
2582
2756
  const value = plaintext;
2583
- const privateKeyPEM = nonEmpty(value["privateKeyPEM"]);
2584
- const publicKeyPEM = nonEmpty(value["publicKeyPEM"]);
2757
+ const privateKeyPEM = nonEmpty$1(value["privateKeyPEM"]);
2758
+ const publicKeyPEM = nonEmpty$1(value["publicKeyPEM"]);
2585
2759
  return privateKeyPEM === void 0 || publicKeyPEM === void 0 ? void 0 : {
2586
2760
  privateKeyPEM,
2587
2761
  publicKeyPEM
@@ -2597,14 +2771,14 @@ async function readTraeIdentity(candidate, options = {}) {
2597
2771
  const env = options.env ?? process.env;
2598
2772
  const storage = JSON.parse(await readFile(candidate.path, "utf8"));
2599
2773
  const appRoot = dirname(dirname(dirname(candidate.path)));
2600
- const machineFile = nonEmpty(await readFile(join(appRoot, "machineid"), "utf8").catch(() => ""));
2601
- const telemetryMachine = nonEmpty(storage["telemetry.machineId"]);
2602
- const devDevice = nonEmpty(storage["telemetry.devDeviceId"]);
2774
+ const machineFile = nonEmpty$1(await readFile(join(appRoot, "machineid"), "utf8").catch(() => ""));
2775
+ const telemetryMachine = nonEmpty$1(storage["telemetry.machineId"]);
2776
+ const devDevice = nonEmpty$1(storage["telemetry.devDeviceId"]);
2603
2777
  const dcDevice = deviceCenterId(storage);
2604
2778
  const machineId = telemetryMachine ?? machineFile;
2605
2779
  if (machineId === void 0) throw new Error(`Trae ${candidate.edition} has no stable machine identity`);
2606
2780
  const deviceId = dcDevice ?? devDevice ?? createHash("sha256").update(machineId).digest("hex").slice(0, 32);
2607
- const buildVersion = nonEmpty(storage["iCubeLastVersion"]);
2781
+ const buildVersion = nonEmpty$1(storage["iCubeLastVersion"]);
2608
2782
  const appName = {
2609
2783
  cn: "Trae CN",
2610
2784
  sg: "Trae",
@@ -2624,18 +2798,18 @@ async function readTraeIdentity(candidate, options = {}) {
2624
2798
  product = JSON.parse(await readFile(path, "utf8"));
2625
2799
  break;
2626
2800
  } catch {}
2627
- const appVersion = nonEmpty(product["appVersion"]);
2628
- const deviceBrand = platform === "darwin" ? nonEmpty(env["TRAE_DEVICE_BRAND"]) : void 0;
2801
+ const appVersion = nonEmpty$1(product["appVersion"]);
2802
+ const deviceBrand = platform === "darwin" ? nonEmpty$1(env["TRAE_DEVICE_BRAND"]) : void 0;
2629
2803
  const deviceCpu = cpus()[0]?.model.split(" ")[0];
2630
2804
  const osVersion = `${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform} ${release()}`;
2631
2805
  const deviceName = (() => {
2632
2806
  try {
2633
- return nonEmpty(userInfo().username);
2807
+ return nonEmpty$1(userInfo().username);
2634
2808
  } catch {
2635
2809
  return;
2636
2810
  }
2637
2811
  })();
2638
- const deviceModel = nonEmpty(env["TRAE_DEVICE_MODEL"]);
2812
+ const deviceModel = nonEmpty$1(env["TRAE_DEVICE_MODEL"]);
2639
2813
  const deviceKeyPair = deviceKeyPairOf(storage);
2640
2814
  return {
2641
2815
  edition: candidate.edition,
@@ -2712,12 +2886,12 @@ async function readTraeCliIdentity(edition, options = {}) {
2712
2886
  const cliHome = join(home, CLI_HOME_BY_EDITION[edition]);
2713
2887
  const argv = await readJsonFile(join(cliHome, "argv.json"));
2714
2888
  const version = await readJsonFile(join(cliHome, "builtin", "ide_version.json"));
2715
- const crashReporterId = nonEmpty(argv?.["crash-reporter-id"]);
2716
- const host = nonEmpty(env["HOSTNAME"]) ?? await readHostname() ?? "unknown-host";
2717
- const user = nonEmpty(env["USER"]) ?? nonEmpty(env["USERNAME"]) ?? "unknown-user";
2889
+ const crashReporterId = nonEmpty$1(argv?.["crash-reporter-id"]);
2890
+ const host = nonEmpty$1(env["HOSTNAME"]) ?? await readHostname() ?? "unknown-host";
2891
+ const user = nonEmpty$1(env["USER"]) ?? nonEmpty$1(env["USERNAME"]) ?? "unknown-user";
2718
2892
  const deviceId = crashReporterId ?? createHash("sha256").update(`trae-cli\0${host}\0${user}`).digest("hex").slice(0, 32);
2719
2893
  const machineId = createHash("sha256").update(`trae-cli-machine\0${deviceId}\0${host}`).digest("hex");
2720
- const appVersion = nonEmpty(version?.["version"]);
2894
+ const appVersion = nonEmpty$1(version?.["version"]);
2721
2895
  const deviceCpu = cpus()[0]?.model.split(" ")[0];
2722
2896
  const osVersion = `${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform} ${release()}`;
2723
2897
  return {
@@ -2743,7 +2917,7 @@ async function readJsonFile(path) {
2743
2917
  async function readHostname() {
2744
2918
  try {
2745
2919
  const { hostname } = await import("node:os");
2746
- return nonEmpty(hostname());
2920
+ return nonEmpty$1(hostname());
2747
2921
  } catch {
2748
2922
  return;
2749
2923
  }
@@ -2803,6 +2977,29 @@ const STATUS_BY_KIND = {
2803
2977
  client: 400,
2804
2978
  unconfigured: 503
2805
2979
  };
2980
+ /**
2981
+ * Failures that say something about the ACCOUNT and are therefore worth another
2982
+ * account: a rejected token, a drained quota, a rate limit, a 5xx.
2983
+ *
2984
+ * `not_found`, `client` and `unconfigured` are deliberately absent — they say
2985
+ * something about the request or this build, and replaying them on a second
2986
+ * account would burn the same error twice while teaching the pool a false fact
2987
+ * about an account that never got a chance to serve.
2988
+ */
2989
+ const FAILOVER_KINDS = /* @__PURE__ */ new Set([
2990
+ "authentication",
2991
+ "hard_credit",
2992
+ "soft_rate",
2993
+ "server"
2994
+ ]);
2995
+ /**
2996
+ * Accounts one request may try.
2997
+ *
2998
+ * Not "all of them": every attempt costs a round-trip before the user sees a
2999
+ * byte, and a pool whose every member is down must fail with a reason instead of
3000
+ * turning one request into a slow tour of the roster.
3001
+ */
3002
+ const MAX_POOL_ATTEMPTS = 3;
2806
3003
  function hostnameOfHost(host) {
2807
3004
  let hostname = host.trim().toLowerCase();
2808
3005
  if (hostname.startsWith("[")) {
@@ -2892,6 +3089,49 @@ function createTraeShim(options) {
2892
3089
  if (actual.length === expected.length && timingSafeEqual(actual, expected)) return route;
2893
3090
  }
2894
3091
  }
3092
+ /**
3093
+ * Run one chat request, failing over across pooled accounts when the account
3094
+ * that was chosen cannot serve it (issue #44).
3095
+ *
3096
+ * The happy path makes exactly one upstream call, so a pool that is switched
3097
+ * off — or on, with the first choice healthy — costs nothing but one cheap
3098
+ * ranking lookup. A failure is reported to the pool BEFORE the decision to
3099
+ * retry, because a rate limit the pool never hears about is a rate limit it
3100
+ * will walk straight back into on the next request.
3101
+ */
3102
+ async function chatWithFailover(route, raw, signal) {
3103
+ const pool = route.pool;
3104
+ let client = route.client;
3105
+ let accountId = pool === void 0 ? void 0 : await pool.prepare().catch(() => void 0);
3106
+ const tried = [];
3107
+ for (let attempt = 0;; attempt += 1) {
3108
+ const result = await client.chatStream(raw, signal);
3109
+ if (result.ok) return {
3110
+ kind: "stream",
3111
+ response: result.response
3112
+ };
3113
+ const retryable = pool !== void 0 && !signal.aborted && FAILOVER_KINDS.has(result.kind);
3114
+ if (retryable && accountId !== void 0) await pool.failed(accountId, result.kind, result.message).catch(() => {});
3115
+ const failed = {
3116
+ kind: "failed",
3117
+ status: STATUS_BY_KIND[result.kind],
3118
+ code: result.kind,
3119
+ message: result.message
3120
+ };
3121
+ if (!retryable) return failed;
3122
+ if (accountId !== void 0) tried.push(accountId);
3123
+ const next = attempt + 1 >= MAX_POOL_ATTEMPTS ? void 0 : await pool.next(tried).catch(() => void 0);
3124
+ if (next === void 0) {
3125
+ const reason = await pool.shortfall(tried).catch(() => void 0);
3126
+ return reason === void 0 ? failed : {
3127
+ ...failed,
3128
+ message: `${failed.message} (${reason})`
3129
+ };
3130
+ }
3131
+ client = next.client;
3132
+ accountId = next.accountId;
3133
+ }
3134
+ }
2895
3135
  async function handle(req, res) {
2896
3136
  try {
2897
3137
  if (!hostIsLoopback(req.headers.host)) return writeError(res, 403, "host_not_allowed", "Host must be loopback");
@@ -2931,8 +3171,8 @@ function createTraeShim(options) {
2931
3171
  const abort = () => controller.abort();
2932
3172
  req.once("aborted", abort);
2933
3173
  req.socket.once("close", abort);
2934
- const result = await route.client.chatStream(raw, controller.signal);
2935
- if (!result.ok) return writeError(res, STATUS_BY_KIND[result.kind], result.kind, result.message);
3174
+ const result = await chatWithFailover(route, raw, controller.signal);
3175
+ if (result.kind === "failed") return writeError(res, result.status, result.code, result.message);
2936
3176
  res.writeHead(200, {
2937
3177
  "Content-Type": "text/event-stream",
2938
3178
  "Cache-Control": "no-cache",
@@ -4975,13 +5215,37 @@ async function readProbeOutput(response) {
4975
5215
  };
4976
5216
  }
4977
5217
  }
4978
- /** The minimal chat call one probe makes. */
4979
- function probeBody(model) {
5218
+ /**
5219
+ * The chat call one probe makes.
5220
+ *
5221
+ * `inputTokens` pads the user turn so the probe carries roughly as much text as
5222
+ * a real conversation. Size matters because an upstream that refuses LARGE
5223
+ * requests will happily answer a one-word probe, and a verdict earned by a
5224
+ * 1-token request says nothing about the 30k-token turn the user is about to
5225
+ * send. `0` keeps the historical minimal probe (one word) — that is the default
5226
+ * because it spends the least credit and answers the question the pool ranks on
5227
+ * ("can this account talk at all").
5228
+ *
5229
+ * The filler is `x` words: one ASCII word is about one token, so the body size
5230
+ * is predictable (tokens ≈ inputTokens × 2 characters) without a tokenizer.
5231
+ */
5232
+ /**
5233
+ * The largest probe this plugin will pad.
5234
+ *
5235
+ * A 100k-token probe is already ~200 KB of JSON; anything above that is a request
5236
+ * the user's real turn would not plausibly reach in this product, and a config
5237
+ * file edited by hand must not turn one "test all" click into a multi-megabyte
5238
+ * upload.
5239
+ */
5240
+ const MAX_PROBE_INPUT_TOKENS = 1e5;
5241
+ function probeBody(model, inputTokens = 0) {
5242
+ const padded = Math.min(Math.max(0, Math.floor(inputTokens)), MAX_PROBE_INPUT_TOKENS);
5243
+ const content = padded > 0 ? `ping ${"x ".repeat(padded).trimEnd()}` : "ping";
4980
5244
  return JSON.stringify({
4981
5245
  model,
4982
5246
  messages: [{
4983
5247
  role: "user",
4984
- content: "ping"
5248
+ content
4985
5249
  }]
4986
5250
  });
4987
5251
  }
@@ -4990,11 +5254,11 @@ function probeBody(model) {
4990
5254
  * recorded verdict describes the user's real path (same wire resolution, same
4991
5255
  * function stamping, same error translation).
4992
5256
  */
4993
- async function probeOneModel(chat, id, timeoutMs = TRAE_PROBE_TIMEOUT_MS) {
5257
+ async function probeOneModel(chat, id, timeoutMs = TRAE_PROBE_TIMEOUT_MS, inputTokens = 0) {
4994
5258
  const controller = new AbortController();
4995
5259
  const timer = setTimeout(() => controller.abort(), timeoutMs);
4996
5260
  try {
4997
- const result = await chat(probeBody(id), controller.signal);
5261
+ const result = await chat(probeBody(id, inputTokens), controller.signal);
4998
5262
  if (!result.ok) return {
4999
5263
  id,
5000
5264
  ...classifyProbeResult(result, false)
@@ -5314,6 +5578,322 @@ var TraeUsageClient = class {
5314
5578
  }
5315
5579
  };
5316
5580
  //#endregion
5581
+ //#region src/account-pool.ts
5582
+ /**
5583
+ * How long an account stays out of the pool when the upstream stated no time.
5584
+ *
5585
+ * A conservative FALLBACK, not a measurement: it applies whenever a failed
5586
+ * outcome carries no `retryAtMs` — every rate limit that came without a reset
5587
+ * sentence AND every transient failure (a transport error, a 5xx). In both cases
5588
+ * "when can I use this again" has no honest answer.
5589
+ *
5590
+ * The two failure directions it avoids: keeping the account in the pool would
5591
+ * re-bill it immediately and re-trip the same limit; dropping it FOREVER would
5592
+ * discard a working account over an event that said nothing about it. A short,
5593
+ * stated cooldown is the least-wrong of the two — and it is what lets the pool
5594
+ * heal itself, since nothing else re-probes an excluded member.
5595
+ */
5596
+ const POOL_UNKNOWN_COOLDOWN_MS = 18e5;
5597
+ /**
5598
+ * Whether one account can be billed at `nowMs`.
5599
+ *
5600
+ * Returns the exclusion reason, or undefined when the account is a candidate.
5601
+ * The order matters and is NOT arbitrary:
5602
+ *
5603
+ * - A `credential-rejected` account is excluded regardless of any cooldown: its
5604
+ * token is not accepted, so waiting cannot help and the user must re-auth.
5605
+ * (`401` / `403` from Trae, and the `ExchangeToken` rejection of an expired
5606
+ * refresh token, land here.)
5607
+ * - A limited, unreachable or failed outcome is checked against its stated (or
5608
+ * fallback) cooldown, so an account whose limit has already reset — or whose
5609
+ * gateway blipped once — is back in the pool without needing another test.
5610
+ * - An `ok` measurement, and no measurement at all, are both candidates. The
5611
+ * latter matters: a freshly discovered account has never been tested and must
5612
+ * not be invisible.
5613
+ *
5614
+ * `not-found`, `credential-rejected` and `plan-gated` deliberately get NO
5615
+ * cooldown: one describes a model this channel does not serve (a catalog fact),
5616
+ * one needs the user to sign in again, and one needs a plan change. None is fixed
5617
+ * by waiting, and pretending otherwise would re-bill a state the upstream has
5618
+ * already refused.
5619
+ *
5620
+ * `policy-rejected` is NOT the `default` branch: a content-policy refusal is a
5621
+ * fact about ONE REQUEST, not about the account, so the account stays a candidate
5622
+ * exactly like `ok`. The measurement is still recorded so the card can explain
5623
+ * what happened.
5624
+ *
5625
+ * An outcome this build does not know becomes `unusable` rather than a candidate:
5626
+ * billing through a state we cannot interpret is worse than leaving the account
5627
+ * out until a later version explains it.
5628
+ */
5629
+ function exclusionOf(probe, nowMs) {
5630
+ if (probe === void 0) return void 0;
5631
+ switch (probe.outcome) {
5632
+ case "ok": return;
5633
+ case "credential-rejected": return "credential-rejected";
5634
+ case "not-found": return "not-found";
5635
+ case "plan-gated": return "plan-gated";
5636
+ case "unavailable": return retryDue(probe, nowMs) ? void 0 : "unavailable";
5637
+ case "failed": return retryDue(probe, nowMs) ? void 0 : "failed";
5638
+ case "rate-limited": return retryDue(probe, nowMs) ? void 0 : "rate-limited";
5639
+ case "model-busy": return retryDue(probe, nowMs, 12e4) ? void 0 : "model-busy";
5640
+ case "out-of-credit": return retryDue(probe, nowMs) ? void 0 : "out-of-credit";
5641
+ case "policy-rejected": return;
5642
+ default: return "unusable";
5643
+ }
5644
+ }
5645
+ /**
5646
+ * Whether an account's cooldown has elapsed (or was never stated).
5647
+ *
5648
+ * The fallback is the ONLY thing a transient outcome relies on: `retryAtMs` is
5649
+ * attached for a rate limit or a drained quota, never for a transport failure, so
5650
+ * `atMs + POOL_UNKNOWN_COOLDOWN_MS` is what brings those back.
5651
+ *
5652
+ * `fallbackMs` lets one outcome carry a SHORTER clock than the generic one. It is
5653
+ * a parameter rather than a second function so every outcome shares one definition
5654
+ * of "due".
5655
+ */
5656
+ function retryDue(probe, nowMs, fallbackMs = POOL_UNKNOWN_COOLDOWN_MS) {
5657
+ return nowMs >= (probe.retryAtMs ?? probe.atMs + fallbackMs);
5658
+ }
5659
+ /**
5660
+ * Rank a pool's members best-first.
5661
+ *
5662
+ * The rule, in priority order — this is the whole feature:
5663
+ *
5664
+ * 1. **Usability.** Only accounts that can be billed right now are candidates;
5665
+ * they always outrank every excluded one.
5666
+ * 2. **Expiring credits first, soonest first.** Points about to expire are worth
5667
+ * exactly zero after they do, so spending them before they lapse is strictly
5668
+ * better than saving them.
5669
+ * 3. **Largest expiring amount.** Between two accounts that both have expiring
5670
+ * points, the one with more at risk goes first.
5671
+ * 4. **Credits, highest first.** Among accounts with nothing expiring, spend the
5672
+ * one with the most, so a nearly-drained account is conserved.
5673
+ * 5. **Freshest credential.** Tie-break so a stale token never wins a draw.
5674
+ * 6. **Account id.** Final tie-break, making the order TOTAL: two accounts equal on
5675
+ * every key would otherwise keep the caller's array order, so the same pool
5676
+ * could rotate differently between two runs.
5677
+ *
5678
+ * WHY EXPIRY OUTRANKS SIZE (workbuddy issue #70). These used to be strict
5679
+ * lexicographic keys: total, THEN expiry. Since totals are almost never exactly
5680
+ * equal, the expiry rule was unreachable — an account with 196 points lapsing that
5681
+ * day ranked LAST behind accounts that expire weeks later, simply because it held
5682
+ * ~1,200 fewer points overall. It was never selected, so those points always
5683
+ * lapsed. A "use it or lose it" rule that another rule completely shadows is not a
5684
+ * tie-break, it is dead code.
5685
+ *
5686
+ * WHY SPENDING THE SMALL EXPIRING ACCOUNT IS STILL RIGHT. Credits are fungible, so
5687
+ * using the rich account instead does not preserve the 1 point — it spends the 1
5688
+ * point's chance and loses it. Using the expiring account spends a point that was
5689
+ * going to be lost anyway, and the rich account is still there for the next
5690
+ * request. The only real cost is a failover round-trip if the expiring account
5691
+ * cannot cover a whole request, which the exclusion/failover path already absorbs.
5692
+ *
5693
+ * Excluded accounts are still RETURNED (sorted after the candidates) so the card
5694
+ * can explain why each is out; they differ from candidates by `excludedBy` being
5695
+ * set, never by being silently dropped.
5696
+ *
5697
+ * Pure and total: the same input always yields the same order, and it never throws
5698
+ * on missing data (an account with no credits is ranked as 0, not crashed on).
5699
+ */
5700
+ function rankPool(members, nowMs) {
5701
+ const rows = members.map((member) => {
5702
+ const excludedBy = exclusionOf(member.probe, nowMs);
5703
+ return {
5704
+ account: member.account,
5705
+ score: scoreOf(member),
5706
+ ...excludedBy === void 0 ? {} : { excludedBy },
5707
+ _usable: excludedBy === void 0,
5708
+ _atRisk: (member.credits?.expiringSoon ?? 0) > 0,
5709
+ _expiringAmount: member.credits?.expiringSoon ?? 0,
5710
+ _expiring: member.credits?.nearestExpiryMs ?? Number.POSITIVE_INFINITY,
5711
+ _tokenExpiresAtMs: member.tokenExpiresAtMs ?? 0
5712
+ };
5713
+ });
5714
+ rows.sort((left, right) => {
5715
+ if (left._usable !== right._usable) return left._usable ? -1 : 1;
5716
+ if (left._atRisk !== right._atRisk) return left._atRisk ? -1 : 1;
5717
+ if (left._atRisk && right._atRisk && left._expiring !== right._expiring) return left._expiring - right._expiring;
5718
+ if (left._expiringAmount !== right._expiringAmount) return right._expiringAmount - left._expiringAmount;
5719
+ if (left.score !== right.score) return right.score - left.score;
5720
+ if (left._tokenExpiresAtMs !== right._tokenExpiresAtMs) return right._tokenExpiresAtMs - left._tokenExpiresAtMs;
5721
+ return left.account.id < right.account.id ? -1 : left.account.id > right.account.id ? 1 : 0;
5722
+ });
5723
+ return rows.map(({ account, score, excludedBy }) => ({
5724
+ account,
5725
+ score,
5726
+ ...excludedBy === void 0 ? {} : { excludedBy }
5727
+ }));
5728
+ }
5729
+ /**
5730
+ * The checked ids that still resolve to a local sign-in, in the caller's order.
5731
+ *
5732
+ * This is THE definition of "effective members". It exists because the same
5733
+ * predicate wants to be written in several places (the status document, the batch
5734
+ * route's empty-pool guard, the host dependency the card reads) and "fixed one
5735
+ * end, missed the other" is this project's most productive bug shape, so the rule
5736
+ * has one home and every site delegates to it.
5737
+ *
5738
+ * ORDER IS THE CALLER'S: the answer preserves the order of `orderedIds`. Callers
5739
+ * pass whichever list's order they want to show — the saved list for the user's
5740
+ * own order, the store's roster for the account table's.
5741
+ */
5742
+ function effectiveMembersOf(orderedIds, listedIds) {
5743
+ return orderedIds.filter((id) => listedIds.has(id));
5744
+ }
5745
+ /**
5746
+ * The members that may actually be billed, in the store's order.
5747
+ *
5748
+ * Three rules in one place, because a request path and a batch that each decide
5749
+ * "who counts" separately is how a pool ends up billing an account the user did
5750
+ * not opt into:
5751
+ *
5752
+ * - the SAVED opt-in list is the roster (`effectiveMembersOf` — an id the user
5753
+ * never selected, or one this machine no longer has, is not a candidate);
5754
+ * - `triedIds` are the accounts that already failed THIS request, so a failover
5755
+ * never walks back into the one that just refused it;
5756
+ * - a ghost (saved but not listed) is absent from `members`, so it drops out here
5757
+ * without a second check.
5758
+ *
5759
+ * An EMPTY `savedIds` yields an empty result on purpose: an enabled pool with no
5760
+ * members bills nobody, rather than silently meaning "all of them".
5761
+ */
5762
+ function billableMembers(members, savedIds, triedIds = []) {
5763
+ const saved = new Set(effectiveMembersOf(savedIds, new Set(members.map((member) => member.account.id))));
5764
+ const tried = new Set(triedIds);
5765
+ return members.filter((member) => saved.has(member.account.id) && !tried.has(member.account.id));
5766
+ }
5767
+ /**
5768
+ * The account to bill, or undefined when the pool has no usable member.
5769
+ *
5770
+ * A thin selector over {@link rankPool} so the winner is decided in exactly one
5771
+ * place: every caller that needs "who serves now" gets the same answer.
5772
+ */
5773
+ function pickAccount(members, nowMs) {
5774
+ return rankPool(members, nowMs).find((row) => row.excludedBy === void 0)?.account;
5775
+ }
5776
+ /**
5777
+ * The credit figure used for ordering.
5778
+ *
5779
+ * Absent credits rank as zero. Deliberately NOT "unknown ranks first": an account
5780
+ * whose balance could not be read must never outrank one we positively know is
5781
+ * full, or a failing credit read would capture all the traffic.
5782
+ */
5783
+ function scoreOf(member) {
5784
+ return member.credits?.total ?? 0;
5785
+ }
5786
+ /**
5787
+ * Pick the free model to test against, from one panel's catalog.
5788
+ *
5789
+ * "Free" means `creditMultiplier === 0` EXACTLY. An absent multiplier is NOT free:
5790
+ * the CN static fallback catalog carries no multiplier at all, so treating absence
5791
+ * as zero would silently bill real credits on a "free" probe.
5792
+ *
5793
+ * Among the free candidates the LARGEST context window wins, because the probe
5794
+ * sends a real payload and a long-conversation probe is the point.
5795
+ *
5796
+ * Returns undefined when the panel has no free model, which the caller must render
5797
+ * as "none available" — never as a fallback to a paid model.
5798
+ */
5799
+ function pickFreeModel(catalog) {
5800
+ let best;
5801
+ for (const model of catalog) {
5802
+ if (model.creditMultiplier !== 0) continue;
5803
+ if (best === void 0 || (model.contextWindow ?? 0) > (best.contextWindow ?? 0)) best = model;
5804
+ }
5805
+ return best;
5806
+ }
5807
+ /**
5808
+ * Resolve the model the pool should test against.
5809
+ *
5810
+ * A user-specified id always wins (it is a preference, not a computed default) and
5811
+ * is returned even when it is a PAID model: the card says so explicitly rather than
5812
+ * silently overriding the user's choice.
5813
+ *
5814
+ * The choice is VALIDATED against the panel's catalog, because the catalog changes:
5815
+ * the CN roster is empty until its first refresh, and models come and go. A saved
5816
+ * id that is no longer offered used to be returned as `preferred`, which let the
5817
+ * card enable its test button and the host run a batch against a model nobody
5818
+ * offers. It is reported as `stale` instead — never silently replaced by the free
5819
+ * model (that would test something the user did not choose) and never run.
5820
+ */
5821
+ function resolveTargetModel(catalog, preferredId) {
5822
+ if (preferredId !== void 0 && preferredId !== "") return catalog.some((model) => model.id === preferredId) ? {
5823
+ modelId: preferredId,
5824
+ source: "preferred"
5825
+ } : {
5826
+ staleModelId: preferredId,
5827
+ source: "stale"
5828
+ };
5829
+ const free = pickFreeModel(catalog);
5830
+ return free === void 0 ? { source: "none" } : {
5831
+ modelId: free.id,
5832
+ source: "free"
5833
+ };
5834
+ }
5835
+ /**
5836
+ * Map one Trae probe verdict onto the pool's outcome vocabulary.
5837
+ *
5838
+ * The mapping is the whole bridge between `src/model-probe.ts` (which answers a
5839
+ * QUESTION ABOUT A MODEL) and the pool (which must decide a FACT ABOUT AN ACCOUNT),
5840
+ * so every row states which side it is reading:
5841
+ *
5842
+ * - `available` → `ok`.
5843
+ * - `auth` → `credential-rejected`: 401/403 means THIS token is refused;
5844
+ * waiting cannot help and the user must sign in again.
5845
+ * - `gated` (`1005`) → `plan-gated`: the account's plan does not cover this model,
5846
+ * which is an ACCOUNT fact (a paying account may pass) but not one that heals on
5847
+ * a clock.
5848
+ * - `rate_limited` → `rate-limited`: `4011` or an honest 429 — transient by
5849
+ * definition, so it cools down rather than leaving the pool forever.
5850
+ * - `unsupported` → `not-found`: `4001` says no SOLO function serves this
5851
+ * `config_name`. It is MODEL-scoped, so the recorded `modelId` is what keeps the
5852
+ * row honest, and nothing about the account changes by waiting.
5853
+ * - `unknown` → `failed`: a timeout, a transport error or a proxy problem.
5854
+ * It cools down (it is not a statement about the account) and carries the
5855
+ * upstream text so the user can retest.
5856
+ *
5857
+ * There is deliberately NO Trae source for `model-busy`, `out-of-credit` and
5858
+ * `policy-rejected` in this table — they are produced by the live-request path
5859
+ * (`src/account-pool.ts` has no network), from `4008` (quota exhausted) and from a
5860
+ * content refusal. A verdict NOT in this table falls through to `failed`, so a new
5861
+ * upstream verdict can never silently mean "healthy".
5862
+ */
5863
+ function outcomeOfVerdict(verdict) {
5864
+ switch (verdict) {
5865
+ case "available": return "ok";
5866
+ case "auth": return "credential-rejected";
5867
+ case "gated": return "plan-gated";
5868
+ case "rate_limited": return "rate-limited";
5869
+ case "unsupported": return "not-found";
5870
+ default: return "failed";
5871
+ }
5872
+ }
5873
+ /**
5874
+ * Which pool a panel's members belong to, for the on-disk probe store and the
5875
+ * `pool` settings slot.
5876
+ *
5877
+ * `enterprise` is its OWN bucket even though it shares the CN credential store:
5878
+ * an account that serves the personal SOLO channel says nothing about whether it
5879
+ * passes the tenant gate (`4019` is only revealed at the first chat), so measuring
5880
+ * one must not silently answer the other.
5881
+ */
5882
+ function poolKeyOfPanel(panel) {
5883
+ return panel;
5884
+ }
5885
+ /**
5886
+ * WHY THERE IS NO SCHEDULER HERE.
5887
+ *
5888
+ * Like the reference implementation, this module carries no timer: rotation is a
5889
+ * PERSISTENT change of who pays while failover is a PER-REQUEST borrow of another
5890
+ * account, and having both with an automatic re-test on a clock made "who is being
5891
+ * billed right now" unanswerable on one card. Recovery is expressed as the cooldown
5892
+ * in {@link exclusionOf} instead, which needs no timer: it is evaluated at ranking
5893
+ * time, so a cooled-down member simply becomes a candidate again on the next
5894
+ * request.
5895
+ */
5896
+ //#endregion
5317
5897
  //#region src/transfer.ts
5318
5898
  /**
5319
5899
  * Account transfer: the export / import file format shared with sibling
@@ -5586,7 +6166,7 @@ function mergeTransferRecords(records, indexes, region, knownRegionOfCredential
5586
6166
  * matched on the code rather than on the HTTP status: read as a transport
5587
6167
  * failure it looks like the plugin is broken, when the day is simply spent.
5588
6168
  */
5589
- const CHECKIN_DEVICE_ALREADY_CLAIMED = 9095;
6169
+ const CHECKIN_DEVICE_ALREADY_CLAIMED$1 = 9095;
5590
6170
  /** Redact token-like content before it crosses to the browser. */
5591
6171
  function safeMessage(error) {
5592
6172
  return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 500);
@@ -5726,7 +6306,8 @@ function toCredits(snapshot, nowMs = Date.now()) {
5726
6306
  displayDesc: pack.displayDesc,
5727
6307
  remain: credit(Math.max(0, (pack.creditsLimit ?? 0) - (pack.consumedCredits ?? 0))),
5728
6308
  size: pack.creditsLimit ?? 0,
5729
- ...usableExpiry(pack.endTimeMs) ? { expiresAtMs: pack.endTimeMs } : {}
6309
+ ...usableExpiry(pack.endTimeMs) ? { expiresAtMs: pack.endTimeMs } : {},
6310
+ ...pack.availableEndpoint === void 0 ? {} : { endpoint: pack.availableEndpoint }
5730
6311
  }));
5731
6312
  const remaining = (endpoint) => snapshot.packs.filter((pack) => pack.availableEndpoint === endpoint).reduce((sum, pack) => credit(sum + Math.max(0, (pack.creditsLimit ?? 0) - (pack.consumedCredits ?? 0))), 0);
5732
6313
  return {
@@ -5880,18 +6461,21 @@ async function traeWebUsage(deps, panel) {
5880
6461
  enabledModelIds: [...deps.enabledModelIds(panel)],
5881
6462
  ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(panel) }
5882
6463
  };
6464
+ const poolSection = await poolSectionOf(deps, panel);
5883
6465
  const client = deps.client(panel);
5884
6466
  if (regionOfPanel(panel) === "ai") try {
5885
6467
  const payStatus = await client.payStatus();
5886
6468
  return {
5887
6469
  status: "signed-in",
5888
6470
  ...account,
6471
+ ...poolSection,
5889
6472
  payStatus
5890
6473
  };
5891
6474
  } catch (error) {
5892
6475
  return {
5893
6476
  status: "signed-in",
5894
6477
  ...account,
6478
+ ...poolSection,
5895
6479
  payStatusError: safeMessage(error)
5896
6480
  };
5897
6481
  }
@@ -5909,6 +6493,7 @@ async function traeWebUsage(deps, panel) {
5909
6493
  if (panel === "enterprise") return {
5910
6494
  status: "signed-in",
5911
6495
  ...account,
6496
+ ...poolSection,
5912
6497
  creditsUnavailable: "enterprise"
5913
6498
  };
5914
6499
  const [snapshotResult, checkinResult] = await Promise.allSettled([client.snapshot(), client.checkinStatus()]);
@@ -5917,6 +6502,7 @@ async function traeWebUsage(deps, panel) {
5917
6502
  return {
5918
6503
  status: "signed-in",
5919
6504
  ...account,
6505
+ ...poolSection,
5920
6506
  ...credits === void 0 ? { creditsError: snapshotResult.status === "rejected" ? safeMessage(snapshotResult.reason) : "unknown" } : { credits },
5921
6507
  ...checkinResult.status === "fulfilled" ? { checkin: toCheckin(checkinResult.value) } : { checkinError: safeMessage(checkinResult.reason) },
5922
6508
  ...alternatives === void 0 ? {} : { alternatives }
@@ -5957,6 +6543,185 @@ async function creditAlternatives(deps, panel, accounts) {
5957
6543
  return alternatives.length === 0 ? void 0 : alternatives;
5958
6544
  }
5959
6545
  /**
6546
+ * The panel's pool section, or an empty standpoint when this Host cannot run one.
6547
+ *
6548
+ * THE rule of this file: the pool is an ADD-ON to the status document, so no
6549
+ * failure inside it may cost the user the sign-in panel. A member with a broken
6550
+ * balance read, a probe store that cannot be parsed, a clock — all of it degrades
6551
+ * to "no pool section" rather than to a 500 that hides the account table.
6552
+ *
6553
+ * What is deliberately NOT swallowed here: a `credential`-class failure of the
6554
+ * BOUND account, which `traeWebUsage` reports as `status: error` before the pool
6555
+ * is even assembled.
6556
+ */
6557
+ async function poolSectionOf(deps, panel) {
6558
+ if (deps.pool === void 0) return {};
6559
+ try {
6560
+ const pool = await traeWebPool(deps, panel);
6561
+ return pool === void 0 ? {} : { pool };
6562
+ } catch {
6563
+ return {};
6564
+ }
6565
+ }
6566
+ /**
6567
+ * Assemble one panel's pool document (see {@link TraeWebPool}).
6568
+ *
6569
+ * Rows come from TWO sources and are kept in the Host's ranking order: checked
6570
+ * members (ranked by {@link rankPool}) first, then the local sign-ins the pool
6571
+ * does NOT cover, so the card can offer them to check. Unchecked rows carry no
6572
+ * credits and no measurement — nothing has been read for an account the pool does
6573
+ * not bill, and printing a friendlier-looking zero would be a lie.
6574
+ *
6575
+ * Every per-row read is failure-tolerant on purpose: one account whose balance
6576
+ * read throws must not remove the other members from the table.
6577
+ */
6578
+ async function traeWebPool(deps, panel) {
6579
+ const pool = deps.pool;
6580
+ if (pool === void 0) return void 0;
6581
+ const preferences = pool.preferences(panel);
6582
+ const members = await pool.members(panel);
6583
+ const nowMs = Date.now();
6584
+ const ranked = rankPool(members, nowMs);
6585
+ const target = resolveTargetModel(pool.catalog?.(panel) ?? [], preferences.targetModelId);
6586
+ const byId = new Map(members.map((member) => [member.account.id, member]));
6587
+ const excludedById = new Map(ranked.flatMap((row) => row.excludedBy === void 0 ? [] : [[row.account.id, row.excludedBy]]));
6588
+ const rankedIds = new Set(ranked.map((row) => row.account.id));
6589
+ const others = (await pool.otherAccounts?.(panel) ?? []).filter((account) => !rankedIds.has(account.id));
6590
+ const checkinState = await pool.checkinState?.(panel).catch(() => ({})) ?? {};
6591
+ const memberNames = await pool.memberNames?.(panel).catch(() => ({})) ?? {};
6592
+ const currentAccountId = await pool.currentAccountId?.(panel).catch(() => void 0);
6593
+ /**
6594
+ * A row per listed account, members first.
6595
+ *
6596
+ * `member` is the membership source of truth: the saved list is what a batch
6597
+ * runs on, so a local sign-in that is not checked must not render as one —
6598
+ * otherwise the card would claim "N accounts will be tested" over a set the
6599
+ * Host never touches.
6600
+ */
6601
+ const savedMembers = new Set(preferences.memberAccountIds ?? []);
6602
+ const rows = [];
6603
+ const rowIds = /* @__PURE__ */ new Set();
6604
+ for (const row of ranked) {
6605
+ rows.push({
6606
+ account: row.account,
6607
+ member: savedMembers.has(row.account.id)
6608
+ });
6609
+ rowIds.add(row.account.id);
6610
+ }
6611
+ for (const account of others) {
6612
+ rows.push({
6613
+ account,
6614
+ member: savedMembers.has(account.id)
6615
+ });
6616
+ rowIds.add(account.id);
6617
+ }
6618
+ for (const accountId of savedMembers) {
6619
+ if (rowIds.has(accountId)) continue;
6620
+ rows.push({
6621
+ account: {
6622
+ id: accountId,
6623
+ accountName: ""
6624
+ },
6625
+ member: true
6626
+ });
6627
+ rowIds.add(accountId);
6628
+ }
6629
+ const reachableIds = new Set(members.map((member) => member.account.id));
6630
+ const tokenExpiresById = /* @__PURE__ */ new Map();
6631
+ for (const member of members) if (member.tokenExpiresAtMs !== void 0) tokenExpiresById.set(member.account.id, member.tokenExpiresAtMs);
6632
+ const accounts = rows.map((row) => {
6633
+ const entry = byId.get(row.account.id);
6634
+ const measured = entry?.probe;
6635
+ const credits = entry?.credits;
6636
+ const checkin = checkinState[row.account.id];
6637
+ const excludedBy = excludedById.get(row.account.id);
6638
+ const accountName = row.account.accountName !== "" ? row.account.accountName : memberNames[row.account.id] ?? measured?.accountName ?? "";
6639
+ return {
6640
+ accountId: row.account.id,
6641
+ accountName,
6642
+ ...credits === void 0 ? {} : {
6643
+ credits: credits.total,
6644
+ ...credits.generalAvailable === void 0 ? {} : { generalAvailable: credits.generalAvailable },
6645
+ ...credits.workAvailable === void 0 ? {} : { workAvailable: credits.workAvailable },
6646
+ expiringSoon: credits.expiringSoon,
6647
+ ...credits.nearestExpiryMs === void 0 ? {} : { nearestExpiryMs: credits.nearestExpiryMs }
6648
+ },
6649
+ ...measured === void 0 ? {} : { probe: {
6650
+ outcome: measured.outcome,
6651
+ atMs: measured.atMs,
6652
+ ...measured.source === void 0 ? {} : { source: measured.source },
6653
+ ...measured.modelId === void 0 ? {} : { modelId: measured.modelId },
6654
+ ...measured.retryAtMs === void 0 ? {} : { retryAtMs: measured.retryAtMs },
6655
+ ...measured.message === void 0 ? {} : { message: safeMessage(measured.message) }
6656
+ } },
6657
+ ...excludedBy === void 0 ? {} : { excludedBy },
6658
+ ...credits === void 0 && (tokenExpiresById.get(row.account.id) ?? Infinity) <= nowMs ? { tokenExpired: true } : {},
6659
+ current: currentAccountId !== void 0 && currentAccountId === row.account.id,
6660
+ member: row.member,
6661
+ reachable: reachableIds.has(row.account.id),
6662
+ ...checkin === void 0 ? {} : {
6663
+ checkedInToday: checkin.checkedIn,
6664
+ ...checkin.deviceCheckedIn === void 0 ? {} : { deviceCheckedIn: checkin.deviceCheckedIn }
6665
+ }
6666
+ };
6667
+ });
6668
+ /**
6669
+ * Why the pool cannot pick anybody, when it cannot.
6670
+ *
6671
+ * Only meaningful for an ENABLED pool: a disabled pool is not picking anybody by
6672
+ * design, and saying "no candidate" there would describe a fault that is not
6673
+ * one. Every member excluded is the case the card must explain, because traffic
6674
+ * then falls back to the saved selection while the table shows a full roster.
6675
+ */
6676
+ let noCandidate;
6677
+ if (preferences.enabled === true) {
6678
+ if (members.length === 0) noCandidate = "empty";
6679
+ else if (ranked.every((row) => row.excludedBy !== void 0)) noCandidate = new Set(ranked.map((row) => row.excludedBy)).size === 1 ? ranked[0]?.excludedBy ?? "unusable" : "unusable";
6680
+ }
6681
+ const effectiveMemberAccountIds = effectiveMembersOf(rows.filter((row) => row.member).map((row) => row.account.id), new Set(members.map((member) => member.account.id)));
6682
+ return {
6683
+ enabled: preferences.enabled === true,
6684
+ checkinSupported: pool.checkin !== void 0 && regionOfPanel(panel) === "cn",
6685
+ ...target.modelId === void 0 ? {} : { targetModelId: target.modelId },
6686
+ targetModelSource: target.source,
6687
+ probeInputTokens: Math.max(0, preferences.probeInputTokens ?? 0),
6688
+ ...target.staleModelId === void 0 ? {} : { staleTargetModelId: target.staleModelId },
6689
+ ...preferences.memberAccountIds === void 0 ? {} : { memberAccountIds: preferences.memberAccountIds },
6690
+ effectiveMemberAccountIds,
6691
+ ...Object.keys(memberNames).length === 0 ? {} : { memberNames },
6692
+ accounts,
6693
+ ...noCandidate === void 0 ? {} : { noCandidate }
6694
+ };
6695
+ }
6696
+ /**
6697
+ * One test row, translated into the pool vocabulary the card renders.
6698
+ *
6699
+ * The runner answers in the model-probe verdicts (`available` / `auth` / `gated` /
6700
+ * `rate_limited` / `unsupported`); the card must not have to know that `gated` and
6701
+ * `1005` are the same event, so the translation happens here — once, for both the
6702
+ * batch and the single-row probe.
6703
+ */
6704
+ function poolTestRowViewOf(row) {
6705
+ return {
6706
+ accountId: row.accountId,
6707
+ accountName: row.accountName,
6708
+ modelId: row.modelId,
6709
+ outcome: outcomeOfVerdict(row.outcome),
6710
+ detail: safeMessage(row.detail),
6711
+ ...row.retryAtMs === void 0 ? {} : { retryAtMs: row.retryAtMs }
6712
+ };
6713
+ }
6714
+ /** One check-in row, redacted on the way out. */
6715
+ function poolCheckinRowViewOf(row) {
6716
+ return {
6717
+ accountId: row.accountId,
6718
+ accountName: row.accountName,
6719
+ status: row.status,
6720
+ ...row.credit === void 0 ? {} : { credit: row.credit },
6721
+ ...row.message === void 0 ? {} : { message: safeMessage(row.message) }
6722
+ };
6723
+ }
6724
+ /**
5960
6725
  * The panel a request addresses, or a 400 answer. Absent parameter means the
5961
6726
  * domestic tab; an unknown value is refused rather than guessed.
5962
6727
  */
@@ -6238,7 +7003,7 @@ function registerTraeUsageRoute(ctx, deps) {
6238
7003
  const outcome = await deps.claimCheckinOf(panel, accountId);
6239
7004
  if (outcome === void 0) return json(res, 404, { error: "unknown account" });
6240
7005
  const { claim, checkin: after } = outcome;
6241
- const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED;
7006
+ const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED$1;
6242
7007
  return json(res, 200, {
6243
7008
  claimed: claim.claimed,
6244
7009
  alreadyCheckedIn: after.checkedIn,
@@ -6261,13 +7026,13 @@ function registerTraeUsageRoute(ctx, deps) {
6261
7026
  claimed: false,
6262
7027
  alreadyCheckedIn: false,
6263
7028
  deviceCheckedIn: true,
6264
- code: CHECKIN_DEVICE_ALREADY_CLAIMED,
7029
+ code: CHECKIN_DEVICE_ALREADY_CLAIMED$1,
6265
7030
  message: "this device already used today's check-in",
6266
7031
  checkin: toCheckin(current)
6267
7032
  });
6268
7033
  const claim = await client.claimCheckin();
6269
7034
  const checkin = await client.checkinStatus();
6270
- const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED;
7035
+ const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED$1;
6271
7036
  json(res, 200, {
6272
7037
  claimed: claim.claimed,
6273
7038
  alreadyCheckedIn: false,
@@ -6281,7 +7046,131 @@ function registerTraeUsageRoute(ctx, deps) {
6281
7046
  }
6282
7047
  }
6283
7048
  });
7049
+ /**
7050
+ * The account pool's ONE mutation surface: `?action=checkin|test|remove`.
7051
+ *
7052
+ * Everything the pool does that costs something happens here and nowhere else —
7053
+ * `checkin` sends a claim, `test` spends a real chat call per member, `remove`
7054
+ * drops one account's measurement. The pool is therefore POST-only and
7055
+ * loopback-only, like the model test and the check-in it wraps.
7056
+ *
7057
+ * The empty-pool guard runs on `effectiveMemberAccountIds` — what this Host can
7058
+ * actually reach — rather than on the saved list. A saved id whose sign-in
7059
+ * disappeared would otherwise let a batch answer `200` with zero rows, which
7060
+ * the card reads as "全部正常".
7061
+ *
7062
+ * Answers are JSON except for a streaming test batch, which writes one NDJSON
7063
+ * line per account as it lands: the loop is serial, so without a stream a user
7064
+ * cannot tell a slow member from a dead button.
7065
+ */
7066
+ const disposePool = ctx.webServer.register({
7067
+ kind: "exact",
7068
+ path: TRAE_POOL_PATH,
7069
+ handler: async (req, res) => {
7070
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
7071
+ if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
7072
+ const panel = requestRegion(req, res);
7073
+ if (panel === void 0) return;
7074
+ const pool = deps.pool;
7075
+ if (pool === void 0) return json(res, 503, { error: "account pool unavailable" });
7076
+ const url = req.url ?? "/";
7077
+ const action = poolActionOf(url);
7078
+ if (action === void 0) return json(res, 400, { error: `missing or unknown action parameter (${TRAE_POOL_ACTION_PARAM})` });
7079
+ const accountId = poolAccountIdOf(url);
7080
+ if (accountId !== void 0 && !isTraeAccountId(accountId)) return json(res, 400, { error: "malformed account id" });
7081
+ if (action === "remove") {
7082
+ if (accountId === void 0) return json(res, 400, { error: "remove needs an accountId" });
7083
+ if (pool.removeOne === void 0) return json(res, 503, { error: "pool removal unavailable" });
7084
+ try {
7085
+ json(res, 200, {
7086
+ accountId,
7087
+ removed: await pool.removeOne(panel, accountId)
7088
+ });
7089
+ } catch (error) {
7090
+ json(res, 500, { error: safeMessage(error) });
7091
+ }
7092
+ return;
7093
+ }
7094
+ /**
7095
+ * Both remaining actions need a NON-EMPTY effective pool.
7096
+ *
7097
+ * `effectiveMemberAccountIds` is optional; when the Host does not supply it
7098
+ * the guard falls back to the saved list, which is weaker but never worse
7099
+ * than no guard at all. `no-live-members` and `no-members` are separate
7100
+ * reasons because the advice differs: re-check the accounts you lost versus
7101
+ * check some accounts in.
7102
+ */
7103
+ const saved = pool.preferences(panel).memberAccountIds ?? [];
7104
+ let effective = saved;
7105
+ if (pool.effectiveMemberAccountIds !== void 0) try {
7106
+ effective = await pool.effectiveMemberAccountIds(panel);
7107
+ } catch (error) {
7108
+ return json(res, 500, { error: safeMessage(error) });
7109
+ }
7110
+ if (effective.length === 0) return json(res, 409, {
7111
+ action,
7112
+ reason: saved.length === 0 ? "no-members" : "no-live-members",
7113
+ error: saved.length === 0 ? "no accounts are checked into this pool" : "none of the checked accounts can be reached on this machine"
7114
+ });
7115
+ if (action === "checkin") {
7116
+ const supported = pool.checkinSupported?.(panel) ?? true;
7117
+ if (!supported || pool.checkin === void 0) return json(res, 409, {
7118
+ action,
7119
+ reason: "checkin-unsupported",
7120
+ error: supported ? "check-in is unavailable in this build" : "this panel has no daily check-in"
7121
+ });
7122
+ try {
7123
+ json(res, 200, { rows: (await pool.checkin(panel)).map((row) => poolCheckinRowViewOf(row)) });
7124
+ } catch (error) {
7125
+ json(res, 500, { error: safeMessage(error) });
7126
+ }
7127
+ return;
7128
+ }
7129
+ const targetModelId = pool.preferences(panel).targetModelId;
7130
+ if (pool.test === void 0) return json(res, 409, {
7131
+ action,
7132
+ reason: "test-unsupported",
7133
+ error: "the pool cannot test this panel"
7134
+ });
7135
+ const single = accountId === void 0 ? void 0 : pool.testOne;
7136
+ if (single !== void 0 && accountId !== void 0) {
7137
+ const resolved = resolveTargetModel(pool.catalog?.(panel) ?? [], targetModelId);
7138
+ if (resolved.modelId === void 0) return json(res, 409, {
7139
+ action,
7140
+ reason: resolved.source === "stale" ? "stale-target" : "no-target-model",
7141
+ error: resolved.source === "stale" ? "the saved test model is no longer offered by this panel" : "no free model is available to test against"
7142
+ });
7143
+ resolved.modelId;
7144
+ }
7145
+ if (single !== void 0 && accountId !== void 0) try {
7146
+ const row = await single(panel, accountId);
7147
+ if (row === void 0) return json(res, 404, { error: "unknown account" });
7148
+ return json(res, 200, { rows: [poolTestRowViewOf(row)] });
7149
+ } catch (error) {
7150
+ return json(res, 500, { error: safeMessage(error) });
7151
+ }
7152
+ res.writeHead(200, {
7153
+ "Content-Type": "application/x-ndjson",
7154
+ "Cache-Control": "no-store"
7155
+ });
7156
+ const writeLine = (value) => {
7157
+ res.write(`${JSON.stringify(value)}\n`);
7158
+ };
7159
+ try {
7160
+ writeLine({ rows: (await pool.test(panel, targetModelId ?? "", (row) => {
7161
+ writeLine({ row: poolTestRowViewOf(row) });
7162
+ })).map((row) => poolTestRowViewOf(row)) });
7163
+ } catch (error) {
7164
+ writeLine({
7165
+ reason: "pool-failed",
7166
+ error: safeMessage(error)
7167
+ });
7168
+ }
7169
+ res.end();
7170
+ }
7171
+ });
6284
7172
  return () => {
7173
+ disposePool();
6285
7174
  disposeCheckin();
6286
7175
  disposeRefresh();
6287
7176
  disposeTransfer();
@@ -6294,6 +7183,538 @@ function registerTraeUsageRoute(ctx, deps) {
6294
7183
  }, "dsh-connect-trae: Web usage route");
6295
7184
  }
6296
7185
  //#endregion
7186
+ //#region src/account-pool-run.ts
7187
+ /** The upstream's business code for "this DEVICE already checked in today". */
7188
+ const CHECKIN_DEVICE_ALREADY_CLAIMED = 9095;
7189
+ /**
7190
+ * Check in every account, one at a time.
7191
+ *
7192
+ * Idempotent by design (see the module note): an account already checked in today
7193
+ * is reported as `already` and NO write is attempted. A failure on one account is
7194
+ * recorded on that row and the batch continues — EXCEPT for the device-level
7195
+ * refusal (`9095`), which stops the run: it is a fact about this MACHINE, so every
7196
+ * later account would hit the same wall and each of those calls is a wasted
7197
+ * upstream request against an endpoint that just said "stop".
7198
+ *
7199
+ * Never throws: every problem becomes a row.
7200
+ */
7201
+ async function checkinAllAccounts(targets, deps) {
7202
+ const rows = [];
7203
+ for (const [index, target] of targets.entries()) {
7204
+ if (index > 0) await (deps.wait ?? defaultWait)(400);
7205
+ const row = await checkinOne(target, deps);
7206
+ rows.push(row);
7207
+ if (row.status === "device-already") {
7208
+ for (const skipped of targets.slice(index + 1)) rows.push({
7209
+ accountId: skipped.accountId,
7210
+ accountName: skipped.accountName,
7211
+ status: "device-already",
7212
+ message: "skipped: this device has already claimed today"
7213
+ });
7214
+ break;
7215
+ }
7216
+ }
7217
+ return rows;
7218
+ }
7219
+ /** One account's check-in, reduced to its row. */
7220
+ async function checkinOne(target, deps) {
7221
+ const base = {
7222
+ accountId: target.accountId,
7223
+ accountName: target.accountName
7224
+ };
7225
+ let credential;
7226
+ try {
7227
+ credential = await deps.credentialFor(target.accountId);
7228
+ } catch (error) {
7229
+ return {
7230
+ ...base,
7231
+ status: "failed",
7232
+ message: messageOf(error)
7233
+ };
7234
+ }
7235
+ if (credential === void 0) return {
7236
+ ...base,
7237
+ status: "failed",
7238
+ message: "no stored credential for this account"
7239
+ };
7240
+ let status;
7241
+ try {
7242
+ status = await deps.checkinStatusOf(credential);
7243
+ } catch (error) {
7244
+ return {
7245
+ ...base,
7246
+ status: "failed",
7247
+ message: messageOf(error)
7248
+ };
7249
+ }
7250
+ if (!status.enabled) return {
7251
+ ...base,
7252
+ status: "disabled",
7253
+ message: "check-in is not enabled for this account"
7254
+ };
7255
+ if (status.checkedIn) return {
7256
+ ...base,
7257
+ status: "already",
7258
+ credit: status.credits
7259
+ };
7260
+ if (status.didCheckedIn) {}
7261
+ try {
7262
+ const claim = await deps.claimCheckinOf(credential);
7263
+ if (claim.claimed) return {
7264
+ ...base,
7265
+ status: "claimed",
7266
+ credit: status.credits,
7267
+ ...nonEmpty(claim.message)
7268
+ };
7269
+ if (claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED) return {
7270
+ ...base,
7271
+ status: "device-already",
7272
+ message: claim.message
7273
+ };
7274
+ return {
7275
+ ...base,
7276
+ status: "failed",
7277
+ message: claim.message === "" ? `code ${String(claim.code)}` : claim.message
7278
+ };
7279
+ } catch (error) {
7280
+ return {
7281
+ ...base,
7282
+ status: "failed",
7283
+ message: messageOf(error)
7284
+ };
7285
+ }
7286
+ }
7287
+ /**
7288
+ * Test every account against one model, one at a time.
7289
+ *
7290
+ * The model is resolved by the caller (see `resolveTargetModel`) so this function
7291
+ * never has to decide "free or not" — an unresolvable target is the caller's
7292
+ * problem to report, and testing against a model nobody chose would be exactly the
7293
+ * silent spend this feature forbids.
7294
+ *
7295
+ * ONE model for the whole batch, deliberately. Letting each row test a different
7296
+ * model makes a green batch UNCOMPARABLE — one account's `ok` on model X and
7297
+ * another's on model Y — which is the reading ("all green") that the reference
7298
+ * implementation removed its per-account override for. What stays is that every
7299
+ * measurement RECORDS the model it probed and each row displays it.
7300
+ *
7301
+ * Never throws: a failed probe is a ROW, not an aborted batch. A credential
7302
+ * rejection does NOT stop the run (unlike the region-wide model test, where every
7303
+ * later probe would fail the same way): here each account has its OWN credential,
7304
+ * so one account's bad token says nothing about its neighbours.
7305
+ *
7306
+ * `onRow` fires the moment one account's row exists, which is what lets a caller
7307
+ * report progress. The loop is SERIAL by design, so without this a caller cannot
7308
+ * say anything until the SLOWEST member answers: one stuck account made a working
7309
+ * batch look like a dead button.
7310
+ */
7311
+ async function testAllAccounts(targets, modelId, probe, deps, onRow) {
7312
+ const rows = [];
7313
+ for (const [index, target] of targets.entries()) {
7314
+ if (index > 0) await (deps.wait ?? defaultWait)(400);
7315
+ const row = await testOne(target, modelId, probe, deps);
7316
+ rows.push(row);
7317
+ onRow?.(row);
7318
+ }
7319
+ return rows;
7320
+ }
7321
+ /** One account's probe, reduced to its row. */
7322
+ async function testOne(target, modelId, probe, deps) {
7323
+ const base = {
7324
+ accountId: target.accountId,
7325
+ accountName: target.accountName,
7326
+ modelId
7327
+ };
7328
+ let credential;
7329
+ try {
7330
+ credential = await deps.credentialFor(target.accountId);
7331
+ } catch (error) {
7332
+ return {
7333
+ ...base,
7334
+ outcome: "auth",
7335
+ detail: messageOf(error)
7336
+ };
7337
+ }
7338
+ if (credential === void 0) return {
7339
+ ...base,
7340
+ outcome: "auth",
7341
+ detail: "no stored credential for this account"
7342
+ };
7343
+ const result = await probe(deps.chatFor, credential, modelId);
7344
+ return {
7345
+ ...base,
7346
+ outcome: result.verdict,
7347
+ detail: result.detail
7348
+ };
7349
+ }
7350
+ /**
7351
+ * Turn a test batch's rows into the per-account measurements to remember.
7352
+ *
7353
+ * A row is stored only when the probe actually measured the MODEL. A row that
7354
+ * failed because the credential could not even be read says nothing about the
7355
+ * model, and recording it would exclude a possibly-perfect account from the pool —
7356
+ * so those accounts keep their previous measurement instead.
7357
+ *
7358
+ * `atMs` is stamped here, once for the batch, so the card's "tested N minutes ago"
7359
+ * reflects when the batch finished rather than drifting per row.
7360
+ *
7361
+ * `detail` is carried through as `message`. It was already produced from the
7362
+ * upstream's own (translated) text, so persisting it cannot leak a token, and
7363
+ * without it the pool table could only repeat one generic sentence for a DNS
7364
+ * failure, a gateway 502 and a cancelled request alike.
7365
+ */
7366
+ function probeUpdatesOf(rows, nowMs = Date.now()) {
7367
+ const updates = {};
7368
+ for (const row of rows) {
7369
+ if (isTransportFailure(row)) continue;
7370
+ const outcome = outcomeOfVerdict(row.outcome);
7371
+ updates[row.accountId] = {
7372
+ outcome,
7373
+ atMs: nowMs,
7374
+ source: "test-batch",
7375
+ modelId: row.modelId,
7376
+ ...row.accountName === "" ? {} : { accountName: row.accountName },
7377
+ ...row.retryAtMs === void 0 ? {} : { retryAtMs: row.retryAtMs },
7378
+ ...row.detail === "" ? {} : { message: row.detail }
7379
+ };
7380
+ }
7381
+ return updates;
7382
+ }
7383
+ /**
7384
+ * Whether a row measures the transport rather than the account/model.
7385
+ *
7386
+ * `auth` carrying the pool's own "no stored credential" message is the one case we
7387
+ * can attribute to ourselves rather than the upstream: the request never left.
7388
+ * Every other rejection came from the upstream and IS a statement about this
7389
+ * account, so it is kept.
7390
+ */
7391
+ function isTransportFailure(row) {
7392
+ return row.outcome === "auth" && row.detail.startsWith("no stored credential");
7393
+ }
7394
+ /** Carry an optional upstream sentence only when it says something. */
7395
+ function nonEmpty(message) {
7396
+ return message === "" ? {} : { message };
7397
+ }
7398
+ /** The default inter-account pause. */
7399
+ function defaultWait(ms) {
7400
+ return new Promise((resolve) => setTimeout(resolve, ms));
7401
+ }
7402
+ /** A safe message from an unknown throwable, bounded in length. */
7403
+ function messageOf(error) {
7404
+ return (error instanceof Error ? error.message : String(error)).slice(0, 300);
7405
+ }
7406
+ //#endregion
7407
+ //#region src/reset-time.ts
7408
+ /**
7409
+ * Parse a reset TIME out of an upstream failure message, when the upstream stated
7410
+ * one.
7411
+ *
7412
+ * 参考:`dsh-connect-workbuddy` 的 `src/probe.ts`(`parseUpstreamResetAt`)。
7413
+ * 该实现在 WorkBuddy 上是实测出来的(其网关的 429 正文里带一句「将在 … 重置」
7414
+ * 与一句英文 `will reset at …`),本文件把同一套解析规则搬过来,并把「偏移量
7415
+ * 必须显式」这条硬约束原样保留。
7416
+ * 改动:诚实标注**证据状态**。Trae 目前被记录在案的限流/配额文案里**没有任何一条
7417
+ * 带时间**:
7418
+ *
7419
+ * - `4011` → `We're sorry, your requests have exceeded the rate limit.
7420
+ * Please wait and try again later.`(docs/ISSUE33_DIAGNOSIS.md:30,实测原文)
7421
+ * - `4008` → `Your requests have exceeded the quota.`(同文档:55,实测原文)
7422
+ *
7423
+ * 所以本函数在 Trae 上大概率永远返回 `undefined`,而**这正是它应有的行为**:
7424
+ * 没有给出的时间不能变成插件自己编的倒计时(池会退回
7425
+ * `POOL_UNKNOWN_COOLDOWN_MS` 的 30 分钟兜底并在卡片上如实显示「上游未给出
7426
+ * 何时恢复」)。它存在的理由只有一个:Trae 的中文网关将来若在正文里写了时间
7427
+ * (与 WorkBuddy 中文网关同一族文案),不要把一个真实存在的时间当没有解析出来
7428
+ * 丢掉 —— 参考实现里那是一条实测的 bug(英文正文解析不到 → 账号比上游要求的
7429
+ * 早 ~18 小时回到池里,再花一次请求重新撞上同一个限制)。
7430
+ *
7431
+ * NEVER use it for a credit PACK's `endTimeMs`: that is when a balance STOPS
7432
+ * being usable, not when a new allowance ARRIVES. Attaching it as a retry time
7433
+ * would put an exhausted account back into rotation at the exact moment its
7434
+ * credit disappears.
7435
+ *
7436
+ * @module dsh-connect-trae/reset-time
7437
+ */
7438
+ /**
7439
+ * The timestamp shape both spellings share: `YYYY-MM-DD HH:MM:SS` followed by an
7440
+ * EXPLICIT UTC offset.
7441
+ *
7442
+ * The offset is required, not optional. `2026-09-30 02:30:30` with no zone is a
7443
+ * moment the plugin would have to guess at, and `Date.parse` guesses LOCAL time —
7444
+ * right on a UTC+8 machine by accident, silently wrong everywhere else. A message
7445
+ * that names a time without a zone therefore yields no time, which is the honest
7446
+ * answer.
7447
+ */
7448
+ const RESET_STAMP = "(\\d{4})-(\\d{2})-(\\d{2})\\s+(\\d{2}):(\\d{2}):(\\d{2})\\s*UTC\\s*([+-])(\\d{1,2})(?::?(\\d{2}))?";
7449
+ /** Chinese spelling: the marker sits AFTER the timestamp (将在 … 重置). */
7450
+ const UPSTREAM_RESET_PATTERN_ZH = new RegExp(`将在?\\s*${RESET_STAMP}[\\s\\S]*?重置`, "u");
7451
+ /**
7452
+ * English spelling: the marker sits BEFORE the timestamp (`reset at` /
7453
+ * `refresh on`). It needs its own pattern because the two languages put the
7454
+ * keyword on different sides of the time, so loosening the surrounding prose
7455
+ * alone cannot cover both.
7456
+ */
7457
+ const UPSTREAM_RESET_PATTERN_EN = new RegExp(`(?:reset|resets|refresh|refreshes|renew|renews)\\s+(?:at|on)\\s+${RESET_STAMP}`, "iu");
7458
+ /**
7459
+ * Parse the reset time out of an upstream failure message.
7460
+ *
7461
+ * Returns epoch ms, or undefined when the message names no parsable time — the
7462
+ * caller must then say "the upstream gave no time", never invent one.
7463
+ *
7464
+ * THE OFFSET IS HONOURED, NOT ASSUMED. `2026-09-30 02:30:30 UTC+8` is 18:30:30 UTC
7465
+ * the previous day, so the components are assembled with `Date.UTC` and the stated
7466
+ * offset subtracted — correct on every host regardless of its own zone.
7467
+ *
7468
+ * A whole-second resolution is what the upstream prints; sub-second precision would
7469
+ * be fiction in the other direction.
7470
+ */
7471
+ function parseUpstreamResetAt(message) {
7472
+ const match = UPSTREAM_RESET_PATTERN_ZH.exec(message) ?? UPSTREAM_RESET_PATTERN_EN.exec(message);
7473
+ if (match === null) return void 0;
7474
+ const [, year, month, day, hour, minute, second, sign, offsetHours, offsetMinutes] = match;
7475
+ const offsetTotalMinutes = (sign === "-" ? -1 : 1) * (Number(offsetHours) * 60 + Number(offsetMinutes ?? "0"));
7476
+ const utcMs = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
7477
+ const parsed = new Date(utcMs);
7478
+ if (parsed.getUTCFullYear() !== Number(year) || parsed.getUTCMonth() !== Number(month) - 1 || parsed.getUTCDate() !== Number(day) || parsed.getUTCHours() !== Number(hour) || parsed.getUTCMinutes() !== Number(minute) || parsed.getUTCSeconds() !== Number(second)) return;
7479
+ if (Math.abs(offsetTotalMinutes) > 840) return void 0;
7480
+ return utcMs - offsetTotalMinutes * 6e4;
7481
+ }
7482
+ //#endregion
7483
+ //#region src/account-pool-store.ts
7484
+ /**
7485
+ * The account pool's RUNTIME store: measured facts, on disk, outside settings.
7486
+ *
7487
+ * 参考:`dsh-connect-workbuddy` 的 `src/account-pool-store.ts`,以及本仓库
7488
+ * `src/auth.ts` 的插件自有文件约定(`$DSH_HOME` 下的 `.trae-auth.<region>.json`,
7489
+ * `withFileLock` + `writeFileAtomic`,`mode: 0o600`)。
7490
+ * 改动:一份、按**面板**分文件(cn / enterprise / ai),因为 enterprise 与 cn 共用
7491
+ * 同一份凭据却走不同的网关 —— 一个账号在个人 SOLO 通道能用,完全不说明它过得了
7492
+ * 租户门(`4019` 只在第一次真实对话时暴露),所以两份测量必须分开落盘。
7493
+ *
7494
+ * 为什么这些事实不放 settings(`regions[panel].pool`):
7495
+ *
7496
+ * 卡片对用户偏好用的是「草稿 → 保存 → 丢弃草稿」模式:保存成功会丢弃草稿。
7497
+ * 如果探测结果也住在那个槽里,用户手工点一次「保存」就会用**几分钟前的草稿**
7498
+ * 覆盖掉刚刚写进去的测量结果 —— 那等于把测量回退,测试白做。
7499
+ * 观测数据由插件高频写入、且不是用户偏好,所以它必须落在自己的文件里。
7500
+ *
7501
+ * 本模块的读写**从不抛异常给调用方**:池是增强功能,一个损坏的观测文件绝不能让
7502
+ * 卡片或 host 启动失败 —— 最坏情况是「回到未测试」,而不是整个插件不可用。
7503
+ *
7504
+ * @module dsh-connect-trae/account-pool-store
7505
+ */
7506
+ /** On-disk format version; readers reject anything else. */
7507
+ const STORE_FORMAT_VERSION = 1;
7508
+ /** Filename prefix for one panel's measured pool facts. */
7509
+ const POOL_STORE_PREFIX = ".trae-pool";
7510
+ /** Whether a parsed value looks like a stored probe. */
7511
+ function isProbe(value) {
7512
+ if (typeof value !== "object" || value === null) return false;
7513
+ const candidate = value;
7514
+ return typeof candidate["outcome"] === "string" && typeof candidate["atMs"] === "number" && (candidate["retryAtMs"] === void 0 || typeof candidate["retryAtMs"] === "number") && (candidate["message"] === void 0 || typeof candidate["message"] === "string") && (candidate["accountName"] === void 0 || typeof candidate["accountName"] === "string") && (candidate["modelId"] === void 0 || typeof candidate["modelId"] === "string") && (candidate["source"] === void 0 || typeof candidate["source"] === "string");
7515
+ }
7516
+ /**
7517
+ * One panel's path for measured pool facts.
7518
+ *
7519
+ * Panel-keyed for the same reason the credential copies are region-keyed: the
7520
+ * panels are parallel stacks and must never overwrite each other's measurements.
7521
+ */
7522
+ function traePoolStorePath(panel) {
7523
+ return join(resolveDshHome(), `${POOL_STORE_PREFIX}.${panel}.json`);
7524
+ }
7525
+ /**
7526
+ * The recovery time a stored record implies — declared, or recoverable from its
7527
+ * own body.
7528
+ *
7529
+ * The field itself wins when present: it is what the writer computed with the
7530
+ * full input set, and re-deriving it could only lose information.
7531
+ *
7532
+ * The fallback exists because a record can state a time in its `message` and
7533
+ * still have no `retryAtMs`. Read-time healing is the only place this can be
7534
+ * repaired for EXISTING records: every consumer receives probes through
7535
+ * {@link readTraePoolProbes}, so healing here fixes all of them at once. Re-
7536
+ * deriving at each consumer instead would be several copies of one rule, and the
7537
+ * next consumer added would silently miss it.
7538
+ *
7539
+ * Not a permanent second derivation: {@link writeTraePoolProbes} reads through
7540
+ * this function before it writes, so the very next write persists the healed
7541
+ * value.
7542
+ *
7543
+ * Scoped to the two outcomes whose vocabulary HAS a stated time. Widening it
7544
+ * would let an incidental sentence in an `ok` body attach a countdown to a
7545
+ * healthy account — the card prints any present `retryAtMs` beside the outcome,
7546
+ * so that would render a waiting time on an account waiting for nothing.
7547
+ */
7548
+ function retryAtMsOf(probe) {
7549
+ if (probe.retryAtMs !== void 0) return probe.retryAtMs;
7550
+ if (probe.outcome !== "rate-limited" && probe.outcome !== "out-of-credit") return void 0;
7551
+ if (probe.message === void 0) return void 0;
7552
+ return parseUpstreamResetAt(probe.message);
7553
+ }
7554
+ /**
7555
+ * Read one panel's measured facts.
7556
+ *
7557
+ * Contains NO credentials. The one free-text field is `message`, and it is
7558
+ * REDACTED BY THE WRITER before it ever reaches this file, so a token-shaped
7559
+ * string cannot survive into storage.
7560
+ *
7561
+ * Returns an empty map for a missing, unreadable, malformed, or
7562
+ * version-mismatched file: all four mean "nothing measured yet", which is a valid
7563
+ * state the pool renders as "untested".
7564
+ */
7565
+ async function readTraePoolProbes(panel) {
7566
+ let raw;
7567
+ try {
7568
+ raw = await readFile(traePoolStorePath(panel), "utf8");
7569
+ } catch {
7570
+ return {};
7571
+ }
7572
+ let parsed;
7573
+ try {
7574
+ parsed = JSON.parse(raw);
7575
+ } catch {
7576
+ return {};
7577
+ }
7578
+ if (typeof parsed !== "object" || parsed === null) return {};
7579
+ const document = parsed;
7580
+ if (document.version !== STORE_FORMAT_VERSION) return {};
7581
+ if (typeof document.probes !== "object" || document.probes === null) return {};
7582
+ const probes = {};
7583
+ for (const [accountId, probe] of Object.entries(document.probes)) {
7584
+ if (accountId === "" || !isProbe(probe)) continue;
7585
+ const retryAtMs = retryAtMsOf(probe);
7586
+ const source = knownProbeSource(probe.source);
7587
+ probes[accountId] = {
7588
+ outcome: probe.outcome,
7589
+ atMs: probe.atMs,
7590
+ ...source === void 0 ? {} : { source },
7591
+ ...retryAtMs === void 0 ? {} : { retryAtMs },
7592
+ ...probe.message === void 0 ? {} : { message: probe.message },
7593
+ ...probe.accountName === void 0 ? {} : { accountName: probe.accountName },
7594
+ ...probe.modelId === void 0 ? {} : { modelId: probe.modelId }
7595
+ };
7596
+ }
7597
+ return probes;
7598
+ }
7599
+ /**
7600
+ * A stored source string reduced to a source this build understands.
7601
+ *
7602
+ * `isProbe` accepts any string so a newer build's label cannot destroy the
7603
+ * measurement it describes; this narrows it at the point of use. An unknown value
7604
+ * becomes `undefined`, which the card renders as "unknown" — the honest answer,
7605
+ * rather than mislabelling it as one of the two kinds we do know.
7606
+ */
7607
+ function knownProbeSource(value) {
7608
+ return value === "test-batch" || value === "live-request" ? value : void 0;
7609
+ }
7610
+ /**
7611
+ * Merge ONE new measurement over the record already stored for that account.
7612
+ *
7613
+ * A plain `{ ...existing, ...update }` is wrong here, and the damage is silent.
7614
+ * The two writers describe the same account at different moments and carry
7615
+ * different fields:
7616
+ *
7617
+ * - `test-batch` measures the TARGET MODEL deliberately, so it is the writer
7618
+ * that can produce a reset point (`retryAtMs`) parsed out of the upstream's
7619
+ * own failure body.
7620
+ * - `live-request` reports what a real chat request just hit. It often carries
7621
+ * no `retryAtMs` at all (a transport blip names no time), and a spread would
7622
+ * therefore DELETE the reset point the probe had already resolved.
7623
+ *
7624
+ * Measured consequence of the spread (reference implementation, a real
7625
+ * quota-exhausted account): the probe recorded "quota refreshes 2026-11-01", the
7626
+ * next live failure rewrote the record without that field, and the pool then
7627
+ * reported "retry in about 30 minutes" — putting an exhausted account back into
7628
+ * rotation to be refused again. The correct value was in the file one write
7629
+ * earlier.
7630
+ *
7631
+ * So `retryAtMs` is INHERITED when the update does not state one, and every other
7632
+ * field is the update's (the newer measurement owns `outcome`, `atMs`, `source`
7633
+ * and `message`). Inheriting is only about the one field whose absence is not a
7634
+ * statement: an update that DOES carry a time still overrides, which is what lets
7635
+ * a fresh, shorter cooldown supersede a stale longer one.
7636
+ *
7637
+ * `accountName` is INHERITED for the same reason: its absence is not a statement.
7638
+ * A writer that could not see the account must not erase the name an earlier
7639
+ * measurement DID see — that name is the only thing that can identify a member
7640
+ * whose sign-in has gone.
7641
+ *
7642
+ * `modelId` is deliberately NOT inherited (see the field note in
7643
+ * `account-pool.ts`): it describes THIS measurement, not the account.
7644
+ *
7645
+ * Pure and exported so the rule is unit-testable without a filesystem — this is
7646
+ * exactly the kind of "two writers, one slot" rule that bites silently.
7647
+ */
7648
+ function mergePoolProbe(existing, update) {
7649
+ if (existing === void 0) return update;
7650
+ const merged = update.accountName === void 0 && existing.accountName !== void 0 ? {
7651
+ ...update,
7652
+ accountName: existing.accountName
7653
+ } : update;
7654
+ if (update.retryAtMs !== void 0) return merged;
7655
+ if (existing.retryAtMs === void 0 || existing.retryAtMs <= update.atMs) return merged;
7656
+ return {
7657
+ ...merged,
7658
+ retryAtMs: existing.retryAtMs
7659
+ };
7660
+ }
7661
+ /**
7662
+ * Merge measurements into one panel's store and write it atomically.
7663
+ *
7664
+ * A MERGE rather than a replace, because batches measure one account at a time and
7665
+ * a partial failure must not erase what earlier runs learned. An account absent
7666
+ * from `updates` keeps its previous measurement, and an account present in both is
7667
+ * combined by {@link mergePoolProbe} rather than overwritten.
7668
+ *
7669
+ * Never throws: a failed write loses only the newest measurements, and the
7670
+ * caller's results are still returned to the user. Surfacing a disk error here
7671
+ * would fail an operation that actually succeeded.
7672
+ */
7673
+ async function writeTraePoolProbes(panel, updates) {
7674
+ if (Object.keys(updates).length === 0) return;
7675
+ const path = traePoolStorePath(panel);
7676
+ try {
7677
+ await withFileLock(path, async () => {
7678
+ const existing = await readTraePoolProbes(panel);
7679
+ const merged = { ...existing };
7680
+ for (const [accountId, update] of Object.entries(updates)) merged[accountId] = mergePoolProbe(existing[accountId], update);
7681
+ await writeFileAtomic(path, `${JSON.stringify({
7682
+ version: STORE_FORMAT_VERSION,
7683
+ probes: merged
7684
+ }, null, 2)}\n`, {
7685
+ mode: 384,
7686
+ dirMode: 448
7687
+ });
7688
+ });
7689
+ } catch {}
7690
+ }
7691
+ /**
7692
+ * Forget one account's measurement, keeping every other row.
7693
+ *
7694
+ * Used when the user removes a pool member: its measurement is no longer about a
7695
+ * member, and leaving it in the file would resurrect a stale `rate-limited` row if
7696
+ * the account were added back later. Written through the same lock/atomic path so a
7697
+ * removal cannot interleave with a batch.
7698
+ */
7699
+ async function dropTraePoolProbe(panel, accountId) {
7700
+ const existing = await readTraePoolProbes(panel);
7701
+ if (existing[accountId] === void 0) return;
7702
+ const path = traePoolStorePath(panel);
7703
+ const probes = {};
7704
+ for (const [id, probe] of Object.entries(existing)) if (id !== accountId) probes[id] = probe;
7705
+ try {
7706
+ await withFileLock(path, async () => {
7707
+ await writeFileAtomic(path, `${JSON.stringify({
7708
+ version: STORE_FORMAT_VERSION,
7709
+ probes
7710
+ }, null, 2)}\n`, {
7711
+ mode: 384,
7712
+ dirMode: 448
7713
+ });
7714
+ });
7715
+ } catch {}
7716
+ }
7717
+ //#endregion
6297
7718
  //#region src/model-config.ts
6298
7719
  /**
6299
7720
  * Parse a secret-free CustomModel debug line emitted by Trae. Optional values
@@ -6615,7 +8036,13 @@ const regionStateConfig = z.object({
6615
8036
  enabledModelIds: z.array(z.string()).default([]),
6616
8037
  imageModelIds: z.array(z.string()).default([]),
6617
8038
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}),
6618
- creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("积分读数显示位置:\"composer\" = 输入框工具栏(默认),\"stats\" = 输入框下方(与「轮次 / 步数 / token」统计同一行)。装了别的插件后读数不显示时改成 stats。")
8039
+ creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("积分读数显示位置:\"composer\" = 输入框工具栏(默认),\"stats\" = 输入框下方(与「轮次 / 步数 / token」统计同一行)。装了别的插件后读数不显示时改成 stats。"),
8040
+ pool: z.object({
8041
+ enabled: z.boolean().default(false).description("账号池:打开后本面板由池中可用账号轮流计费(关闭时按下面的绑定账号)"),
8042
+ targetModelId: z.string().default("").description("账号池测速使用的模型 id(留空 = 自动选择免费模型)"),
8043
+ memberAccountIds: z.array(z.string()).default([]).description("加入本面板账号池的账号 id(显式勾选,默认为空)"),
8044
+ probeInputTokens: z.number().default(0).description("账号池测试请求填充的 token 量(0 = 最小请求,不填充)")
8045
+ }).default({})
6619
8046
  });
6620
8047
  const accountSelectionConfig = z.object({
6621
8048
  cn: z.string().description("Selected domestic (CN) account id (never a token)"),
@@ -6644,6 +8071,28 @@ const Config = z.object({
6644
8071
  });
6645
8072
  /** Every region, in card tab order. */
6646
8073
  const REGION_KEYS = ["cn", "ai"];
8074
+ /**
8075
+ * What ONE failed real request means for the account that served it (issue #44).
8076
+ *
8077
+ * The shim only reports account-scoped kinds here, so this table never has to
8078
+ * interpret `not_found` (a model the account cannot see) or `client` (our own bad
8079
+ * body) — both would exclude a healthy account for good over something that is
8080
+ * not about the account at all.
8081
+ *
8082
+ * `credential-rejected` is the one permanent entry, and deliberately so: a 401
8083
+ * from the chat gateway means the stored token cannot be refreshed into one that
8084
+ * works, and the honest recovery is a re-login (which the card offers), not a
8085
+ * silent retry every half hour.
8086
+ */
8087
+ const POOL_OUTCOME_BY_KIND = {
8088
+ authentication: "credential-rejected",
8089
+ hard_credit: "out-of-credit",
8090
+ soft_rate: "rate-limited",
8091
+ server: "unavailable",
8092
+ not_found: "not-found",
8093
+ client: "failed",
8094
+ unconfigured: "failed"
8095
+ };
6647
8096
  function apply(ctx, config) {
6648
8097
  const settingsNs = settingsNamespaceOf(ctx);
6649
8098
  let current = () => config;
@@ -6854,7 +8303,8 @@ function apply(ctx, config) {
6854
8303
  };
6855
8304
  shimRoutes.push({
6856
8305
  label: route,
6857
- client: delegating
8306
+ client: delegating,
8307
+ pool: poolShimHooksOf(panel)
6858
8308
  });
6859
8309
  }
6860
8310
  const shim = createTraeShim({
@@ -6922,11 +8372,13 @@ function apply(ctx, config) {
6922
8372
  * A throwaway client is used on purpose: the shared `usageClient` reads
6923
8373
  * `store.resolve()`, which is bound to the selected account, and calling
6924
8374
  * `selectAccount` to look at a neighbour would both race concurrent chat
6925
- * requests and persist a switch the user never made. A stale token simply
6926
- * fails here and the account is left out of the answer.
8375
+ * requests and persist a switch the user never made. The credential is
8376
+ * resolved (not merely looked up) so an account whose short-lived access
8377
+ * token lapsed overnight still answers: its refresh token is good for
8378
+ * months, and reporting `—` for a recoverable account is a lie.
6927
8379
  */
6928
8380
  creditsOfAccount: async (accountId) => {
6929
- const credential = await store.credentialOf(accountId);
8381
+ const credential = await store.resolveCredentialOf(accountId);
6930
8382
  if (credential === void 0) return void 0;
6931
8383
  return toCredits(await new TraeUsageClient({ credential: async () => credential }).snapshot());
6932
8384
  },
@@ -6934,7 +8386,7 @@ function apply(ctx, config) {
6934
8386
  const state = { id: accountId };
6935
8387
  const selectedCredential = await store.current();
6936
8388
  const client = selectedCredential !== void 0 && traeAccountId(selectedCredential) === accountId ? usageClient : (() => {
6937
- const credential = store.credentialOf(accountId);
8389
+ const credential = store.resolveCredentialOf(accountId);
6938
8390
  return new TraeUsageClient({
6939
8391
  credential: async () => credential,
6940
8392
  deviceId: async () => (await identity()).deviceId
@@ -6952,7 +8404,7 @@ function apply(ctx, config) {
6952
8404
  claimCheckinOf: async (accountId) => {
6953
8405
  const selectedCredential = await store.current();
6954
8406
  const client = selectedCredential !== void 0 && traeAccountId(selectedCredential) === accountId ? usageClient : (() => {
6955
- const credential = store.credentialOf(accountId);
8407
+ const credential = store.resolveCredentialOf(accountId);
6956
8408
  return new TraeUsageClient({
6957
8409
  credential: async () => credential,
6958
8410
  deviceId: async () => (await identity()).deviceId
@@ -7012,10 +8464,389 @@ function apply(ctx, config) {
7012
8464
  };
7013
8465
  },
7014
8466
  invalidateAdapter: () => {},
7015
- rawDiagnostic: () => rawDiagnostic()
8467
+ rawDiagnostic: () => rawDiagnostic(),
8468
+ wireResolver,
8469
+ identity,
8470
+ routeOf: (provider) => routeOfTraeProvider(provider) ?? "solo"
8471
+ };
8472
+ }
8473
+ /** This panel's saved pool preferences, unwrapped and defaulted to OFF. */
8474
+ const poolPreferencesOf = (panel) => unwrapVolatileDeep(regionStateOf(current(), panel).pool) ?? {};
8475
+ /**
8476
+ * Project one account's card credits onto the pool's ranking shape.
8477
+ *
8478
+ * The pool's `total` is the figure a request can SPEND, which on Trae is the
8479
+ * General bucket alone (SOLO chat never drains Work). The card's `total` is the
8480
+ * wider `available` figure, so it is deliberately NOT reused here: ranking by it
8481
+ * would put an account with a large Work balance and no General credit in front
8482
+ * of one that can actually serve. `undefined` (the read failed) stays undefined
8483
+ * so `rankPool` treats it as zero rather than as full.
8484
+ */
8485
+ /**
8486
+ * Credit totals the pool ranks on, cached per account.
8487
+ *
8488
+ * The CHAT path must not spend an upstream round-trip per candidate just to
8489
+ * decide who bills: {@link poolMembersOf} reads every balance, which is right
8490
+ * for the card (one read per poll) and far too slow per request. So the card's
8491
+ * poll warms this cache and the router only reads it.
8492
+ *
8493
+ * A cold cache means "unknown", not "no credit": `rankPool` orders a missing
8494
+ * figure as zero and then falls back to token freshness, so a cold start still
8495
+ * picks a member — the user chose the roster — instead of refusing to bill.
8496
+ */
8497
+ const poolCreditCache = {
8498
+ cn: /* @__PURE__ */ new Map(),
8499
+ enterprise: /* @__PURE__ */ new Map(),
8500
+ ai: /* @__PURE__ */ new Map()
8501
+ };
8502
+ const poolCreditsOfAccount = async (panel, accountId) => {
8503
+ const credits = await stacks[panel].creditsOfAccount(accountId);
8504
+ if (credits === void 0) return void 0;
8505
+ const within3d = credits.expiry?.within3d;
8506
+ const projected = {
8507
+ total: credits.generalAvailable,
8508
+ generalAvailable: credits.generalAvailable,
8509
+ workAvailable: credits.workAvailable,
8510
+ expiringSoon: within3d?.total ?? 0,
8511
+ ...within3d?.earliestExpiresAtMs === void 0 ? {} : { nearestExpiryMs: within3d.earliestExpiresAtMs }
8512
+ };
8513
+ poolCreditCache[panel].set(accountId, projected);
8514
+ return projected;
8515
+ };
8516
+ /**
8517
+ * Assemble one pool member per LOCAL account, in the store's order.
8518
+ *
8519
+ * "Member" means the account still has a sign-in this machine can use: a saved
8520
+ * id whose desktop credentials vanished is a ghost, and a ghost cannot be tested
8521
+ * or checked in, so listing it as a member would enable a batch over nothing.
8522
+ * The saved list is intersected separately (see
8523
+ * {@link TraePoolDeps.effectiveMemberAccountIds}) — here the store is the source.
8524
+ *
8525
+ * Balance reads are settled independently: one account whose credit read fails
8526
+ * must not take the whole pool document down, and it ranks as zero rather than
8527
+ * disappearing.
8528
+ */
8529
+ const poolMembersOf = async (panel) => {
8530
+ const probes = await readTraePoolProbes(poolKeyOfPanel(panel));
8531
+ const choices = await stacks[panel].store.accounts();
8532
+ return await Promise.all(choices.map(async (choice) => {
8533
+ const credits = await poolCreditsOfAccount(panel, choice.id).catch(() => void 0);
8534
+ return {
8535
+ account: {
8536
+ id: choice.id,
8537
+ accountName: choice.accountName
8538
+ },
8539
+ ...credits === void 0 ? {} : { credits },
8540
+ ...probes[choice.id] === void 0 ? {} : { probe: probes[choice.id] },
8541
+ ...choice.tokenExpiresAtMs === void 0 ? {} : { tokenExpiresAtMs: choice.tokenExpiresAtMs }
8542
+ };
8543
+ }));
8544
+ };
8545
+ /**
8546
+ * The catalog a pool test resolves its target model against.
8547
+ *
8548
+ * The LAST discovery wins, and the saved/live catalog is the fallback: a test
8549
+ * clicked before the first discovery lands must still find the free models the
8550
+ * region already knows, and one clicked after it must not be judged against a
8551
+ * roster the upstream has since replaced.
8552
+ */
8553
+ const poolCatalogOf = (panel) => stacks[panel].lastDiscovery.length > 0 ? stacks[panel].lastDiscovery : stacks[panel].catalog.current();
8554
+ /** Id → display name for one panel, for the rows a batch reports. */
8555
+ const poolMemberNamesOf = async (panel) => {
8556
+ const names = {};
8557
+ for (const member of await poolMembersOf(panel)) names[member.account.id] = member.account.accountName;
8558
+ return names;
8559
+ };
8560
+ /**
8561
+ * The accounts a batch runs on: the SAVED member list, narrowed to what this
8562
+ * machine can still reach, in the user's own order.
8563
+ *
8564
+ * Order is the user's, not the ranking's, on purpose: a batch result table that
8565
+ * reorders itself between presses is hard to read row by row, and the ranking
8566
+ * is already shown by the table above it.
8567
+ */
8568
+ const poolTargetsOf = async (panel) => {
8569
+ const saved = poolPreferencesOf(panel).memberAccountIds ?? [];
8570
+ const names = await poolMemberNamesOf(panel);
8571
+ return effectiveMembersOf(saved, new Set(Object.keys(names))).map((accountId) => ({
8572
+ accountId,
8573
+ accountName: names[accountId] ?? ""
8574
+ }));
8575
+ };
8576
+ /**
8577
+ * Pool members from LOCAL state only: the store's logins, the saved probes and
8578
+ * the cached credit totals. No upstream call, so it is safe on the request
8579
+ * path (see {@link poolCreditCache} for why the cache exists).
8580
+ */
8581
+ async function localPoolMembers(panel) {
8582
+ const probes = await readTraePoolProbes(poolKeyOfPanel(panel)).catch(() => ({}));
8583
+ const choices = await stacks[panel].store.accounts().catch(() => []);
8584
+ const cache = poolCreditCache[panel];
8585
+ return choices.map((choice) => {
8586
+ const credits = cache.get(choice.id);
8587
+ return {
8588
+ account: {
8589
+ id: choice.id,
8590
+ accountName: choice.accountName
8591
+ },
8592
+ ...credits === void 0 ? {} : { credits },
8593
+ ...probes[choice.id] === void 0 ? {} : { probe: probes[choice.id] },
8594
+ ...choice.tokenExpiresAtMs === void 0 ? {} : { tokenExpiresAtMs: choice.tokenExpiresAtMs }
8595
+ };
8596
+ });
8597
+ }
8598
+ /**
8599
+ * The members that may actually be billed: the user's SAVED opt-in list
8600
+ * intersected with what this machine can still reach, minus the accounts that
8601
+ * already failed this request (`billableMembers` is the one definition of that
8602
+ * rule, shared with the batch paths).
8603
+ */
8604
+ async function poolBillableMembers(panel, tried = []) {
8605
+ return billableMembers(await localPoolMembers(panel), poolPreferencesOf(panel).memberAccountIds ?? [], tried);
8606
+ }
8607
+ /**
8608
+ * Point the NEXT request at the best pooled account, or back at the bound one
8609
+ * when the pool is off (issue #44).
8610
+ *
8611
+ * Returns the account it chose. Both the chat path and the card's usage read go
8612
+ * through here, so "currently serving" on screen and the next real request
8613
+ * cannot disagree — one rule, one answer.
8614
+ *
8615
+ * When EVERY member is excluded the rotation is cleared rather than pointed at
8616
+ * an excluded account: the bound account then serves, which is the user's own
8617
+ * explicit choice and the only account a refusal would not have served anyway.
8618
+ * The card reports the exclusion reasons, so this is never silent.
8619
+ */
8620
+ async function applyPoolSelection(panel) {
8621
+ const store = stacks[panel].store;
8622
+ if (poolPreferencesOf(panel).enabled !== true) {
8623
+ store.setRotatedAccount(void 0);
8624
+ return;
8625
+ }
8626
+ try {
8627
+ const winner = pickAccount(await poolBillableMembers(panel), Date.now());
8628
+ store.setRotatedAccount(winner?.id);
8629
+ return winner?.id;
8630
+ } catch {
8631
+ store.setRotatedAccount(void 0);
8632
+ return;
8633
+ }
8634
+ }
8635
+ /**
8636
+ * The client for the next account worth trying, bound to THAT account.
8637
+ *
8638
+ * Built per attempt from the account's own stored credential, so a failover
8639
+ * never rebinds the store and never disturbs a request that is already billing
8640
+ * the chosen account (the same rule per-account probes follow, issue #25). The
8641
+ * bridge is the same one a normal turn uses, so display ids still resolve to
8642
+ * their wire names.
8643
+ */
8644
+ async function failoverClientFor(panel, tried) {
8645
+ const next = pickAccount(await poolBillableMembers(panel, tried), Date.now());
8646
+ if (next === void 0) return void 0;
8647
+ const stack = stacks[panel];
8648
+ const accountId = next.id;
8649
+ return {
8650
+ accountId,
8651
+ client: new TraeSoloBridge(new TraeSoloUpstreamClient({
8652
+ credential: async () => {
8653
+ const credential = await stack.store.resolveCredentialOf(accountId);
8654
+ if (credential === void 0) throw new Error(`no stored credential for account ${accountId}`);
8655
+ return credential;
8656
+ },
8657
+ identity: () => stack.identity(),
8658
+ route: stack.routeOf(providerOfPanel(panel)),
8659
+ log: (message, detail) => ctx.logger.warn(message, detail)
8660
+ }), stack.catalog, stack.wireResolver)
8661
+ };
8662
+ }
8663
+ /**
8664
+ * Persist what a real request taught us about an account (issue #44).
8665
+ *
8666
+ * Written as a `live-request` probe so the card can tell a measurement taken by
8667
+ * a batch from one taken by traffic, and so the ranking excludes the account on
8668
+ * its next turn instead of walking back into the same limit.
8669
+ */
8670
+ async function recordPoolFailure(panel, accountId, kind, message) {
8671
+ const outcome = POOL_OUTCOME_BY_KIND[kind];
8672
+ if (outcome === void 0) return;
8673
+ await writeTraePoolProbes(poolKeyOfPanel(panel), { [accountId]: {
8674
+ outcome,
8675
+ atMs: Date.now(),
8676
+ source: "live-request",
8677
+ message: `${kind}: ${message}`
8678
+ } }).catch(() => {});
8679
+ }
8680
+ /**
8681
+ * Why nothing else could serve, as one clause for the error the caller sees.
8682
+ *
8683
+ * `undefined` when the pool is off (the plain upstream error is the whole
8684
+ * story) or when there was in fact a candidate left — the shim asked because it
8685
+ * ran out of attempts, not out of accounts.
8686
+ */
8687
+ async function poolShortfallOf(panel, tried) {
8688
+ if (poolPreferencesOf(panel).enabled !== true) return void 0;
8689
+ let members;
8690
+ let remaining;
8691
+ try {
8692
+ members = await poolBillableMembers(panel);
8693
+ if (members.length === 0) return "the account pool is enabled but has no usable member";
8694
+ remaining = await poolBillableMembers(panel, tried);
8695
+ } catch {
8696
+ return;
8697
+ }
8698
+ if (remaining.length === 0) return `all ${members.length} pooled account(s) were tried for this request`;
8699
+ const ranked = rankPool(remaining, Date.now());
8700
+ const reasons = [...new Set(ranked.flatMap((entry) => entry.excludedBy === void 0 ? [] : [entry.excludedBy]))];
8701
+ return reasons.length === 0 ? void 0 : `every remaining pooled account is excluded (${reasons.join(", ")})`;
8702
+ }
8703
+ /**
8704
+ * The shim's view of one panel's pool.
8705
+ *
8706
+ * Declared as a hoisted function because the shim's route table is built ABOVE
8707
+ * this point in `apply`; only the bodies run later, on a real request.
8708
+ */
8709
+ function poolShimHooksOf(panel) {
8710
+ return {
8711
+ prepare: () => applyPoolSelection(panel),
8712
+ next: (tried) => failoverClientFor(panel, tried),
8713
+ failed: (accountId, kind, message) => recordPoolFailure(panel, accountId, kind, message),
8714
+ shortfall: (tried) => poolShortfallOf(panel, tried)
7016
8715
  };
7017
8716
  }
7018
8717
  /**
8718
+ * The pool's dependency bag for ONE panel.
8719
+ *
8720
+ * Every member re-reads the store on each call rather than capturing a list:
8721
+ * a batch runs for minutes, and a sign-in the user performed half-way through
8722
+ * must be visible to the rows it has not reached yet.
8723
+ *
8724
+ * The bag is built per REQUEST, so it captures the panel and never has to be
8725
+ * told which tab asked.
8726
+ */
8727
+ const poolDepsOf = (panel) => {
8728
+ const bridges = /* @__PURE__ */ new Map();
8729
+ const chatBridgeFor = (accountId) => {
8730
+ const existing = bridges.get(accountId);
8731
+ if (existing !== void 0) return existing;
8732
+ const stack = stacks[panel];
8733
+ const bridge = new TraeSoloBridge(new TraeSoloUpstreamClient({
8734
+ credential: async () => {
8735
+ const credential = await stack.store.resolveCredentialOf(accountId);
8736
+ if (credential === void 0) throw new Error(`no stored credential for account ${accountId}`);
8737
+ return credential;
8738
+ },
8739
+ identity: () => stack.identity(),
8740
+ route: stack.routeOf(providerOfPanel(panel)),
8741
+ log: (message, detail) => ctx.logger.warn(message, detail)
8742
+ }), stack.catalog, stack.wireResolver);
8743
+ const chat = (body, signal) => bridge.chatStream(body, signal);
8744
+ bridges.set(accountId, chat);
8745
+ return chat;
8746
+ };
8747
+ const probeDeps = () => ({
8748
+ credentialFor: (accountId) => stacks[panel].store.resolveCredentialOf(accountId),
8749
+ checkinStatusOf: async (credential) => {
8750
+ return new TraeUsageClient({
8751
+ credential: async () => credential,
8752
+ deviceId: async () => (await stacks[panel].identity()).deviceId
8753
+ }).checkinStatus();
8754
+ },
8755
+ claimCheckinOf: async (credential) => {
8756
+ return new TraeUsageClient({
8757
+ credential: async () => credential,
8758
+ deviceId: async () => (await stacks[panel].identity()).deviceId
8759
+ }).claimCheckin();
8760
+ },
8761
+ chatFor: (credential) => chatBridgeFor(traeAccountId(credential))
8762
+ });
8763
+ /** How much text a probe should carry for this panel (0 = the minimal probe). */
8764
+ const probeInputTokensOf = (stored) => {
8765
+ const tokens = poolPreferencesOf(stored).probeInputTokens;
8766
+ return typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0 ? Math.min(tokens, MAX_PROBE_INPUT_TOKENS) : 0;
8767
+ };
8768
+ /** One account's row, through the same bridge and probe the batch uses. */
8769
+ const testWith = async (target, modelId, probeInputTokens) => {
8770
+ const [row] = await testAllAccounts([target], modelId, (chatFor, credential, id) => {
8771
+ if (credential === void 0) return Promise.resolve({
8772
+ id,
8773
+ verdict: "auth",
8774
+ detail: "no stored credential for this account"
8775
+ });
8776
+ return probeOneModel(chatFor(credential), id, void 0, probeInputTokens);
8777
+ }, probeDeps());
8778
+ return row;
8779
+ };
8780
+ return {
8781
+ preferences: (stored) => poolPreferencesOf(stored),
8782
+ members: (stored) => poolMembersOf(stored),
8783
+ effectiveMemberAccountIds: async (stored) => {
8784
+ return effectiveMembersOf(poolPreferencesOf(stored).memberAccountIds ?? [], new Set((await poolMembersOf(stored)).map((member) => member.account.id)));
8785
+ },
8786
+ memberNames: async (stored) => {
8787
+ const names = {};
8788
+ for (const member of await poolMembersOf(stored)) names[member.account.id] = member.account.accountName;
8789
+ return names;
8790
+ },
8791
+ otherAccounts: async (stored) => (await poolMembersOf(stored)).map((member) => member.account),
8792
+ currentAccountId: async (stored) => {
8793
+ const configured = configuredAccountsOf(current())[stored];
8794
+ return isTraeAccountId(configured) ? configured : void 0;
8795
+ },
8796
+ catalog: (stored) => poolCatalogOf(stored),
8797
+ checkinSupported: (stored) => regionOfPanel(stored) === "cn",
8798
+ checkin: async (stored) => checkinAllAccounts(await poolTargetsOf(stored), probeDeps()),
8799
+ checkinState: async (stored) => {
8800
+ const state = {};
8801
+ for (const target of await poolTargetsOf(stored)) {
8802
+ const credential = await stacks[stored].store.resolveCredentialOf(target.accountId);
8803
+ if (credential === void 0) continue;
8804
+ const client = new TraeUsageClient({
8805
+ credential: async () => credential,
8806
+ deviceId: async () => (await stacks[stored].identity()).deviceId
8807
+ });
8808
+ try {
8809
+ const status = await client.checkinStatus();
8810
+ state[target.accountId] = {
8811
+ checkedIn: status.checkedIn,
8812
+ deviceCheckedIn: status.didCheckedIn,
8813
+ enabled: status.enabled
8814
+ };
8815
+ } catch {}
8816
+ }
8817
+ return state;
8818
+ },
8819
+ test: async (stored, modelId, onRow) => {
8820
+ const probeInputTokens = probeInputTokensOf(stored);
8821
+ const rows = await testAllAccounts(await poolTargetsOf(stored), modelId, (chatFor, credential, id) => {
8822
+ if (credential === void 0) return Promise.resolve({
8823
+ id,
8824
+ verdict: "auth",
8825
+ detail: "no stored credential for this account"
8826
+ });
8827
+ return probeOneModel(chatFor(credential), id, void 0, probeInputTokens);
8828
+ }, probeDeps(), onRow);
8829
+ await writeTraePoolProbes(poolKeyOfPanel(stored), probeUpdatesOf(rows));
8830
+ return rows;
8831
+ },
8832
+ testOne: async (stored, accountId) => {
8833
+ const model = resolveTargetModel(poolCatalogOf(stored), poolPreferencesOf(stored).targetModelId);
8834
+ if (model.modelId === void 0) return void 0;
8835
+ const names = await poolMemberNamesOf(stored);
8836
+ const row = await testWith({
8837
+ accountId,
8838
+ accountName: names[accountId] ?? ""
8839
+ }, model.modelId, probeInputTokensOf(stored));
8840
+ await writeTraePoolProbes(poolKeyOfPanel(stored), probeUpdatesOf([row]));
8841
+ return row;
8842
+ },
8843
+ removeOne: async (stored, accountId) => {
8844
+ await dropTraePoolProbe(poolKeyOfPanel(stored), accountId);
8845
+ return true;
8846
+ }
8847
+ };
8848
+ };
8849
+ /**
7019
8850
  * The app's current sign-in when it differs from the bound account (issue
7020
8851
  * #29). Pure read: the store resolves the bound account verbatim, so billing
7021
8852
  * never moves from here — the card's hint and the follow switch below are
@@ -7071,7 +8902,28 @@ function apply(ctx, config) {
7071
8902
  creditsOfAccount: (panel, accountId) => stacks[panel].creditsOfAccount(accountId),
7072
8903
  accountStateOf: (panel, accountId) => stacks[panel].accountStateOf(accountId),
7073
8904
  claimCheckinOf: (panel, accountId) => stacks[panel].claimCheckinOf(accountId),
7074
- liveLogin: (panel) => liveLoginOf(panel)
8905
+ liveLogin: (panel) => liveLoginOf(panel),
8906
+ pool: {
8907
+ preferences: (panel) => poolPreferencesOf(panel),
8908
+ members: (panel) => poolMembersOf(panel),
8909
+ effectiveMemberAccountIds: async (panel) => {
8910
+ return effectiveMembersOf(poolPreferencesOf(panel).memberAccountIds ?? [], new Set((await poolMembersOf(panel)).map((member) => member.account.id)));
8911
+ },
8912
+ memberNames: (panel) => poolMemberNamesOf(panel),
8913
+ otherAccounts: async (panel) => (await poolMembersOf(panel)).map((member) => member.account),
8914
+ currentAccountId: async (panel) => {
8915
+ await applyPoolSelection(panel).catch(() => void 0);
8916
+ const credential = await stacks[panel].store.current().catch(() => void 0);
8917
+ return credential === void 0 ? void 0 : traeAccountId(credential);
8918
+ },
8919
+ catalog: (panel) => poolCatalogOf(panel),
8920
+ checkinSupported: (panel) => regionOfPanel(panel) === "cn",
8921
+ checkin: (panel) => poolDepsOf(panel).checkin?.(panel) ?? Promise.resolve([]),
8922
+ checkinState: (panel) => poolDepsOf(panel).checkinState?.(panel) ?? Promise.resolve({}),
8923
+ test: (panel, modelId, onRow) => poolDepsOf(panel).test?.(panel, modelId, onRow) ?? Promise.resolve([]),
8924
+ testOne: (panel, accountId) => poolDepsOf(panel).testOne?.(panel, accountId) ?? Promise.resolve(void 0),
8925
+ removeOne: (panel, accountId) => poolDepsOf(panel).removeOne?.(panel, accountId) ?? Promise.resolve(false)
8926
+ }
7075
8927
  }));
7076
8928
  /**
7077
8929
  * Live registration handles per region, filled once the shim is listening.
@@ -7306,4 +9158,4 @@ function apply(ctx, config) {
7306
9158
  });
7307
9159
  }
7308
9160
  //#endregion
7309
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
9161
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };