@eddyskywalker/dsh-chatgpt-subscription 0.10.10 → 0.10.12

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
@@ -905,6 +905,16 @@ function poolCredentialAccount$1(filePath) {
905
905
  return createHash("sha256").update(path.resolve(filePath)).digest("hex");
906
906
  }
907
907
  const poolOperations = /* @__PURE__ */ new Map();
908
+ const poolIdentityRevisions = /* @__PURE__ */ new Map();
909
+ const poolRefreshes = /* @__PURE__ */ new Map();
910
+ /**
911
+ * A selected row was deleted, or became unusable, while a caller awaited it.
912
+ *
913
+ * Distinct from every other credential error because a MODEL REQUEST can still
914
+ * be served by another account: routing excludes the row and tries the next
915
+ * one, while a caller that named the account still gets the plain refusal.
916
+ */
917
+ var PoolAccountUnavailableError = class extends LlmError {};
908
918
  /**
909
919
  * The shared multi-account pool: encrypted storage, eligibility filtering,
910
920
  * rotation strategy, 429 cooldowns and per-account auth failures.
@@ -926,11 +936,14 @@ var AccountPoolCore = class {
926
936
  * can therefore invalidate on identity change without paying a credential read
927
937
  * to discover it.
928
938
  */
929
- identityRevision = 0;
939
+ /** Pool-file identity of this instance: locks, revisions and flights key on it. */
940
+ operationKey;
930
941
  constructor(hooks) {
931
942
  this.hooks = hooks;
932
943
  this.filePath = hooks.poolFile;
933
944
  this.backend = hooks.backend ?? createPoolBackend(hooks);
945
+ const resolved = path.resolve(this.filePath);
946
+ this.operationKey = process.platform === "win32" ? resolved.toLowerCase() : resolved;
934
947
  }
935
948
  /**
936
949
  * A cheap, in-memory revision of the pool's serving identity.
@@ -946,15 +959,21 @@ var AccountPoolCore = class {
946
959
  * would report one account's usage under another account's name.
947
960
  */
948
961
  currentIdentityRevision() {
949
- return this.identityRevision;
962
+ return poolIdentityRevisions.get(this.operationKey) ?? 0;
950
963
  }
951
964
  /** Human description of where this pool's credentials live. */
952
965
  path() {
953
966
  if (process.platform === "win32") return `${this.filePath}.dpapi`;
954
967
  return `${process.platform === "darwin" ? "Keychain" : "Secret Service"}: ${this.hooks.keychainService}/${poolCredentialAccount$1(this.filePath)}`;
955
968
  }
969
+ /**
970
+ * Queue one storage transaction for this pool file.
971
+ *
972
+ * NOT reentrant: a transaction must use {@link loadPoolData} and
973
+ * {@link updatePool} rather than calling the public read/write methods.
974
+ */
956
975
  serialize(operation) {
957
- const key = path.resolve(this.filePath);
976
+ const key = this.operationKey;
958
977
  const result = (poolOperations.get(key) || Promise.resolve()).then(operation);
959
978
  const settled = result.then(() => void 0, () => void 0);
960
979
  poolOperations.set(key, settled);
@@ -978,31 +997,74 @@ var AccountPoolCore = class {
978
997
  * getter write to disk.
979
998
  */
980
999
  read() {
981
- return this.serialize(async () => {
982
- let current = null;
983
- try {
984
- current = await this.backend.load();
985
- } catch {}
986
- if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
987
- try {
988
- const legacy = await this.hooks.legacyAccount?.();
989
- if (legacy) return {
990
- version: 1,
991
- activeAccountId: legacy.id,
992
- rotationStrategy: current?.rotationStrategy || "sequential",
993
- accounts: [legacy]
994
- };
995
- } catch {}
996
- return current || {
1000
+ return this.serialize(() => this.loadPoolData());
1001
+ }
1002
+ /**
1003
+ * Load the pool without holding the file lock.
1004
+ *
1005
+ * Providers override this to maintain their own in-memory indexes, so every
1006
+ * load — including one inside {@link updatePool} — refreshes them.
1007
+ */
1008
+ async loadPoolData() {
1009
+ let current = null;
1010
+ try {
1011
+ current = await this.backend.load();
1012
+ } catch {}
1013
+ if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
1014
+ try {
1015
+ const legacy = await this.hooks.legacyAccount?.();
1016
+ if (legacy) return {
997
1017
  version: 1,
998
- rotationStrategy: "sequential",
999
- accounts: []
1018
+ activeAccountId: legacy.id,
1019
+ rotationStrategy: current?.rotationStrategy || "sequential",
1020
+ accounts: [legacy]
1000
1021
  };
1001
- });
1022
+ } catch {}
1023
+ return current || {
1024
+ version: 1,
1025
+ rotationStrategy: "sequential",
1026
+ accounts: []
1027
+ };
1002
1028
  }
1029
+ async persistPoolData(data) {
1030
+ poolIdentityRevisions.set(this.operationKey, this.currentIdentityRevision() + 1);
1031
+ await this.saveVerified(data);
1032
+ }
1033
+ /**
1034
+ * Replace the whole document (initialization, import), NOT a read-modify-write.
1035
+ * Prefer {@link updatePool} for mutations so the draft is read under the lock.
1036
+ */
1003
1037
  write(data) {
1004
- this.identityRevision += 1;
1005
- return this.serialize(() => this.saveVerified(this.hooks.parsePoolData(data)));
1038
+ return this.serialize(() => this.persistPoolData(data));
1039
+ }
1040
+ /**
1041
+ * Read the LATEST document, mutate it and persist it as one file transaction.
1042
+ *
1043
+ * This is the whole fix for the read-modify-write race: the old shape read a
1044
+ * snapshot, awaited (a token refresh, a settings write, an encrypted round
1045
+ * trip) and then wrote that whole stale snapshot back, erasing whatever landed
1046
+ * in between — including another account's freshly rotated token.
1047
+ *
1048
+ * The callback runs under the file lock, so it must not call read, write or
1049
+ * updatePool (the lock is not reentrant) and must not do network I/O.
1050
+ * Provider-local stores belong in `afterCommit`, which runs only once the
1051
+ * pool write is verified, and must not re-enter the pool either.
1052
+ */
1053
+ updatePool(mutate, afterCommit) {
1054
+ return this.serialize(async () => {
1055
+ const data = await this.loadPoolData();
1056
+ const before = structuredClone(data);
1057
+ const result = await mutate(data);
1058
+ if (!isDeepStrictEqual(before, data)) {
1059
+ await this.persistPoolData(data);
1060
+ await afterCommit?.(data, result).catch(() => void 0);
1061
+ for (const account of data.accounts) if (!isDeepStrictEqual(before.accounts.find((entry) => entry.id === account.id)?.credentials, account.credentials)) await this.hooks.credentialsCommitted?.(account).catch(() => void 0);
1062
+ const previousPrimary = before.accounts.find((account) => account.isPrimary);
1063
+ const primary = data.accounts.find((account) => account.isPrimary);
1064
+ if (previousPrimary?.id !== primary?.id || !isDeepStrictEqual(previousPrimary?.credentials, primary?.credentials)) await this.hooks.mirrorPrimary?.(primary?.credentials ?? null).catch(() => void 0);
1065
+ }
1066
+ return result;
1067
+ });
1006
1068
  }
1007
1069
  /** Public summaries, with cooldowns that already expired reported as absent. */
1008
1070
  async listAccounts() {
@@ -1031,8 +1093,17 @@ var AccountPoolCore = class {
1031
1093
  return this.hooks.extendSummary ? this.hooks.extendSummary(account, summary, now) : summary;
1032
1094
  }
1033
1095
  /** Add or re-authorize one account; an existing dedupe key updates in place. */
1034
- async addAccount(credentials, alias) {
1035
- const data = await this.read();
1096
+ addAccount(credentials, alias) {
1097
+ return this.updatePool((data) => this.addAccountToPool(data, credentials, alias));
1098
+ }
1099
+ /**
1100
+ * Add or re-authorize inside an existing transaction.
1101
+ *
1102
+ * Capacity, dedupe and the primary decision are all taken against the live
1103
+ * document, so two simultaneous sign-ins cannot each pass a check the other
1104
+ * has already invalidated.
1105
+ */
1106
+ addAccountToPool(data, credentials, alias) {
1036
1107
  const key = this.hooks.dedupeKey?.(credentials);
1037
1108
  const existingIndex = key === void 0 ? -1 : data.accounts.findIndex((account) => this.hooks.dedupeKey?.(account.credentials) === key);
1038
1109
  const maxAccounts = this.hooks.maxAccounts ?? 20;
@@ -1058,77 +1129,79 @@ var AccountPoolCore = class {
1058
1129
  if (existingIndex >= 0) data.accounts[existingIndex] = account;
1059
1130
  else data.accounts.push(account);
1060
1131
  if (!data.activeAccountId || account.isPrimary) data.activeAccountId = account.id;
1061
- await this.write(data);
1062
- if (account.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
1063
1132
  return account;
1064
1133
  }
1065
- async setPrimary(accountId) {
1066
- const data = await this.read();
1067
- for (const account of data.accounts) account.isPrimary = account.id === accountId;
1068
- const primary = data.accounts.find((account) => account.isPrimary);
1069
- if (primary) {
1070
- data.activeAccountId = primary.id;
1071
- this.hooks.mirrorPrimary?.(primary.credentials).catch(() => void 0);
1072
- }
1073
- await this.write(data);
1134
+ setPrimary(accountId) {
1135
+ return this.updatePool((data) => {
1136
+ for (const account of data.accounts) account.isPrimary = account.id === accountId;
1137
+ const primary = data.accounts.find((account) => account.isPrimary);
1138
+ if (primary) data.activeAccountId = primary.id;
1139
+ });
1074
1140
  }
1075
1141
  /**
1076
1142
  * Persist a refreshed credential for one account.
1077
1143
  *
1144
+ * `expected` makes this a conditional write: when the stored credential is
1145
+ * no longer the one the caller started from, something newer won the race (a
1146
+ * re-login, a rotation committed while this call was in flight) and the
1147
+ * latest row is returned WITHOUT being overwritten. A deleted account is
1148
+ * never brought back.
1149
+ *
1078
1150
  * Used by a forced refresh (a 401 mid-request) rather than by the rotating
1079
- * getEffectiveAccount path, which already writes back what it refreshed.
1151
+ * getEffectiveAccount path, which refreshes through its own single-flight.
1080
1152
  */
1081
- async updateAccountCredentials(accountId, credentials) {
1082
- const data = await this.read();
1083
- const target = data.accounts.find((account) => account.id === accountId);
1084
- if (target === void 0) return void 0;
1085
- target.credentials = credentials;
1086
- await this.write(data);
1087
- if (target.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
1088
- return target;
1153
+ updateAccountCredentials(accountId, credentials, expected) {
1154
+ return this.updatePool((data) => {
1155
+ const target = data.accounts.find((account) => account.id === accountId);
1156
+ if (target && (expected === void 0 || isDeepStrictEqual(target.credentials, expected))) target.credentials = credentials;
1157
+ return target;
1158
+ });
1089
1159
  }
1090
- async setAlias(accountId, alias) {
1091
- const data = await this.read();
1092
- const target = data.accounts.find((account) => account.id === accountId);
1093
- if (target && alias.trim()) {
1094
- target.alias = alias.trim();
1095
- await this.write(data);
1096
- }
1160
+ setAlias(accountId, alias) {
1161
+ return this.updatePool((data) => {
1162
+ const target = data.accounts.find((account) => account.id === accountId);
1163
+ if (target && alias.trim()) target.alias = alias.trim();
1164
+ });
1097
1165
  }
1098
- async deleteAccount(accountId) {
1099
- const data = await this.read();
1166
+ deleteAccount(accountId) {
1167
+ return this.updatePool((data) => this.deleteAccountFromPool(data, accountId));
1168
+ }
1169
+ /**
1170
+ * Remove one account inside a transaction.
1171
+ *
1172
+ * Provider refusal rules (a snapshot this plugin does not own, a desktop
1173
+ * account it may only hide) belong in an override of this, so the check and
1174
+ * the removal cannot be separated by a concurrent change.
1175
+ */
1176
+ deleteAccountFromPool(data, accountId) {
1100
1177
  const wasPrimary = data.accounts.find((account) => account.id === accountId)?.isPrimary;
1101
1178
  data.accounts = data.accounts.filter((account) => account.id !== accountId);
1102
- if (wasPrimary && data.accounts.length > 0) {
1103
- data.accounts[0].isPrimary = true;
1104
- this.hooks.mirrorPrimary?.(data.accounts[0].credentials).catch(() => void 0);
1105
- } else if (data.accounts.length === 0) this.hooks.mirrorPrimary?.(null).catch(() => void 0);
1179
+ if (wasPrimary && data.accounts.length > 0) data.accounts[0].isPrimary = true;
1106
1180
  if (data.activeAccountId === accountId) data.activeAccountId = data.accounts.find((account) => account.isPrimary)?.id ?? data.accounts[0]?.id;
1107
- await this.write(data);
1108
1181
  }
1109
- async setStrategy(strategy) {
1110
- const data = await this.read();
1111
- data.rotationStrategy = strategy;
1112
- await this.write(data);
1182
+ setStrategy(strategy) {
1183
+ return this.updatePool((data) => {
1184
+ data.rotationStrategy = strategy;
1185
+ });
1113
1186
  }
1114
1187
  /** Cool one account down after an upstream 429. */
1115
- async markCooldown(accountId, durationMs, reason) {
1116
- const data = await this.read();
1117
- const target = data.accounts.find((account) => account.id === accountId);
1118
- if (target) {
1119
- target.cooldownUntil = Date.now() + Math.max(MIN_COOLDOWN_MS, durationMs);
1120
- target.cooldownReason = reason;
1121
- await this.write(data);
1122
- }
1188
+ markCooldown(accountId, durationMs, reason) {
1189
+ return this.updatePool((data) => {
1190
+ const target = data.accounts.find((account) => account.id === accountId);
1191
+ if (target) {
1192
+ target.cooldownUntil = Date.now() + Math.max(MIN_COOLDOWN_MS, durationMs);
1193
+ target.cooldownReason = reason;
1194
+ }
1195
+ });
1123
1196
  }
1124
- async clearCooldown(accountId) {
1125
- const data = await this.read();
1126
- const target = data.accounts.find((account) => account.id === accountId);
1127
- if (target) {
1128
- target.cooldownUntil = void 0;
1129
- target.cooldownReason = void 0;
1130
- await this.write(data);
1131
- }
1197
+ clearCooldown(accountId) {
1198
+ return this.updatePool((data) => {
1199
+ const target = data.accounts.find((account) => account.id === accountId);
1200
+ if (target) {
1201
+ target.cooldownUntil = void 0;
1202
+ target.cooldownReason = void 0;
1203
+ }
1204
+ });
1132
1205
  }
1133
1206
  /**
1134
1207
  * Record that an account can no longer authenticate.
@@ -1136,23 +1209,23 @@ var AccountPoolCore = class {
1136
1209
  * The account stays in the pool: signing in again is what restores it, and a
1137
1210
  * deleted account would take the user's alias and ordering with it.
1138
1211
  */
1139
- async markAuthFailed(accountId, reason, status = "expired") {
1140
- const data = await this.read();
1141
- const target = data.accounts.find((account) => account.id === accountId);
1142
- if (target) {
1143
- target.authStatus = status;
1144
- target.authFailedReason = reason;
1145
- await this.write(data);
1146
- }
1212
+ markAuthFailed(accountId, reason, status = "expired") {
1213
+ return this.updatePool((data) => {
1214
+ const target = data.accounts.find((account) => account.id === accountId);
1215
+ if (target) {
1216
+ target.authStatus = status;
1217
+ target.authFailedReason = reason;
1218
+ }
1219
+ });
1147
1220
  }
1148
- async clearAuthFailed(accountId) {
1149
- const data = await this.read();
1150
- const target = data.accounts.find((account) => account.id === accountId);
1151
- if (target) {
1152
- target.authStatus = void 0;
1153
- target.authFailedReason = void 0;
1154
- await this.write(data);
1155
- }
1221
+ clearAuthFailed(accountId) {
1222
+ return this.updatePool((data) => {
1223
+ const target = data.accounts.find((account) => account.id === accountId);
1224
+ if (target) {
1225
+ target.authStatus = void 0;
1226
+ target.authFailedReason = void 0;
1227
+ }
1228
+ });
1156
1229
  }
1157
1230
  /** Whether an account may serve a request right now. */
1158
1231
  isEligible(account, now, triedAccountIds) {
@@ -1192,24 +1265,16 @@ var AccountPoolCore = class {
1192
1265
  *
1193
1266
  * @param fetchFn - fetch used if the credential is close to expiry.
1194
1267
  */
1195
- async getCredentialAccount(fetchFn = fetch) {
1268
+ async getCredentialAccount(fetchFn = fetch, forceRefresh = false) {
1196
1269
  const data = await this.read();
1197
1270
  if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
1198
- const now = Date.now();
1199
1271
  const usable = data.accounts.filter((account) => this.isCredentialUsable(account));
1200
1272
  if (usable.length === 0) throw new LlmError(`全部 ${data.accounts.length} 个 ${this.hooks.displayName} 账号均需要重新登录。`, "AUTH", { status: 401 });
1201
1273
  const selected = this.selectAccount(usable, data);
1202
- if (this.shouldRefreshCredential(selected, now) && this.hooks.refresh) {
1203
- const refreshed = await this.hooks.refresh(selected.credentials, fetchFn);
1204
- await this.updateAccountCredentials(selected.id, refreshed);
1205
- return {
1206
- account: selected,
1207
- credentials: refreshed
1208
- };
1209
- }
1274
+ const account = await this.freshAccount(selected, fetchFn, forceRefresh);
1210
1275
  return {
1211
- account: selected,
1212
- credentials: selected.credentials
1276
+ account,
1277
+ credentials: account.credentials
1213
1278
  };
1214
1279
  }
1215
1280
  /**
@@ -1244,8 +1309,8 @@ var AccountPoolCore = class {
1244
1309
  * looks unexpired, and only the service's own refusal says so. Renewing it is
1245
1310
  * what separates that from a credential that genuinely needs a new sign-in.
1246
1311
  */
1247
- async renewCredential(accountId, fetchFn = fetch) {
1248
- return this.credentialFor(accountId, fetchFn, true);
1312
+ async renewCredential(accountId, fetchFn = fetch, expected) {
1313
+ return this.credentialFor(accountId, fetchFn, true, expected);
1249
1314
  }
1250
1315
  /**
1251
1316
  * Resolve, and if needed renew, one account's credential.
@@ -1255,29 +1320,106 @@ var AccountPoolCore = class {
1255
1320
  * not name an account (the catalog loader) still has to present the
1256
1321
  * credential that is actually in play.
1257
1322
  */
1258
- async credentialFor(accountId, fetchFn, force) {
1323
+ async credentialFor(accountId, fetchFn, force, expected) {
1259
1324
  const data = await this.read();
1260
1325
  const target = accountId === void 0 ? data.accounts.find((account) => account.id === data.activeAccountId) ?? data.accounts.find((account) => account.isPrimary) ?? data.accounts[0] : data.accounts.find((account) => account.id === accountId);
1261
1326
  if (target === void 0) throw new LlmError(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`, "AUTH");
1262
1327
  if (!this.isCredentialUsable(target)) throw new LlmError(`${this.hooks.displayName} 账号的凭据已被拒绝,需要重新登录。`, "AUTH", { status: 401 });
1263
- if (!force && (!this.shouldRefreshCredential(target, Date.now()) || !this.hooks.refresh)) return target.credentials;
1264
- if (!this.hooks.refresh) return target.credentials;
1328
+ if (expected !== void 0 && !isDeepStrictEqual(expected, target.credentials)) return target.credentials;
1329
+ return (await this.freshAccount(target, fetchFn, force)).credentials;
1330
+ }
1331
+ /**
1332
+ * Return one account's current row, refreshing it at most once per generation.
1333
+ *
1334
+ * Shared by every entry point — routing, auxiliary tools, the settings card
1335
+ * and forced renewals — across all instances of the pool file, because a
1336
+ * rotating refresh token may be spent once: two parallel exchanges both fail,
1337
+ * and the losing one gets recorded as a dead sign-in.
1338
+ *
1339
+ * Reading the current generation and registering/joining its flight happen in
1340
+ * one short transaction, so a commit cannot slip between the two decisions.
1341
+ * The network call is fired AFTER the lock is released, and the plan returned
1342
+ * from the lock is a plain value: awaiting a flight while holding the lock
1343
+ * would deadlock that flight's own commit.
1344
+ */
1345
+ async freshAccount(target, fetchFn, force = false) {
1346
+ let launch;
1347
+ const plan = await this.serialize(async () => {
1348
+ const current = (await this.loadPoolData()).accounts.find((account) => account.id === target.id);
1349
+ if (current === void 0 || !this.isCredentialUsable(current)) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
1350
+ if (!isDeepStrictEqual(current.credentials, target.credentials)) return { account: current };
1351
+ if (!this.hooks.refresh || !force && !this.shouldRefreshCredential(current, Date.now())) return { account: current };
1352
+ const key = JSON.stringify([this.operationKey, current.id]);
1353
+ const pending = poolRefreshes.get(key);
1354
+ if (pending !== void 0 && isDeepStrictEqual(pending.credentials, current.credentials)) return { result: pending.result };
1355
+ const result = new Promise((resolve, reject) => {
1356
+ launch = () => {
1357
+ this.refreshAccount(current, fetchFn).then(resolve, reject);
1358
+ };
1359
+ });
1360
+ const flight = {
1361
+ credentials: structuredClone(current.credentials),
1362
+ result
1363
+ };
1364
+ poolRefreshes.set(key, flight);
1365
+ const release = () => {
1366
+ if (poolRefreshes.get(key) === flight) poolRefreshes.delete(key);
1367
+ };
1368
+ result.then(release, release);
1369
+ return { result };
1370
+ });
1371
+ launch?.();
1372
+ return plan.result ?? plan.account;
1373
+ }
1374
+ /**
1375
+ * Refresh one account: network outside the lock, then a conditional commit.
1376
+ *
1377
+ * Every write-back is checked against the credential the refresh started from,
1378
+ * which is what keeps a late response from restoring a spent token over a
1379
+ * re-login, and keeps a deleted account from coming back.
1380
+ */
1381
+ async refreshAccount(target, fetchFn) {
1382
+ const expected = structuredClone(target.credentials);
1265
1383
  const live = await this.hooks.liveCredentialsFor?.(target).catch(() => null);
1266
1384
  if (live !== null && live !== void 0 && live.advanced) {
1267
- target.credentials = live.credentials;
1268
- await this.write(data).catch(() => void 0);
1269
- return live.credentials;
1385
+ const account = await this.updateAccountCredentials(target.id, live.credentials, expected).catch(() => ({
1386
+ ...target,
1387
+ credentials: live.credentials
1388
+ }));
1389
+ this.assertUsableAccount(account);
1390
+ return account;
1270
1391
  }
1271
1392
  let refreshed;
1272
1393
  try {
1273
1394
  refreshed = await this.hooks.refresh(target.credentials, fetchFn);
1274
1395
  } catch (error) {
1275
1396
  const status = this.hooks.refreshFailureStatus?.(error);
1276
- if (status !== void 0) await this.markAuthFailed(target.id, error instanceof Error ? error.message : String(error), status).catch(() => void 0);
1397
+ let superseded;
1398
+ await this.updatePool((data) => {
1399
+ const current = data.accounts.find((account) => account.id === target.id);
1400
+ if (current === void 0) return;
1401
+ if (!isDeepStrictEqual(current.credentials, expected)) {
1402
+ superseded = current;
1403
+ return;
1404
+ }
1405
+ if (status !== void 0) {
1406
+ current.authStatus = status;
1407
+ current.authFailedReason = error instanceof Error ? error.message : String(error);
1408
+ }
1409
+ }).catch(() => void 0);
1410
+ if (superseded !== void 0) {
1411
+ this.assertUsableAccount(superseded);
1412
+ return superseded;
1413
+ }
1277
1414
  throw error;
1278
1415
  }
1279
- await this.updateAccountCredentials(target.id, refreshed);
1280
- return refreshed;
1416
+ const account = await this.updateAccountCredentials(target.id, refreshed, expected);
1417
+ this.assertUsableAccount(account);
1418
+ return account;
1419
+ }
1420
+ assertUsableAccount(account) {
1421
+ if (account === void 0) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
1422
+ if (!this.isCredentialUsable(account)) throw new PoolAccountUnavailableError(this.hooks.displayName + " 账号的凭据已被拒绝,需要重新登录。", "AUTH", { status: 401 });
1281
1423
  }
1282
1424
  /** Whether one account's credential must be refreshed before it is handed out. */
1283
1425
  shouldRefreshCredential(account, now) {
@@ -1321,37 +1463,34 @@ var AccountPoolCore = class {
1321
1463
  * @param fetchFn - fetch used by the provider's token refresh.
1322
1464
  * @throws LlmError with `RATE_LIMIT` when every account is cooling down.
1323
1465
  */
1324
- async getEffectiveAccount(excludeIds, fetchFn = fetch) {
1466
+ async getEffectiveAccount(excludeIds, fetchFn = fetch, forceRefresh = false) {
1325
1467
  const data = await this.read();
1326
1468
  if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
1327
1469
  const now = Date.now();
1328
1470
  const eligible = data.accounts.filter((account) => this.isEligible(account, now, excludeIds));
1329
1471
  if (eligible.length === 0) throw this.allUnavailableError(data, now);
1330
1472
  const selected = this.selectAccount(eligible, data);
1331
- if (this.shouldRefreshCredential(selected, now) && this.hooks.refresh) {
1332
- let refreshed;
1333
- try {
1334
- refreshed = await this.hooks.refresh(selected.credentials, fetchFn);
1335
- } catch (error) {
1336
- const status = this.hooks.refreshFailureStatus?.(error);
1337
- if (status === void 0) throw error;
1338
- await this.markAuthFailed(selected.id, error instanceof Error ? error.message : String(error), status);
1339
- const nextExclude = new Set(excludeIds ?? []);
1340
- nextExclude.add(selected.id);
1341
- if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn);
1342
- throw error;
1343
- }
1344
- selected.credentials = refreshed;
1345
- const index = data.accounts.findIndex((account) => account.id === selected.id);
1346
- if (index >= 0) data.accounts[index] = selected;
1347
- if (selected.isPrimary) this.hooks.mirrorPrimary?.(refreshed).catch(() => void 0);
1348
- }
1349
- selected.lastUsedAt = now;
1350
- data.activeAccountId = selected.id;
1351
- await this.write(data).catch(() => void 0);
1473
+ let refreshed;
1474
+ try {
1475
+ refreshed = await this.freshAccount(selected, fetchFn, forceRefresh);
1476
+ } catch (error) {
1477
+ if (this.hooks.refreshFailureStatus?.(error) === void 0 && !(error instanceof PoolAccountUnavailableError)) throw error;
1478
+ const nextExclude = new Set(excludeIds ?? []);
1479
+ nextExclude.add(selected.id);
1480
+ if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn, forceRefresh);
1481
+ throw error;
1482
+ }
1483
+ const account = await this.updatePool((current) => {
1484
+ const latest = current.accounts.find((entry) => entry.id === refreshed.id);
1485
+ if (latest === void 0 || !isDeepStrictEqual(latest.credentials, refreshed.credentials)) return latest;
1486
+ latest.lastUsedAt = now;
1487
+ if (current.activeAccountId === data.activeAccountId) current.activeAccountId = latest.id;
1488
+ return latest;
1489
+ }).catch(() => refreshed);
1490
+ this.assertUsableAccount(account);
1352
1491
  return {
1353
- account: selected,
1354
- credentials: selected.credentials
1492
+ account,
1493
+ credentials: account.credentials
1355
1494
  };
1356
1495
  }
1357
1496
  };
@@ -1724,8 +1863,7 @@ var OAuthService = class {
1724
1863
  async refresh() {
1725
1864
  this.assertAvailable();
1726
1865
  if (this.pool !== null) {
1727
- const { account, credentials } = await this.pool.getEffectiveAccount(void 0, this.fetchFn);
1728
- await this.refreshAccount(credentials).then((refreshed) => this.pool.updateAccountCredentials(account.id, refreshed));
1866
+ await this.pool.getEffectiveAccount(void 0, this.fetchFn, true);
1729
1867
  return this.status();
1730
1868
  }
1731
1869
  const stored = await this.loadAuthenticated();
@@ -1787,18 +1925,8 @@ var OAuthService = class {
1787
1925
  */
1788
1926
  async credentials(forceRefresh = false, access = {}) {
1789
1927
  if (this.pool !== null) {
1790
- if (access.purpose === "tool") {
1791
- const { account, credentials } = await this.pool.getCredentialAccount(this.fetchFn);
1792
- if (!forceRefresh) return credentials;
1793
- const refreshed = await this.refreshAccount(credentials, this.fetchFn);
1794
- await this.pool.updateAccountCredentials(account.id, refreshed);
1795
- return refreshed;
1796
- }
1797
- const { account, credentials } = await this.pool.getEffectiveAccount(void 0, this.fetchFn);
1798
- if (!forceRefresh) return credentials;
1799
- const refreshed = await this.refreshAccount(credentials, this.fetchFn);
1800
- await this.pool.updateAccountCredentials(account.id, refreshed);
1801
- return refreshed;
1928
+ if (access.purpose === "tool") return (await this.pool.getCredentialAccount(this.fetchFn, forceRefresh)).credentials;
1929
+ return (await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh)).credentials;
1802
1930
  }
1803
1931
  const stored = await this.loadAuthenticated();
1804
1932
  if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return this.refreshCredentials(stored);
@@ -2251,11 +2379,8 @@ var CodexAccountPool = class extends AccountPoolCore {
2251
2379
  async refreshAccountNow(accountId) {
2252
2380
  const account = (await this.read()).accounts.find((entry) => entry.id === accountId);
2253
2381
  if (account === void 0) throw new Error("ChatGPT 账号已不在号池中");
2254
- const refresher = this.refresherRef.current;
2255
- if (refresher === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
2256
- const refreshed = await refresher.refreshAccount(account.credentials);
2257
- await this.updateAccountCredentials(accountId, refreshed);
2258
- return refreshed;
2382
+ if (this.refresherRef.current === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
2383
+ return this.renewCredential(accountId, fetch, account.credentials);
2259
2384
  }
2260
2385
  /** The single-credential store this pool mirrors its primary account into. */
2261
2386
  mirrorStore() {
@@ -21552,23 +21677,30 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21552
21677
  * because that one genuinely needs a sign-in.
21553
21678
  */
21554
21679
  async healStaleAuthFailures(current) {
21555
- const data = current ?? await this.read();
21556
- if (!data.accounts.some(isMarkedFailed)) return false;
21680
+ const failedAccounts = (current ?? await this.read()).accounts.filter(isMarkedFailed);
21681
+ if (failedAccounts.length === 0) return false;
21682
+ const targets = new Map(failedAccounts.map((account) => [account.id, {
21683
+ credentials: account.credentials,
21684
+ authStatus: account.authStatus
21685
+ }]));
21557
21686
  const live = await this.store.read().catch(() => null);
21558
21687
  if (live === null) return false;
21559
- let changed = false;
21560
- for (const account of data.accounts) {
21561
- if (account.authStatus === void 0 || account.authStatus === "ok") continue;
21562
- if (!minimaxCodeSameCredentialSession(live, account.credentials)) continue;
21563
- if (!minimaxCodeCredentialAdvancedPast(live, account.credentials)) continue;
21564
- account.authStatus = void 0;
21565
- account.authFailedReason = void 0;
21566
- account.credentials = live;
21567
- changed = true;
21568
- }
21569
- if (!changed) return false;
21570
- await this.write(data).catch(() => void 0);
21571
- return true;
21688
+ return await this.updatePool((draft) => {
21689
+ let changed = false;
21690
+ for (const account of draft.accounts) {
21691
+ const expected = targets.get(account.id);
21692
+ if (!expected) continue;
21693
+ if (account.authStatus !== expected.authStatus) continue;
21694
+ if (!isDeepStrictEqual(account.credentials, expected.credentials)) continue;
21695
+ if (!minimaxCodeSameCredentialSession(live, account.credentials)) continue;
21696
+ if (!minimaxCodeCredentialAdvancedPast(live, account.credentials)) continue;
21697
+ account.authStatus = void 0;
21698
+ account.authFailedReason = void 0;
21699
+ account.credentials = live;
21700
+ changed = true;
21701
+ }
21702
+ return changed;
21703
+ });
21572
21704
  }
21573
21705
  /**
21574
21706
  * Pick the account for the next request.
@@ -21582,7 +21714,13 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21582
21714
  const effective = await this.getEffectiveAccount(excludeIds, fetchFn);
21583
21715
  const live = await this.liveNativeCredential(effective.account);
21584
21716
  if (live === null) return effective;
21585
- if (live.accessToken !== effective.credentials.accessToken) await this.updateAccountCredentials(effective.account.id, live).catch(() => void 0);
21717
+ if (live.accessToken !== effective.credentials.accessToken) {
21718
+ const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
21719
+ if (updated) return {
21720
+ account: updated,
21721
+ credentials: updated.credentials
21722
+ };
21723
+ }
21586
21724
  return {
21587
21725
  account: effective.account,
21588
21726
  credentials: live
@@ -21644,7 +21782,7 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21644
21782
  return this.addAccount(credentials, alias);
21645
21783
  }
21646
21784
  /**
21647
- * Delete one account.
21785
+ * Remove one account inside a transaction.
21648
21786
  *
21649
21787
  * An account whose credential is the desktop app's own \`auth.json\` is refused:
21650
21788
  * this plugin does not own that file, and removing the account would either
@@ -21652,9 +21790,9 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
21652
21790
  * file — sign the user out of MiniMax Code itself. Signing out there is the
21653
21791
  * supported way to end it, and the card is told so by \`removable: false\`.
21654
21792
  */
21655
- async deleteAccount(accountId) {
21656
- if ((await this.read()).accounts.find((account) => account.id === accountId)?.source === "minimax-native") throw new Error("该账号来自 MiniMax Code 桌面端(~/.minimax/auth),本插件只能读取和续期,不能删除。请在 MiniMax Code 应用中退出登录。");
21657
- await super.deleteAccount(accountId);
21793
+ deleteAccountFromPool(data, accountId) {
21794
+ if (data.accounts.find((account) => account.id === accountId)?.source === "minimax-native") throw new Error("该账号来自 MiniMax Code 桌面端(~/.minimax/auth),本插件只能读取和续期,不能删除。请在 MiniMax Code 应用中退出登录。");
21795
+ super.deleteAccountFromPool(data, accountId);
21658
21796
  }
21659
21797
  /** The credential store this pool mirrors its primary account into. */
21660
21798
  mirrorStore() {
@@ -26157,15 +26295,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26157
26295
  this.options_selection = options.selection;
26158
26296
  }
26159
26297
  /**
26160
- * Read the pool, refreshing the legacy-key index on the way.
26298
+ * Load the pool, refreshing the legacy-key index on the way.
26161
26299
  *
26162
- * Every routing decision goes through this method before it consults the
26163
- * settings selection, so building the index here is what keeps a pin written
26164
- * before the identity fix resolvable when {@link resolveSelectionId} is asked
26165
- * for it.
26300
+ * Every routing decision and mutation goes through this method before it
26301
+ * consults the settings selection, so building the index here is what keeps
26302
+ * a pin written before the identity fix resolvable when
26303
+ * {@link resolveSelectionId} is asked for it.
26166
26304
  */
26167
- async read() {
26168
- const data = await super.read();
26305
+ async loadPoolData() {
26306
+ const data = await super.loadPoolData();
26169
26307
  this.legacyIds.clear();
26170
26308
  for (const account of data.accounts) for (const alias of workBuddyAccountIdAliases(account.credentials)) if (alias !== account.id) this.legacyIds.set(alias, account.id);
26171
26309
  return data;
@@ -26185,20 +26323,34 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26185
26323
  */
26186
26324
  async syncDesktopAccounts() {
26187
26325
  const discovered = await this.store.discoverDesktopCredentials();
26188
- if (discovered.length > 0) {
26189
- const data = await this.read();
26190
- const known = new Set(data.accounts.map((account) => workBuddyAccountId(account.credentials)));
26191
- const seen = /* @__PURE__ */ new Set();
26192
- for (const credentials of discovered) {
26193
- const id = workBuddyAccountId(credentials);
26194
- if (known.has(id) || seen.has(id)) continue;
26195
- seen.add(id);
26196
- await this.addAccount({
26197
- ...credentials,
26198
- source: "desktop"
26199
- });
26326
+ const candidates = [];
26327
+ const seen = /* @__PURE__ */ new Set();
26328
+ for (const raw of discovered) {
26329
+ const credentials = withResolvedIdentity({
26330
+ ...raw,
26331
+ source: "desktop"
26332
+ });
26333
+ const id = workBuddyAccountId(credentials);
26334
+ if (seen.has(id)) continue;
26335
+ seen.add(id);
26336
+ candidates.push(credentials);
26337
+ }
26338
+ if (candidates.length > 0) await this.updatePool((data) => {
26339
+ const existingKeys = /* @__PURE__ */ new Set();
26340
+ for (const account of data.accounts) {
26341
+ existingKeys.add(account.id);
26342
+ const key = this.hooks.dedupeKey?.(account.credentials);
26343
+ if (key !== void 0) existingKeys.add(key);
26200
26344
  }
26201
- }
26345
+ for (const credentials of candidates) {
26346
+ const key = this.hooks.dedupeKey?.(credentials);
26347
+ const id = this.hooks.accountId?.(credentials);
26348
+ if (key !== void 0 && existingKeys.has(key) || id !== void 0 && existingKeys.has(id)) continue;
26349
+ this.addAccountToPool(data, credentials);
26350
+ if (key !== void 0) existingKeys.add(key);
26351
+ if (id !== void 0) existingKeys.add(id);
26352
+ }
26353
+ });
26202
26354
  return this.listAccounts();
26203
26355
  }
26204
26356
  /**
@@ -26214,7 +26366,13 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26214
26366
  const live = await this.store.readDesktopFile(effective.account.sourceFile);
26215
26367
  if (live === null) return effective;
26216
26368
  if (isExpired(live)) return effective;
26217
- if (live.accessToken !== effective.credentials.accessToken) await this.updateAccountCredentials(effective.account.id, live).catch(() => void 0);
26369
+ if (live.accessToken !== effective.credentials.accessToken) {
26370
+ const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
26371
+ if (updated) return {
26372
+ account: updated,
26373
+ credentials: updated.credentials
26374
+ };
26375
+ }
26218
26376
  return {
26219
26377
  account: effective.account,
26220
26378
  credentials: live
@@ -26251,15 +26409,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
26251
26409
  return super.addAccount(withResolvedIdentity(credentials), alias);
26252
26410
  }
26253
26411
  /**
26254
- * Delete one account.
26412
+ * Remove one account inside a transaction.
26255
26413
  *
26256
26414
  * A desktop account is refused: the plugin does not own that credential file,
26257
26415
  * so removing it from the pool would strand the IDE's session while telling
26258
26416
  * the user their account was deleted. Hiding is the supported action there.
26259
26417
  */
26260
- async deleteAccount(accountId) {
26261
- if ((await this.read()).accounts.find((account) => account.id === accountId)?.source === "desktop") throw new Error("桌面账号由 CodeBuddy 客户端所有,只能隐藏,不能删除。");
26262
- await super.deleteAccount(accountId);
26418
+ deleteAccountFromPool(data, accountId) {
26419
+ if (data.accounts.find((account) => account.id === accountId)?.source === "desktop") throw new Error("桌面账号由 CodeBuddy 客户端所有,只能隐藏,不能删除。");
26420
+ super.deleteAccountFromPool(data, accountId);
26263
26421
  }
26264
26422
  };
26265
26423
  //#endregion
@@ -26351,6 +26509,17 @@ const OAUTH_BETA = "oauth-2025-04-20";
26351
26509
  const CLAUDE_CODE_BETA = "claude-code-20250219";
26352
26510
  /** Lets thinking blocks interleave with tool use across a multi-turn loop. */
26353
26511
  const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
26512
+ /**
26513
+ * Licenses `thinking.block_binding` in a request body.
26514
+ *
26515
+ * The field is not optional to license: the vendor documents that a body
26516
+ * carrying `block_binding` WITHOUT this beta is a 400 whose message ends
26517
+ * `block_binding: Extra inputs are not permitted` — every request, every turn.
26518
+ * The mid-convo thinking form always sends `block_binding`, so a request that
26519
+ * uses that form must carry this value. The reference sends it for exactly those
26520
+ * models (`THINKING_BINDING_CONTROLS_BETA`, pushed when `supportsMidConvoEffort`).
26521
+ */
26522
+ const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
26354
26523
  /** Anthropic protocol revision sent as `anthropic-version`. */
26355
26524
  const ANTHROPIC_VERSION = "2023-06-01";
26356
26525
  /**
@@ -26380,12 +26549,18 @@ const ANTHROPIC_VERSION = "2023-06-01";
26380
26549
  * trades a clear refusal for an unpredictable one. Note that npm's `stable`
26381
26550
  * dist-tag can itself sit below a model's floor, so `latest` is the tag to read.
26382
26551
  *
26552
+ * It must also be at or above the release that FIRST SHIPPED each model in the
26553
+ * table, whether or not a floor for that model has been observed: upstream has
26554
+ * so far gated a new model on the release that added it (Opus 5.5 arrived in
26555
+ * 2.1.280 and is refused below 2.1.280). Claude Sonnet 5.5 arrived in 2.1.284,
26556
+ * so 2.1.283 could not be relied on to reach it; 2.1.285 is npm `latest`.
26557
+ *
26383
26558
  * It is still not a literal that only moves when this package is republished: a
26384
26559
  * user whose installed baseline has aged past the server's floor has to be able
26385
26560
  * to raise it without waiting for a release. Read the effective value with
26386
26561
  * {@link claudeCliVersion}, pin one with {@link setClaudeCliVersion}.
26387
26562
  */
26388
- const CLAUDE_CLI_VERSION = "2.1.283";
26563
+ const CLAUDE_CLI_VERSION = "2.1.285";
26389
26564
  /**
26390
26565
  * Loopback callback port the subscription flow defaults to.
26391
26566
  *
@@ -26743,8 +26918,10 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
26743
26918
  * LOCAL ADDITIONS — the rows the reference snapshot predates
26744
26919
  * ---------------------------------------------------------------------------
26745
26920
  *
26746
- * `claude-opus-5-5` is the only such row today. The mirror list in the test is
26747
- * `LOCALLY_CURATED_MODEL_IDS`; the two must be changed together.
26921
+ * `claude-opus-5-5` and `claude-sonnet-5-5` are the two such rows today. The
26922
+ * mirror list in the test is `LOCALLY_CURATED_MODEL_IDS`; the two must be
26923
+ * changed together. Sonnet 5.5's own reasons are recorded at its row; what
26924
+ * follows below is Opus 5.5's.
26748
26925
  *
26749
26926
  * It is CURATED rather than transcribed because the snapshot this table copies
26750
26927
  * has no entry for it, and the one thing a transcription table must never do is
@@ -26796,8 +26973,8 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
26796
26973
  * the user has to reach.
26797
26974
  */
26798
26975
  /**
26799
- * Fifteen rows: the snapshot's own 14, in the snapshot's own declaration order,
26800
- * followed by the curated row the snapshot predates (see LOCAL ADDITIONS above).
26976
+ * Sixteen rows: the snapshot's own 14, in the snapshot's own declaration order,
26977
+ * followed by the curated rows the snapshot predates (see LOCAL ADDITIONS above).
26801
26978
  *
26802
26979
  * The order matters and is not cosmetic. The test asserts the snapshot's ids
26803
26980
  * appear in the table as a SUBSEQUENCE, so a curated row may be appended or
@@ -27065,7 +27242,26 @@ const CLAUDE_MODELS = Object.freeze([
27065
27242
  "max"
27066
27243
  ],
27067
27244
  canDisableThinking: false,
27068
- minCliVersion: "2.1.280"
27245
+ minCliVersion: "2.1.280",
27246
+ bindsThinkingToPrefix: true
27247
+ },
27248
+ {
27249
+ id: "claude-sonnet-5-5",
27250
+ name: "Claude Sonnet 5.5",
27251
+ contextWindow: 1e6,
27252
+ maxTokens: 128e3,
27253
+ supportsImage: true,
27254
+ supportsTemperature: false,
27255
+ thinkingMode: "mid-convo",
27256
+ reasoningEfforts: [
27257
+ "low",
27258
+ "medium",
27259
+ "high",
27260
+ "xhigh",
27261
+ "max"
27262
+ ],
27263
+ canDisableThinking: false,
27264
+ bindsThinkingToPrefix: true
27069
27265
  }
27070
27266
  ]);
27071
27267
  /** Every id the fallback table knows, in declaration order. */
@@ -27165,6 +27361,10 @@ function claudeThinkingMode(modelId, catalog) {
27165
27361
  function claudeModelSupportsTemperature(modelId, catalog) {
27166
27362
  return resolveClaudeModel(modelId, catalog ?? CLAUDE_MODELS).supportsTemperature;
27167
27363
  }
27364
+ /** Whether one model runs the documented preserved-thinking prefix check. */
27365
+ function claudeModelBindsThinkingToPrefix(modelId, catalog) {
27366
+ return resolveClaudeModel(modelId, catalog ?? CLAUDE_MODELS).bindsThinkingToPrefix === true;
27367
+ }
27168
27368
  /** Whether thinking can be turned off for one model. */
27169
27369
  function claudeModelCanDisableThinking(modelId, catalog) {
27170
27370
  return resolveClaudeModel(modelId, catalog ?? CLAUDE_MODELS).canDisableThinking;
@@ -27990,6 +28190,7 @@ function claudeBetas(options = {}) {
27990
28190
  if (options.model === void 0 || !isHaikuModel(options.model)) betas.push(CLAUDE_CODE_BETA);
27991
28191
  betas.push(OAUTH_BETA);
27992
28192
  if (options.thinking === true) betas.push(INTERLEAVED_THINKING_BETA);
28193
+ if (options.thinkingBinding === true) betas.push(THINKING_BINDING_CONTROLS_BETA);
27993
28194
  return betas;
27994
28195
  }
27995
28196
  /** The `User-Agent` value this line reports for one surface. */
@@ -28136,7 +28337,7 @@ async function performCatalogLoad(credentials, options) {
28136
28337
  let models;
28137
28338
  let live = false;
28138
28339
  try {
28139
- const response = await fetchFn("https://api.anthropic.com/v1/models", {
28340
+ const response = await fetchFn("https://api.anthropic.com/v1/models?limit=1000", {
28140
28341
  method: "GET",
28141
28342
  headers: buildClaudeHeaders(credentials.accessToken, { method: "GET" }),
28142
28343
  signal: timeoutSignal(options.signal, DISCOVERY_TIMEOUT_MS)
@@ -28603,8 +28804,15 @@ function classifyFailure(status, bodyText, headers = {}) {
28603
28804
  retryAfterMs: retryAfter
28604
28805
  };
28605
28806
  switch (envelope.type) {
28606
- case ERROR_TYPE.AUTHENTICATION:
28607
28807
  case ERROR_TYPE.PERMISSION: return {
28808
+ ...base,
28809
+ kind: "request",
28810
+ message: envelope.message ?? "This Claude account is not permitted to use the requested resource.",
28811
+ retryable: false,
28812
+ accountScoped: false,
28813
+ resetsAt: null
28814
+ };
28815
+ case ERROR_TYPE.AUTHENTICATION: return {
28608
28816
  ...base,
28609
28817
  kind: "credential",
28610
28818
  message: envelope.message ?? "Claude rejected the stored credential. Sign in again.",
@@ -28762,7 +28970,9 @@ function classifyFailure(status, bodyText, headers = {}) {
28762
28970
  * everything, including a caller asking for thinking off: the
28763
28971
  * block_binding exists so a prefix mismatch is dropped instead of
28764
28972
  * surfacing as a persistent 400, and suppressing it is exactly
28765
- * how that 400 comes back.
28973
+ * how that 400 comes back. The field is itself a 400 unless the
28974
+ * request carries the thinking-binding-controls beta, so the
28975
+ * adapter reads claudeBodyBindsThinking(body) to add it.
28766
28976
  * adaptive -> { type: 'adaptive', display: 'summarized' }, plus
28767
28977
  * output_config = { effort } when an effort was named. The budget
28768
28978
  * form is rejected by these models.
@@ -28855,11 +29065,12 @@ function classifyFailure(status, bodyText, headers = {}) {
28855
29065
  * 'error' event THROWS, because a severed reply flushed as a clean stop is a
28856
29066
  * wrong answer rather than a visible failure.
28857
29067
  *
28858
- * Reasoning deltas are BUFFERED and emitted as one block at content_block_stop
28859
- * (CHOICE, see emitPendingThinking): the signature that decides whether the
28860
- * block may be replayed at all arrives at the END of the block, so a signed
28861
- * block cannot be recognized before then, and a reasoning block that cannot be
28862
- * replayed is not content the caller should be told it received.
29068
+ * Reasoning deltas are STREAMED as they arrive (see openThinkingBlock); only the
29069
+ * replay entry waits for content_block_stop, because the signature that decides
29070
+ * whether the block may be replayed arrives at the END of the block. They used
29071
+ * to be buffered until the stop, which made DSH start the step's first-token
29072
+ * clock after all thinking was done - so TPS divided every thinking token by the
29073
+ * answer's time alone, and TTFT absorbed the whole thinking phase.
28863
29074
  *
28864
29075
  * ---------------------------------------------------------------------------
28865
29076
  * 7. PROMPT CACHING IS ON BY DEFAULT, AND IT IS THE REQUEST THAT TURNS IT ON
@@ -28958,9 +29169,17 @@ function jsonSafeValue(value, seen = /* @__PURE__ */ new WeakSet()) {
28958
29169
  seen.delete(value);
28959
29170
  }
28960
29171
  }
28961
- /** Strip a NUL that would truncate the string inside a JSON encoder. */
29172
+ /**
29173
+ * Strip what the wire cannot carry: a NUL, which would truncate the string
29174
+ * inside a JSON encoder, and an UNPAIRED surrogate. JSON.stringify writes the
29175
+ * latter as a lone `\uD83D` escape, which upstream rejects as invalid JSON ("no
29176
+ * low surrogate in string") - and since the text sits in history, every later
29177
+ * request fails the same way. A tool output sliced mid-emoji is enough to
29178
+ * produce one. Paired surrogates (emoji) are untouched. Same rule as the
29179
+ * reference's sanitizeSurrogates.
29180
+ */
28962
29181
  function sanitizeText(text) {
28963
- return text.replace(/\0/g, "");
29182
+ return text.replace(/\0|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
28964
29183
  }
28965
29184
  /**
28966
29185
  * Whether one failure is a cancellation rather than a real read failure.
@@ -29175,7 +29394,8 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
29175
29394
  if (mode === "adaptive") return {
29176
29395
  thinking: {
29177
29396
  type: "adaptive",
29178
- display: "summarized"
29397
+ display: "summarized",
29398
+ ...claudeModelBindsThinkingToPrefix(model) ? { block_binding: { prefix_mismatch_behavior: "drop_block" } } : {}
29179
29399
  },
29180
29400
  ...namedEffort === void 0 ? {} : { outputConfig: { effort: namedEffort } }
29181
29401
  };
@@ -29738,9 +29958,10 @@ function toolResultContent(blocks, images) {
29738
29958
  for (const block of blocks) {
29739
29959
  if (!isRecord$5(block)) continue;
29740
29960
  if (block.type === "text" && typeof block.text === "string") {
29741
- out.push({
29961
+ const text = sanitizeText(block.text);
29962
+ if (text !== "") out.push({
29742
29963
  type: "text",
29743
- text: sanitizeText(block.text)
29964
+ text
29744
29965
  });
29745
29966
  continue;
29746
29967
  }
@@ -29760,7 +29981,7 @@ function toolResultContent(blocks, images) {
29760
29981
  if (!hasImage) return void 0;
29761
29982
  if (out[0]?.type !== "text") out.unshift({
29762
29983
  type: "text",
29763
- text: ""
29984
+ text: "(see attached image)"
29764
29985
  });
29765
29986
  return out;
29766
29987
  }
@@ -29889,6 +30110,17 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
29889
30110
  return body;
29890
30111
  }
29891
30112
  /**
30113
+ * Whether one built body carries `thinking.block_binding`.
30114
+ *
30115
+ * Read off the BODY rather than re-derived from the catalog, because the header
30116
+ * this answers for (THINKING_BINDING_CONTROLS_BETA) exists to license that one
30117
+ * field: sent without it the field is a 400 on every request, so the header must
30118
+ * follow the bytes, not a second copy of the rule that produced them.
30119
+ */
30120
+ function claudeBodyBindsThinking(body) {
30121
+ return isRecord$5(body.thinking) && body.thinking.block_binding !== void 0;
30122
+ }
30123
+ /**
29892
30124
  * Fresh stream state.
29893
30125
  *
29894
30126
  * @param toolNames - the SAME ClaudeToolNames the request was built with, so a
@@ -29913,6 +30145,7 @@ function createStreamState$1(toolNames) {
29913
30145
  cacheReadTokens: 0,
29914
30146
  cacheWriteTokens: 0,
29915
30147
  reasoningTokens: 0,
30148
+ reportedReasoningTokens: void 0,
29916
30149
  sawUsage: false,
29917
30150
  finished: false
29918
30151
  };
@@ -30009,14 +30242,15 @@ function closeToolCall(state, contentIndex) {
30009
30242
  }];
30010
30243
  }
30011
30244
  /**
30012
- * Emit one buffered thinking block.
30245
+ * Close one thinking block: end it for the caller and record its replay entry.
30013
30246
  *
30014
- * A signed block is emitted as DSH reasoning and replayed verbatim afterwards.
30015
- * An unsigned one is emitted WITHOUT a replay entry: it is still shown to the
30016
- * caller (the model did think, and hiding it would misreport the turn) but it
30017
- * cannot be sent back, and the request builder drops it on the next turn.
30247
+ * Its text already reached the caller as it streamed (see openThinkingBlock);
30248
+ * what waits for the close is the replay entry, because the signature arrives
30249
+ * last. A signed block is replayed verbatim afterwards. An unsigned one is
30250
+ * still shown (the model did think) but the request builder drops it on the
30251
+ * next turn, since it cannot be sent back.
30018
30252
  */
30019
- function emitPendingThinking(state, contentIndex) {
30253
+ function closeThinkingBlock(state, contentIndex) {
30020
30254
  const pending = state.pending.get(contentIndex);
30021
30255
  if (pending === void 0) return [];
30022
30256
  state.pending.delete(contentIndex);
@@ -30036,20 +30270,45 @@ function emitPendingThinking(state, contentIndex) {
30036
30270
  ...pending.signature === "" ? {} : { signature: pending.signature }
30037
30271
  };
30038
30272
  state.reasoningTokens += estimateReasoningTokens(text);
30273
+ return [{
30274
+ type: "block-end",
30275
+ index: pending.index,
30276
+ block
30277
+ }];
30278
+ }
30279
+ /**
30280
+ * Open one thinking block and stream whatever text its start already carries.
30281
+ *
30282
+ * STREAMED, not held to content_block_stop: DSH times a step's first token from
30283
+ * the first non-empty delta, and the usage it divides by that window counts every
30284
+ * thinking token. Holding the text until the block closed started the clock
30285
+ * after all the thinking was done - inflating TPS and TTFT alike - and kept the
30286
+ * UI blank for the whole thinking phase. Only the replay entry waits for the
30287
+ * close, because the signature arrives last.
30288
+ */
30289
+ function openThinkingBlock(state, contentIndex, block, kind) {
30290
+ const index = state.blocks.length;
30291
+ state.blocks.push({
30292
+ type: "reasoning",
30293
+ text: ""
30294
+ });
30295
+ const seed = kind === "thinking" ? asString$2(block.thinking) ?? "" : "";
30296
+ state.pending.set(contentIndex, {
30297
+ index,
30298
+ kind,
30299
+ text: seed,
30300
+ signature: asString$2(block.signature) ?? "",
30301
+ data: asString$2(block.data) ?? ""
30302
+ });
30039
30303
  const out = [{
30040
30304
  type: "block-start",
30041
- index: pending.index,
30305
+ index,
30042
30306
  blockType: "reasoning"
30043
30307
  }];
30044
- if (text !== "") out.push({
30308
+ if (seed !== "") out.push({
30045
30309
  type: "reasoning-delta",
30046
- index: pending.index,
30047
- text
30048
- });
30049
- out.push({
30050
- type: "block-end",
30051
- index: pending.index,
30052
- block
30310
+ index,
30311
+ text: seed
30053
30312
  });
30054
30313
  return out;
30055
30314
  }
@@ -30066,7 +30325,7 @@ function flushOpenBlocks(state) {
30066
30325
  });
30067
30326
  for (const [contentIndex, pending] of state.pending) closers.push({
30068
30327
  index: pending.index,
30069
- close: () => emitPendingThinking(state, contentIndex)
30328
+ close: () => closeThinkingBlock(state, contentIndex)
30070
30329
  });
30071
30330
  closers.sort((left, right) => left.index - right.index);
30072
30331
  const out = [];
@@ -30085,17 +30344,25 @@ function flushOpenBlocks(state) {
30085
30344
  * the billed-input total unaccountable.
30086
30345
  */
30087
30346
  function tokenUsage(state) {
30347
+ const reasoningTokens = Math.min(state.reportedReasoningTokens ?? state.reasoningTokens, state.outputTokens);
30088
30348
  return {
30089
30349
  inputTokens: state.inputTokens,
30090
30350
  outputTokens: state.outputTokens,
30091
30351
  ...state.cacheReadTokens > 0 ? { cacheReadTokens: state.cacheReadTokens } : {},
30092
30352
  ...state.cacheWriteTokens > 0 ? { cacheWriteTokens: state.cacheWriteTokens } : {},
30093
- ...state.reasoningTokens > 0 ? { reasoningTokens: state.reasoningTokens } : {}
30353
+ ...reasoningTokens > 0 ? { reasoningTokens } : {}
30094
30354
  };
30095
30355
  }
30096
30356
  function finishReasonFor(state) {
30097
30357
  const reason = state.finishReason ?? "";
30098
- if (reason === "max_tokens") return { kind: "max-tokens" };
30358
+ if (reason === "refusal" || reason === "sensitive") return {
30359
+ kind: "error",
30360
+ failure: {
30361
+ message: state.stopExplanation ?? "Claude declined to respond (stop_reason: " + reason + ").",
30362
+ code: "PROVIDER_ERROR"
30363
+ }
30364
+ };
30365
+ if (reason === "max_tokens" || reason === "model_context_window_exceeded") return { kind: "max-tokens" };
30099
30366
  if (state.hasToolCall || reason === "tool_use") return { kind: "tool-calls" };
30100
30367
  return { kind: "stop" };
30101
30368
  }
@@ -30143,6 +30410,8 @@ function applyUsage(state, usage, cumulativeOutput) {
30143
30410
  if (usage.cache_creation_input_tokens !== void 0) state.cacheWriteTokens = numberOr(usage.cache_creation_input_tokens, state.cacheWriteTokens);
30144
30411
  if (usage.cache_read_input_tokens !== void 0) state.cacheReadTokens = numberOr(usage.cache_read_input_tokens, state.cacheReadTokens);
30145
30412
  if (usage.output_tokens !== void 0) state.outputTokens = cumulativeOutput ? numberOr(usage.output_tokens, state.outputTokens) : state.outputTokens + numberOr(usage.output_tokens, 0);
30413
+ const details = isRecord$5(usage.output_tokens_details) ? usage.output_tokens_details : void 0;
30414
+ if (details !== void 0 && details.thinking_tokens !== void 0) state.reportedReasoningTokens = numberOr(details.thinking_tokens, state.reportedReasoningTokens ?? 0);
30146
30415
  }
30147
30416
  /**
30148
30417
  * Feed one SSE line of a Messages response into the state.
@@ -30213,19 +30482,8 @@ function processStreamLine(line, state) {
30213
30482
  return out;
30214
30483
  }
30215
30484
  if (blockType === "thinking" || blockType === "redacted_thinking") {
30216
- if (state.pending.get(contentIndex) !== void 0) out.push(...emitPendingThinking(state, contentIndex));
30217
- const index = state.blocks.length;
30218
- state.blocks.push({
30219
- type: "reasoning",
30220
- text: ""
30221
- });
30222
- state.pending.set(contentIndex, {
30223
- index,
30224
- kind: blockType === "redacted_thinking" ? "redacted_thinking" : "thinking",
30225
- text: asString$2(block.thinking) ?? "",
30226
- signature: asString$2(block.signature) ?? "",
30227
- data: asString$2(block.data) ?? ""
30228
- });
30485
+ out.push(...closeThinkingBlock(state, contentIndex));
30486
+ out.push(...openThinkingBlock(state, contentIndex, block, blockType));
30229
30487
  return out;
30230
30488
  }
30231
30489
  out.push(...openTextAt(state, contentIndex).chunks);
@@ -30258,8 +30516,14 @@ function processStreamLine(line, state) {
30258
30516
  }
30259
30517
  if (deltaType === "thinking_delta") {
30260
30518
  const pending = state.pending.get(contentIndex);
30261
- const text = asString$2(delta.thinking) ?? "";
30262
- if (pending !== void 0 && text !== "") pending.text += sanitizeText(text);
30519
+ const text = sanitizeText(asString$2(delta.thinking) ?? "");
30520
+ if (pending === void 0 || pending.kind !== "thinking" || text === "") return out;
30521
+ pending.text += text;
30522
+ out.push({
30523
+ type: "reasoning-delta",
30524
+ index: pending.index,
30525
+ text
30526
+ });
30263
30527
  return out;
30264
30528
  }
30265
30529
  const text = asString$2(delta.text) ?? "";
@@ -30278,13 +30542,16 @@ function processStreamLine(line, state) {
30278
30542
  if (type === "content_block_stop") {
30279
30543
  const contentIndex = numberOr(event.index, -1);
30280
30544
  if (state.toolCalls.has(contentIndex)) return closeToolCall(state, contentIndex);
30281
- if (state.pending.has(contentIndex)) return emitPendingThinking(state, contentIndex);
30545
+ if (state.pending.has(contentIndex)) return closeThinkingBlock(state, contentIndex);
30282
30546
  return closeTextBlock(state, contentIndex);
30283
30547
  }
30284
30548
  if (type === "message_delta") {
30285
30549
  const delta = isRecord$5(event.delta) ? event.delta : void 0;
30286
30550
  const stop = delta === void 0 ? void 0 : asString$2(delta.stop_reason);
30287
30551
  if (stop !== void 0 && stop !== "") state.finishReason = stop;
30552
+ const details = delta !== void 0 && isRecord$5(delta.stop_details) ? delta.stop_details : void 0;
30553
+ const explanation = details === void 0 ? void 0 : asString$2(details.explanation);
30554
+ if (explanation !== void 0 && explanation !== "") state.stopExplanation = explanation;
30288
30555
  const usage = isRecord$5(event.usage) ? event.usage : void 0;
30289
30556
  if (usage !== void 0) applyUsage(state, usage, true);
30290
30557
  return out;
@@ -30719,7 +30986,7 @@ async function refreshAccessToken(refreshToken, options = {}) {
30719
30986
  try {
30720
30987
  return await postTokenGrant(body, options, false, [refreshToken]);
30721
30988
  } catch (error) {
30722
- if (error instanceof ClaudeUnauthorizedError) throw error;
30989
+ if (!(error instanceof ClaudeRetryableError)) throw error;
30723
30990
  lastError = error;
30724
30991
  }
30725
30992
  if (attempt < REFRESH_MAX_ATTEMPTS - 1) await sleep(REFRESH_BACKOFF_BASE_MS * 2 ** attempt, options.signal);
@@ -31804,19 +32071,20 @@ var ClaudeAdapter = class extends LlmAdapter {
31804
32071
  * no credentials were resolved for it.
31805
32072
  */
31806
32073
  async attemptRequest(credentials, requestOptions, images, toolNames, thinking, signal, fetchFn) {
31807
- const body = JSON.stringify(buildClaudeRequestBody(requestOptions, images, {
32074
+ const payload = buildClaudeRequestBody(requestOptions, images, {
31808
32075
  toolNames,
31809
32076
  cacheControl: true
31810
- }));
32077
+ });
31811
32078
  try {
31812
32079
  return await fetchFn("https://api.anthropic.com/v1/messages?beta=true", {
31813
32080
  method: "POST",
31814
32081
  headers: buildClaudeHeaders(credentials.accessToken, {
31815
32082
  model: requestOptions.model,
31816
32083
  thinking,
32084
+ thinkingBinding: claudeBodyBindsThinking(payload),
31817
32085
  method: "POST"
31818
32086
  }),
31819
- body,
32087
+ body: JSON.stringify(payload),
31820
32088
  signal
31821
32089
  });
31822
32090
  } catch (error) {
@@ -32467,7 +32735,7 @@ function isAdoptedCredentialExpired(credential, now = Date.now()) {
32467
32735
  * caller in the process, so with a pool it would hand the second account the
32468
32736
  * first account's freshly rotated credential — and, through the adapter above,
32469
32737
  * write it into the second account's record. The pool therefore keeps its OWN
32470
- * single-flight, per account (see the inFlight map), which preserves the property
32738
+ * single-flight, per pool file and account, which preserves the property
32471
32739
  * the frozen module wanted (one rotation per expiry, no two callers spending the
32472
32740
  * same rotating token) without the cross-account contamination.
32473
32741
  *
@@ -32853,7 +33121,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
32853
33121
  * marked expired immediately after a refresh that in fact succeeded. Keyed by
32854
33122
  * the record's id — never by a token — so it cannot mix two accounts up.
32855
33123
  */
32856
- inFlight = /* @__PURE__ */ new Map();
32857
33124
  constructor(options = {}) {
32858
33125
  const store = options.store ?? new FileCredentialStore();
32859
33126
  const refreshSeam = options.refreshCredentials ?? ((credentials, fetchFn) => refreshClaudeCredential(credentials, fetchFn));
@@ -32900,6 +33167,10 @@ var ClaudeAccountPool = class extends AccountPoolCore {
32900
33167
  if (record === void 0) return null;
32901
33168
  return poolAccountForRecord(record, true);
32902
33169
  },
33170
+ credentialsCommitted: async (account) => {
33171
+ if (isAdoptedPoolCredential(account.credentials)) return;
33172
+ await store.saveAccount(account.credentials, { internalId: account.id });
33173
+ },
32903
33174
  mirrorPrimary: async (credentials) => {
32904
33175
  if (credentials === null) {
32905
33176
  await store.delete();
@@ -32961,8 +33232,8 @@ var ClaudeAccountPool = class extends AccountPoolCore {
32961
33232
  * {@link recordIdsFor} reads is kept in step with the rows that actually exist.
32962
33233
  * See the recordIds field for why entries are not pruned.
32963
33234
  */
32964
- async read() {
32965
- const data = await super.read();
33235
+ async loadPoolData() {
33236
+ const data = await super.loadPoolData();
32966
33237
  for (const account of data.accounts) this.indexAccount(account);
32967
33238
  return data;
32968
33239
  }
@@ -32975,10 +33246,27 @@ var ClaudeAccountPool = class extends AccountPoolCore {
32975
33246
  * 'one account, one key' section.
32976
33247
  */
32977
33248
  async addAccount(credentials, alias) {
32978
- const data = await this.read();
33249
+ return this.updatePool((data) => this.addManagedAccount(data, credentials, alias)).catch((error) => {
33250
+ this.recordIds.clear();
33251
+ throw error;
33252
+ });
33253
+ }
33254
+ async addManagedAccount(data, credentials, alias) {
32979
33255
  if (isAdoptedPoolCredential(credentials)) return this.addAdoptedAccount(credentials, data, alias);
33256
+ parseClaudeCredentials(credentials);
32980
33257
  const knownId = await this.recordIdFor(credentials);
32981
- const record = await this.store.saveAccount(credentials);
33258
+ const document = await this.store.read();
33259
+ const keys = identityKeysFor(credentials);
33260
+ const stored = document?.accounts.find((record) => record.identityKeys.some((key) => keys.includes(key)));
33261
+ const record = stored === void 0 ? {
33262
+ internalId: data.accounts.find((account) => account.id === knownId)?.adopted === false ? knownId ?? createInternalId() : createInternalId(),
33263
+ identityKeys: keys,
33264
+ credentials
33265
+ } : {
33266
+ ...stored,
33267
+ identityKeys: mergeIdentityKeys(stored.identityKeys, keys),
33268
+ credentials
33269
+ };
32982
33270
  this.indexRecord(record);
32983
33271
  if (knownId !== void 0 && knownId !== record.internalId) {
32984
33272
  const row = data.accounts.find((account) => account.id === knownId);
@@ -32990,10 +33278,9 @@ var ClaudeAccountPool = class extends AccountPoolCore {
32990
33278
  delete row.sourcePath;
32991
33279
  row.identityKeys = mergeIdentityKeys(row.identityKeys, record.identityKeys);
32992
33280
  if (data.activeAccountId === knownId) data.activeAccountId = record.internalId;
32993
- await this.write(data);
32994
33281
  }
32995
33282
  }
32996
- return super.addAccount(credentials, alias);
33283
+ return this.addAccountToPool(data, credentials, alias);
32997
33284
  }
32998
33285
  /**
32999
33286
  * Add, or re-import, one borrowed Claude Code snapshot.
@@ -33022,9 +33309,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33022
33309
  const candidateId = byIdentity ?? this.recordIds.get(sourceKey);
33023
33310
  const existing = candidateId === void 0 ? void 0 : data.accounts.find((account) => account.id === candidateId);
33024
33311
  if (existing !== void 0 && existing.adopted !== true) {
33025
- const before = existing.identityKeys.length;
33026
33312
  existing.identityKeys = mergeIdentityKeys(existing.identityKeys, identityKeysFor(credentials));
33027
- if (existing.identityKeys.length !== before) await this.write(data);
33028
33313
  return existing;
33029
33314
  }
33030
33315
  if (candidateId === void 0) {
@@ -33035,7 +33320,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33035
33320
  this.recordIds.set(sourceKey, candidateId);
33036
33321
  this.indexCredential(credentials, candidateId);
33037
33322
  }
33038
- return super.addAccount(credentials, alias);
33323
+ return this.addAccountToPool(data, credentials, alias);
33039
33324
  }
33040
33325
  /**
33041
33326
  * Merge two rows that are one account.
@@ -33058,7 +33343,21 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33058
33343
  */
33059
33344
  async mergeAccounts(keepAccountId, dropAccountId) {
33060
33345
  if (keepAccountId === dropAccountId) throw new Error("A Claude pool merge needs two different accounts.");
33061
- const data = await this.read();
33346
+ let originals;
33347
+ return await this.updatePool(async (data) => {
33348
+ const keep = data.accounts.find((account) => account.id === keepAccountId);
33349
+ const drop = data.accounts.find((account) => account.id === dropAccountId);
33350
+ if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
33351
+ originals = {
33352
+ keep,
33353
+ drop
33354
+ };
33355
+ return this.mergeAccountsInPool(data, keepAccountId, dropAccountId);
33356
+ }, async (_data, result) => {
33357
+ if (originals !== void 0) await this.reconcileMergedDocument(originals.keep, originals.drop, result);
33358
+ });
33359
+ }
33360
+ async mergeAccountsInPool(data, keepAccountId, dropAccountId) {
33062
33361
  const keep = data.accounts.find((account) => account.id === keepAccountId);
33063
33362
  const drop = data.accounts.find((account) => account.id === dropAccountId);
33064
33363
  if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
@@ -33107,9 +33406,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33107
33406
  data.accounts[index] = merged;
33108
33407
  data.accounts = data.accounts.filter((account) => account.id !== dropAccountId);
33109
33408
  if (data.activeAccountId === dropAccountId) data.activeAccountId = merged.id;
33110
- await this.write(data);
33111
- if (merged.isPrimary) this.hooks.mirrorPrimary?.(merged.credentials).catch(() => void 0);
33112
- await this.reconcileMergedDocument(keep, drop, merged);
33113
33409
  return merged;
33114
33410
  }
33115
33411
  /**
@@ -33149,10 +33445,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33149
33445
  * neither path.
33150
33446
  */
33151
33447
  async removeImportedAccount(accountId) {
33152
- const target = (await this.read()).accounts.find((account) => account.id === accountId);
33153
- if (target === void 0) return;
33154
- if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
33155
- await super.deleteAccount(accountId);
33448
+ return this.updatePool((data) => {
33449
+ const target = data.accounts.find((account) => account.id === accountId);
33450
+ if (target === void 0) return;
33451
+ if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
33452
+ super.deleteAccountFromPool(data, accountId);
33453
+ });
33156
33454
  }
33157
33455
  /**
33158
33456
  * Delete one plugin-owned account.
@@ -33165,9 +33463,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33165
33463
  * back a credential the user just deleted.
33166
33464
  */
33167
33465
  async deleteAccount(accountId) {
33168
- if ((await this.read()).accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from Claude Code is not an account this plugin owns; remove the import instead.");
33169
- await super.deleteAccount(accountId);
33170
- await this.store.deleteAccount(accountId);
33466
+ return this.updatePool((data) => {
33467
+ if (data.accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from Claude Code is not an account this plugin owns; remove the import instead.");
33468
+ super.deleteAccountFromPool(data, accountId);
33469
+ }, async () => {
33470
+ await this.store.deleteAccount(accountId);
33471
+ });
33171
33472
  }
33172
33473
  /**
33173
33474
  * The immutable id of the record that owns a credential, or nothing.
@@ -33247,44 +33548,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
33247
33548
  */
33248
33549
  refreshCredentials(credentials, fetchFn) {
33249
33550
  if (isAdoptedPoolCredential(credentials)) return Promise.reject(new ClaudeAdoptedCredentialError(adoptedCredentialExpired(credentials) ? ADOPTED_CREDENTIAL_EXPIRED_HINT : ADOPTED_NEVER_REFRESHED_MESSAGE));
33250
- return this.singleFlightRefresh(credentials, fetchFn);
33251
- }
33252
- /**
33253
- * The per-account single-flight, resolving the account's id first.
33254
- *
33255
- * Separated from the hook above only because resolving the id consults the
33256
- * credential document, and a hook cannot await: the guard stays synchronous so
33257
- * it can never be skipped by a caller that forgets to wait.
33258
- */
33259
- async singleFlightRefresh(credentials, fetchFn) {
33260
- const key = await this.recordIdFor(credentials);
33261
- if (key !== void 0) {
33262
- const shared = this.inFlight.get(key);
33263
- if (shared !== void 0) return shared;
33264
- }
33265
- const pending = this.performRefresh(credentials, fetchFn);
33266
- if (key !== void 0) {
33267
- this.inFlight.set(key, pending);
33268
- pending.then(() => void 0, () => void 0).then(() => {
33269
- if (this.inFlight.get(key) === pending) this.inFlight.delete(key);
33270
- });
33271
- }
33272
- return pending;
33273
- }
33274
- /**
33275
- * Rotate one managed credential and keep the credential document in step.
33276
- *
33277
- * The document write is what stops the pre-pool projection from handing out a
33278
- * token the rotation has already spent. Its failure is swallowed on purpose:
33279
- * the credential WAS refreshed, the pool's own record holds it, and a mirror
33280
- * that could not be written must not fail a request that can still be served —
33281
- * the same rule the core applies to its own bookkeeping writes.
33282
- */
33283
- async performRefresh(credentials, fetchFn) {
33284
- const recordId = await this.recordIdFor(credentials);
33285
- const refreshed = await this.refreshSeam(credentials, fetchFn);
33286
- if (recordId !== void 0) await new ClaudeCredentialStoreAdapter(this.store, recordId).write(refreshed).catch(() => void 0);
33287
- return refreshed;
33551
+ return this.refreshSeam(credentials, fetchFn);
33288
33552
  }
33289
33553
  };
33290
33554
  //#endregion
@@ -33586,24 +33850,18 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
33586
33850
  * is addressed at THAT account, which is what makes a re-login repair the row
33587
33851
  * the user clicked instead of adding a second one.
33588
33852
  */
33589
- const loginStore = (accountId) => {
33590
- const base = pool !== void 0 && accountId !== void 0 ? pool.credentialStoreFor(accountId) : {
33591
- read: async () => (await activeAccount()).credentials ?? null,
33592
- write: async (credentials) => {
33593
- if (pool !== void 0) await pool.addAccount(credentials);
33594
- else await store.saveAccount(credentials);
33595
- }
33596
- };
33597
- return {
33598
- read: () => base.read(),
33599
- write: async (credentials) => {
33600
- await base.write(credentials);
33601
- if (pool !== void 0 && accountId !== void 0) await pool.clearAuthFailed(accountId).catch(() => void 0);
33602
- clearCachedQuota$1();
33603
- clearCachedCatalog();
33853
+ const loginStore = (accountId) => ({
33854
+ read: async () => pool !== void 0 && accountId !== void 0 ? pool.credentialStoreFor(accountId).read() : (await activeAccount()).credentials ?? null,
33855
+ write: async (credentials) => {
33856
+ if (pool === void 0) await store.saveAccount(credentials);
33857
+ else {
33858
+ const landed = await pool.addAccount(credentials);
33859
+ if (accountId !== void 0 && landed.id === accountId) await pool.clearAuthFailed(accountId).catch(() => void 0);
33604
33860
  }
33605
- };
33606
- };
33861
+ clearCachedQuota$1();
33862
+ clearCachedCatalog();
33863
+ }
33864
+ });
33607
33865
  const readStatus = (quotaError = null) => activeAccount().then((active) => getClaudeWebStatus(store, modelSettings, preferences, options, quotaError, active));
33608
33866
  return ctx.webServer.register({
33609
33867
  kind: "prefix",
@@ -37008,9 +37266,9 @@ function appendWindow(current, addition, limit) {
37008
37266
  /**
37009
37267
  * Install the reasoning-collapse guard on a Context.
37010
37268
  *
37011
- * The guard is inert unless the host exposes the `llm/stream` seam, so a
37012
- * harness without it keeps the built-in behaviour instead of failing this
37013
- * plugin's load.
37269
+ * The guard is inert unless the host exposes the event bus, so a harness
37270
+ * without it keeps the built-in behaviour instead of failing this plugin's
37271
+ * load.
37014
37272
  *
37015
37273
  * Resuming needs the live Agent for `cancel` and `steer`, and no supported
37016
37274
  * generation hands the agent to `llm/stream`. `agent/created` reports it on
@@ -37020,19 +37278,19 @@ function appendWindow(current, addition, limit) {
37020
37278
  *
37021
37279
  * @param ctx - plugin context.
37022
37280
  * @param options - see {@link GuardOptions}; validated fail-loud.
37023
- * @returns a disposer, or undefined when the seam is unavailable.
37281
+ * @returns a disposer removing both listeners, or undefined when the host
37282
+ * exposes no event bus.
37024
37283
  */
37025
37284
  function installReasoningCollapseGuard(ctx, options = {}) {
37026
37285
  const resolved = resolveGuardOptions(options);
37027
- const runtime = ctx.llm;
37028
- if (runtime === void 0 || typeof runtime.stream !== "function") return void 0;
37029
- const original = runtime.stream;
37286
+ if (typeof ctx.on !== "function") return void 0;
37030
37287
  const watched = new Set(resolved.includeModels);
37031
37288
  const breakers = /* @__PURE__ */ new WeakMap();
37032
37289
  let currentAgent;
37033
- const releaseAgent = typeof ctx.on === "function" ? ctx.on("agent/created", ({ agent }) => {
37034
- currentAgent = agent;
37035
- }) : void 0;
37290
+ const releaseAgent = ctx.on("agent/created", (payload) => {
37291
+ currentAgent = payload.agent;
37292
+ });
37293
+ const releaseStream = ctx.on("llm/stream", guardStream, { global: true });
37036
37294
  function breakerFor(key) {
37037
37295
  let state = breakers.get(key);
37038
37296
  if (state === void 0) {
@@ -37070,7 +37328,7 @@ function installReasoningCollapseGuard(ctx, options = {}) {
37070
37328
  agent.steer?.(createResumeMessage(text));
37071
37329
  });
37072
37330
  }
37073
- runtime.stream = function reasoningCollapseGuardStream(request, next) {
37331
+ function guardStream(request, next) {
37074
37332
  const state = breakerFor(request.signal ?? STATIC_KEY);
37075
37333
  if (state.exhausted || !watchedModel(request.model)) return next();
37076
37334
  async function* guarded() {
@@ -37092,9 +37350,9 @@ function installReasoningCollapseGuard(ctx, options = {}) {
37092
37350
  }
37093
37351
  }
37094
37352
  return guarded();
37095
- };
37353
+ }
37096
37354
  return () => {
37097
- runtime.stream = original;
37355
+ releaseListener(releaseStream);
37098
37356
  releaseListener(releaseAgent);
37099
37357
  };
37100
37358
  }
@@ -37186,7 +37444,7 @@ function apply(ctx, pluginConfig = {}) {
37186
37444
  ctx.effect(() => {
37187
37445
  const dispose = installReasoningCollapseGuard(ctx, guardOptions);
37188
37446
  if (dispose === void 0) {
37189
- ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no llm/stream seam");
37447
+ ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no event bus");
37190
37448
  return () => void 0;
37191
37449
  }
37192
37450
  return dispose;