dsh-xdpool 2.4.6 → 2.5.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
@@ -15,6 +15,15 @@ import { env } from "node:process";
15
15
  import http, { createServer, request } from "node:http";
16
16
  import https, { request as request$1 } from "node:https";
17
17
  import { Readable, Transform } from "node:stream";
18
+ //#region src/credit-reserve.ts
19
+ /** Build a merge-safe reserve update. Zero overrides inherited/previous floors. */
20
+ function creditReservesWith(current, accountId, reserve) {
21
+ return {
22
+ ...current,
23
+ [accountId]: reserve
24
+ };
25
+ }
26
+ //#endregion
18
27
  //#region src/badges.ts
19
28
  /**
20
29
  * Promotion badges the upstream attaches to a model.
@@ -390,10 +399,39 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
390
399
  * rewrite is applied to assistant history too — one past reply carrying a
391
400
  * fingerprint would otherwise fail every later request in the conversation.
392
401
  *
402
+ * A THIRD FINGERPRINT (measured against the live domestic gateway 2026-10-28)
403
+ *
404
+ * The file-reference guidance DSH contributes whenever the agent has a `read`
405
+ * tool reintroduced the same 400/11128 after the two rewrites above shipped:
406
+ *
407
+ * Tokens prefixed with @ are paths the user explicitly referenced.
408
+ * Relative paths resolve from the workspace root; absolute paths identify
409
+ * files or directories on the host.
410
+ *
411
+ * Ordinary phrasing throughout — the blocked region names no product — so the
412
+ * fingerprint is not "this prompt is not WorkBuddy's" but a literal substring
413
+ * list that happens to include this sentence. Neither sentence is the trigger
414
+ * on its own: each 200s in isolation, and the pair 400s; the smallest edit
415
+ * anywhere in the pair clears it, which is what marks it as content and not
416
+ * prompt length, and the matching half is the second sentence ("Tokens
417
+ * prefixed with @" alone stayed clean while the "Relative paths…" line was the
418
+ * one whose text the gateway recognised).
419
+ *
420
+ * The rewrite turns the indicative into the same statement's participle —
421
+ * "Relative paths resolve from the workspace root" becomes "Relative paths are
422
+ * resolved against the workspace root". Meaning, length class and line breaks
423
+ * are preserved; the substring the gateway lists is gone. Verified at 200 with
424
+ * the full runtime system prompt (≈41k chars) that reproduced the 400 on the
425
+ * same account, and with the pair alone.
426
+ *
393
427
  * The list is an ARRAY because these fingerprints are appended to over time; a
394
428
  * future rejection should be one more row here, not a new code path.
395
429
  */
396
- const BANNED_SYSTEM_PHRASES = Object.freeze([[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"], [/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface"]]);
430
+ const BANNED_SYSTEM_PHRASES = Object.freeze([
431
+ [/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"],
432
+ [/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface"],
433
+ [/Relative paths resolve from the workspace root/gi, "Relative paths are resolved against the workspace root"]
434
+ ]);
397
435
  /**
398
436
  * Rewrite fingerprinted system/assistant content, leaving everything else
399
437
  * byte-identical.
@@ -1124,7 +1162,8 @@ function parseTask(value) {
1124
1162
  status: typeof raw["status"] === "string" ? raw["status"] : "",
1125
1163
  claimable: !claimed && target > 0 && current >= target,
1126
1164
  claimed,
1127
- locked: raw["locked"] === true
1165
+ locked: raw["locked"] === true,
1166
+ jumpUrl: typeof raw["jump_url"] === "string" ? raw["jump_url"] : ""
1128
1167
  };
1129
1168
  }
1130
1169
  var WorkBuddyUpstreamClient = class {
@@ -3840,11 +3879,11 @@ var WorkBuddyAccountPool = class {
3840
3879
  * model id the legacy account-wide check applies (callers that cannot name a
3841
3880
  * model, e.g. CLI diagnostics).
3842
3881
  */
3843
- available(now, modelId, region) {
3882
+ available(now, modelId, region, reserveExempt) {
3844
3883
  return this.accounts.filter((account) => {
3845
3884
  if (this.disabledIds.has(account.id)) return false;
3846
3885
  const reserve = this.creditReserves.get(account.id);
3847
- if (reserve !== void 0 && reserve > 0) {
3886
+ if (!reserveExempt?.has(account.id) && reserve !== void 0 && reserve > 0) {
3848
3887
  const balance = this.creditBalances.get(account.id);
3849
3888
  if (balance !== void 0 && balance <= reserve) return false;
3850
3889
  }
@@ -3868,7 +3907,7 @@ var WorkBuddyAccountPool = class {
3868
3907
  * Counts are over the region's accounts, since a provider only ever sees its
3869
3908
  * own gateway.
3870
3909
  */
3871
- unavailableReason(modelId, region) {
3910
+ unavailableReason(modelId, region, reserveExempt) {
3872
3911
  const now = Date.now();
3873
3912
  const inRegion = this.accounts.filter((account) => region === void 0 || regionOf(account.credential.domain) === region);
3874
3913
  if (inRegion.length === 0) return {
@@ -3881,6 +3920,29 @@ var WorkBuddyAccountPool = class {
3881
3920
  let cooling = 0;
3882
3921
  let disabled = 0;
3883
3922
  let dead = 0;
3923
+ /**
3924
+ * Accounts that survived every OTHER filter and are held back only by their
3925
+ * credit floor.
3926
+ *
3927
+ * Counted rather than returned on the first hit. The old `return` fired the
3928
+ * moment it met one reserved account, so a single account parked under its
3929
+ * floor reported `reserve` for the whole gateway — even while four other
3930
+ * accounts were healthy and had just been tried. The caller turns
3931
+ * `reason === 'reserve'` into a 402 telling the user to lower the reserve,
3932
+ * which pointed at a setting that was not the problem (#98: a five-account
3933
+ * pool reported `reserve_floor` because of one account that had sat below
3934
+ * its floor for weeks, while the actual state was "the rest are cooling").
3935
+ */
3936
+ let reserved = 0;
3937
+ /**
3938
+ * Accounts that reached the end of the walk unblocked.
3939
+ *
3940
+ * Non-zero means the pool still has somewhere to send a request, so a
3941
+ * reserve is NOT what this answer is about: editing a number cannot fix a
3942
+ * request that some other account is already able to serve, and saying
3943
+ * otherwise sends the reader to the wrong setting.
3944
+ */
3945
+ let usable = 0;
3884
3946
  for (const account of inRegion) {
3885
3947
  if (this.disabledIds.has(account.id)) {
3886
3948
  disabled += 1;
@@ -3896,16 +3958,14 @@ var WorkBuddyAccountPool = class {
3896
3958
  continue;
3897
3959
  }
3898
3960
  const reserve = this.creditReserves.get(account.id);
3899
- if (reserve !== void 0 && reserve > 0) {
3961
+ if (!reserveExempt?.has(account.id) && reserve !== void 0 && reserve > 0) {
3900
3962
  const balance = this.creditBalances.get(account.id);
3901
- if (balance !== void 0 && balance <= reserve) return {
3902
- total: inRegion.length,
3903
- cooling,
3904
- disabled,
3905
- dead,
3906
- reason: "reserve"
3907
- };
3963
+ if (balance !== void 0 && balance <= reserve) {
3964
+ reserved += 1;
3965
+ continue;
3966
+ }
3908
3967
  }
3968
+ usable += 1;
3909
3969
  }
3910
3970
  if (dead > 0) return {
3911
3971
  total: inRegion.length,
@@ -3928,6 +3988,13 @@ var WorkBuddyAccountPool = class {
3928
3988
  dead,
3929
3989
  reason: "disabled"
3930
3990
  };
3991
+ if (usable === 0 && reserved > 0) return {
3992
+ total: inRegion.length,
3993
+ cooling,
3994
+ disabled,
3995
+ dead,
3996
+ reason: "reserve"
3997
+ };
3931
3998
  return {
3932
3999
  total: inRegion.length,
3933
4000
  cooling,
@@ -4104,16 +4171,18 @@ var WorkBuddyAccountPool = class {
4104
4171
  * Scans on first use, and rescans when every known account is cooling down: a
4105
4172
  * fresh desktop login is the usual way out of an exhausted pool.
4106
4173
  *
4174
+ * `reserveExempt` is host-only: account ids whose own credentials freshly
4175
+ * confirmed a zero-credit model. It never bypasses disabled, dead credentials, cooldowns or region.
4107
4176
  * `conversationKey` is only consulted under `sticky`; other modes ignore it.
4108
4177
  */
4109
- async acquire(modelId, region, conversationKey) {
4178
+ async acquire(modelId, region, conversationKey, reserveExempt) {
4110
4179
  if (this.accounts.length === 0) await this.scan();
4111
4180
  const maxTries = this.accounts.length + 1;
4112
4181
  for (let attempt = 0; attempt < maxTries; attempt += 1) {
4113
- let pool = this.available(Date.now(), modelId, region);
4182
+ let pool = this.available(Date.now(), modelId, region, reserveExempt);
4114
4183
  if (pool.length === 0 && attempt === 0) {
4115
4184
  await this.scan();
4116
- pool = this.available(Date.now(), modelId, region);
4185
+ pool = this.available(Date.now(), modelId, region, reserveExempt);
4117
4186
  }
4118
4187
  if (pool.length === 0) return void 0;
4119
4188
  if (this.preferredAccountId !== void 0) {
@@ -4966,22 +5035,6 @@ const POOL_COMBOS_SAVE_PATH = "/plugins/dsh-xdpool/combos/save";
4966
5035
  */
4967
5036
  const POOL_RELAY_PATH = "/plugins/dsh-xdpool/relay";
4968
5037
  /**
4969
- * What the locally running billion-context proxy is doing, read-only.
4970
- *
4971
- * Its own route because the answer comes from ANOTHER plugin: billion-context
4972
- * runs its own loopback listener and publishes its own JSON, so this route only
4973
- * finds that listener and relays a filtered subset. Nothing here is folded into
4974
- * {@link POOL_STATUS_PATH}: the pool card polls that every 30 seconds, and a
4975
- * third-party proxy's session table is not something every pool poll should
4976
- * carry.
4977
- *
4978
- * The word "read-only" is load-bearing on both ends. This plugin sends GETs
4979
- * only — it never configures, restarts or writes to the proxy — and the payload
4980
- * it relays deliberately omits every field that could carry conversation
4981
- * material.
4982
- */
4983
- const POOL_BILLION_PATH = "/plugins/dsh-xdpool/billion";
4984
- /**
4985
5038
  * Read the usage panel's aggregate.
4986
5039
  *
4987
5040
  * GET only, and token-free by construction: the recorder holds counts and
@@ -12198,65 +12251,6 @@ function createWorkBuddyAdapter(options) {
12198
12251
  };
12199
12252
  }
12200
12253
  //#endregion
12201
- //#region src/billion-mount.ts
12202
- /** The package name the loader resolves, exactly as our patch row names it. */
12203
- const BILLION_PACKAGE = "billion-context";
12204
- /**
12205
- * Is this package installed and able to run?
12206
- *
12207
- * Any entry whose name matches counts — including one whose fiber has not
12208
- * started yet. When this function still decided whether to mount, a fiber-less
12209
- * entry had to be treated as absent: treating an import-in-flight as present
12210
- * would have skipped the mount and left the compressor never starting. Now that
12211
- * nothing mounts, the distinction only picks a label, and the honest one is
12212
- * "installed": a user watching billion-context's own boot does not want the
12213
- * card telling them it is missing while its proxy is half-way up.
12214
- *
12215
- * A DISABLED entry does not count. `loader.entries()` yields disabled rows too
12216
- * — the host skips starting them, it does not drop them — and our own patch
12217
- * row is disabled on purpose. Counting it would make every profile that never
12218
- * installed billion-context read as "installed", which is precisely the lie the
12219
- * card must not tell.
12220
- */
12221
- function mountedInLoader(loader, packageName) {
12222
- for (const entry of loader.entries()) {
12223
- if (entry.options?.name !== packageName) continue;
12224
- if (entry.options?.disabled === true) continue;
12225
- return true;
12226
- }
12227
- return false;
12228
- }
12229
- /**
12230
- * Report whether the compressor is already running. Never mounts.
12231
- *
12232
- * Resolves rather than rejects on every path: a compressor that will not start
12233
- * is a card that says so, never a pool that fails to boot. The pool's own model
12234
- * registry has nothing to do with compression, and one missing optional feature
12235
- * must not take the accounts down with it.
12236
- */
12237
- async function mountBillionContext(ctx, logger) {
12238
- try {
12239
- let loader;
12240
- ctx.inject(["loader"], (scoped) => {
12241
- loader = scoped.loader;
12242
- });
12243
- if (loader === void 0) return {
12244
- state: "unavailable",
12245
- reason: "the host exposes no plugin loader"
12246
- };
12247
- if (mountedInLoader(loader, BILLION_PACKAGE)) {
12248
- logger.info?.(`[xdpool] billion-context is already mounted; leaving it alone`);
12249
- return { state: "already-mounted" };
12250
- }
12251
- return { state: "not-required" };
12252
- } catch (error) {
12253
- return {
12254
- state: "unavailable",
12255
- reason: error instanceof Error ? error.message : String(error)
12256
- };
12257
- }
12258
- }
12259
- //#endregion
12260
12254
  //#region src/pack-accounts.ts
12261
12255
  /**
12262
12256
  * Channel ids whose accounts this plugin serves itself.
@@ -13726,6 +13720,33 @@ function withSessionQuotaHint(error, retryAfterMs) {
13726
13720
  return new AggregateError(error.kind, `${error.message} — this quota is counted per conversation on the free tier, so a new conversation (or another model) starts with its own budget`, error.status);
13727
13721
  }
13728
13722
  /**
13723
+ * Say what a 403 on this lane actually means (#96).
13724
+ *
13725
+ * The gateway answers a client it does not recognise with `403 FreeTierError`
13726
+ * and no explanation of which of its four fingerprint facts was wrong. That
13727
+ * reaches the caller as a bare `client` failure, and a bare 4xx is what the
13728
+ * host then renders in its own words: the user in #96 saw "当前请求的额度已用尽"
13729
+ * for a request that had never been billed. The wording pointed at waiting,
13730
+ * while the actual cause was "this build's fingerprint no longer matches, update
13731
+ * the plugin" — the two need opposite responses.
13732
+ *
13733
+ * The lane cannot tell them apart from the outside (both look like a refusal
13734
+ * with a thin body), so this does not pretend to. It names the actionable
13735
+ * reading FIRST — the fingerprint, which the user can fix by updating — and
13736
+ * keeps the quota possibility second, because it is the one that fixes itself.
13737
+ *
13738
+ * A 429 keeps its own hint ({@link withSessionQuotaHint}): that one really is
13739
+ * quota, and blurring the two would lose a distinction worth keeping.
13740
+ */
13741
+ function withFreeTierHint(status, refusal, retryAfterMs) {
13742
+ const base = classifyUpstream(status, refusal, retryAfterMs, "");
13743
+ if (status !== 403) return base;
13744
+ const separator = base.message.indexOf(": ");
13745
+ const detail = separator === -1 ? "" : base.message.slice(separator + 2);
13746
+ const kept = detail === "" ? "" : ` (provider said: ${detail})`;
13747
+ return new AggregateError(base.kind, `the free lane refused this client: its request fingerprint no longer matches what opencode.ai accepts, so this is NOT an exhausted quota and waiting will not help — update dsh-xdpool and try again; if it persists, report this message. The shared free quota is also metered per conversation, so one retry in a fresh conversation covers the case where only that budget is spent${kept}`, status);
13748
+ }
13749
+ /**
13729
13750
  * POST one streaming turn and hand back chat-shaped deltas.
13730
13751
  *
13731
13752
  * The shape of the ANSWER decides how it is read, never its Content-Type: a front
@@ -13769,7 +13790,7 @@ async function postFreeStreamed({ modelId, body, session, requestId, hooks, plan
13769
13790
  const retryAfter = retryAfterMs(response);
13770
13791
  if (!response.ok) {
13771
13792
  const refusal = await response.text().catch(() => "");
13772
- throw withSessionQuotaHint(classifyUpstream(response.status, refusal, retryAfter, ""), retryAfter);
13793
+ throw withSessionQuotaHint(withFreeTierHint(response.status, refusal, retryAfter), retryAfter);
13773
13794
  }
13774
13795
  if (response.body === null) throw new AggregateError("server", "the free lane returned no body", response.status);
13775
13796
  let events;
@@ -13802,7 +13823,7 @@ async function postFreeStreamed({ modelId, body, session, requestId, hooks, plan
13802
13823
  } catch {
13803
13824
  continue;
13804
13825
  }
13805
- if (isRecord$1(decoded) && (decoded.error !== void 0 || decoded.type === "error")) throw withSessionQuotaHint(classifyUpstream(response.status, event, retryAfter, ""), retryAfter);
13826
+ if (isRecord$1(decoded) && (decoded.error !== void 0 || decoded.type === "error")) throw withSessionQuotaHint(withFreeTierHint(response.status, event, retryAfter), retryAfter);
13806
13827
  const translated = chatChunksFor(wire, decoded, tools);
13807
13828
  for (const delta of translated.frames) hooks.onDelta(delta);
13808
13829
  if (translated.usage !== void 0) usage = translated.usage;
@@ -15019,6 +15040,71 @@ function appearanceChain(themeKey = APPEARANCE_THEME_KEY) {
15019
15040
  };
15020
15041
  }
15021
15042
  /**
15043
+ * Pull the `expertId` out of a task's `jump_url`.
15044
+ *
15045
+ * The "体验XX专家" tasks (#98) name an expert that is NOT in the marketplace
15046
+ * listing — the reporter翻遍了 marketExpertList in both `agent` and `team`
15047
+ * types and got zero hits — so the only place the id appears is the deeplink
15048
+ * embedded in the task itself:
15049
+ *
15050
+ * https://www.workbuddy.cn/work/launch/?deeplink=workbuddy%3A%2F%2Fexperts%3FexpertId%3Dex_GDl31WahyIH0&buddy_type=workbuddy
15051
+ *
15052
+ * The deeplink is percent-encoded, and it has to be: the `?` and `&` inside it
15053
+ * would otherwise end the outer query. So the parse is two steps — decode the
15054
+ * `deeplink` parameter, then read `expertId` off the scheme it decodes to — and
15055
+ * NOT a regex over the whole URL, which would also match the outer `expertId`
15056
+ * spelling in a future URL shape and silently pick the wrong one.
15057
+ *
15058
+ * Returns undefined for the tasks whose deeplink carries no id at all
15059
+ * (`WorkBuddy_Valorant` points at `workbuddy://expert` with no query), which is
15060
+ * the honest answer: those need a different source and are not guessed here.
15061
+ */
15062
+ function expertIdFromJumpUrl(jumpUrl) {
15063
+ if (jumpUrl === "") return void 0;
15064
+ /** Read `expertId` off any URL-ish string, decoded or not. */
15065
+ const readId = (value) => {
15066
+ let query = value;
15067
+ try {
15068
+ const parsed = new URL(value);
15069
+ query = parsed.search === "" ? value : parsed.search;
15070
+ } catch {
15071
+ query = value;
15072
+ }
15073
+ const marker = query.indexOf("expertId=");
15074
+ if (marker === -1) return void 0;
15075
+ const rest = query.slice(marker + 9);
15076
+ const end = rest.search(/[&#]/u);
15077
+ const id = (end === -1 ? rest : rest.slice(0, end)).trim();
15078
+ return id === "" ? void 0 : id;
15079
+ };
15080
+ let source = jumpUrl;
15081
+ try {
15082
+ const raw = new URL(jumpUrl).searchParams.get("deeplink");
15083
+ if (raw !== null && raw !== "") source = raw;
15084
+ } catch {
15085
+ source = jumpUrl;
15086
+ }
15087
+ return readId(source);
15088
+ }
15089
+ /**
15090
+ * Build a summon record for an expert the marketplace does not list.
15091
+ *
15092
+ * Everything the chains need beyond the id is cosmetic — the scorer keys on
15093
+ * `expertId` — so the title from the task is reused as the display name rather
15094
+ * than inventing one. `version` is left empty, which every consumer already
15095
+ * defaults to `1.0.0`.
15096
+ */
15097
+ function expertFromTask(expertId, title) {
15098
+ return {
15099
+ expertId,
15100
+ expertType: "agent",
15101
+ displayName: title,
15102
+ profession: title,
15103
+ version: "",
15104
+ categories: []
15105
+ };
15106
+ }
15107
+ /**
15022
15108
  * The three "summon an expert" events (`expert_summon_click` and friends).
15023
15109
  *
15024
15110
  * Paid before the conversation, in the order the app emits them.
@@ -15141,7 +15227,12 @@ const EVENT_CHAIN_BUILDERS = {
15141
15227
  skill_1: true,
15142
15228
  expert_5: true,
15143
15229
  Expert_team_use_3: true,
15144
- Expert_lighthouse: true
15230
+ Expert_lighthouse: true,
15231
+ expert_use_maidehao: true,
15232
+ expert_use_douyinyin: true,
15233
+ expert_use_shangjia_xuanchuan: true,
15234
+ expert_use_bowangwang: true,
15235
+ WorkBuddy_Valorant: true
15145
15236
  };
15146
15237
  const AUTOMATION_TICK_MS = 6e4;
15147
15238
  /** The jobs in the order a tick runs them: report before tasks, always. */
@@ -15349,6 +15440,16 @@ var WorkBuddyScheduler = class {
15349
15440
  /** Host hooks that persist the ledger across restarts. */
15350
15441
  loadEarnings;
15351
15442
  saveEarningsFn;
15443
+ /**
15444
+ * The most recent ledger write that may still be in flight.
15445
+ *
15446
+ * `persistEarnings` is synchronous from its callers' point of view, but the
15447
+ * host's settings write is not: it is an async document update. Keeping the
15448
+ * promise here is what lets {@link flushEarnings} wait for the write that
15449
+ * matters — the last one before a restart — without making every counter
15450
+ * update await disk.
15451
+ */
15452
+ pendingEarningsWrite = Promise.resolve();
15352
15453
  constructor(pool, client, options = {}) {
15353
15454
  this.pool = pool;
15354
15455
  this.client = client;
@@ -15385,6 +15486,18 @@ var WorkBuddyScheduler = class {
15385
15486
  this.saveEarningsFn = save;
15386
15487
  }
15387
15488
  /**
15489
+ * Wait for the ledger write that is (or was last) in flight.
15490
+ *
15491
+ * Called at the END of a task pass, right after the last claim, so the rewards
15492
+ * that were just collected are on disk before a restart can lose them. It is
15493
+ * deliberately NOT awaited on every counter bump: the settings document write
15494
+ * is the slow part, and paying it per account would slow the pass for no
15495
+ * benefit while the process is still alive.
15496
+ */
15497
+ async flushEarnings() {
15498
+ await this.pendingEarningsWrite;
15499
+ }
15500
+ /**
15388
15501
  * Fold a previously persisted ledger back in, when it belongs to today.
15389
15502
  *
15390
15503
  * Used after the settings document becomes readable, which happens after
@@ -15654,7 +15767,7 @@ var WorkBuddyScheduler = class {
15654
15767
  compensationCredit: (existing?.compensationCredit ?? 0) + compensationCredit,
15655
15768
  date: today
15656
15769
  });
15657
- this.persistEarnings();
15770
+ this.pendingEarningsWrite = this.persistEarnings();
15658
15771
  }
15659
15772
  /** Clear the per-account counters when the local day changes. */
15660
15773
  rollEarnings(today) {
@@ -15670,9 +15783,18 @@ var WorkBuddyScheduler = class {
15670
15783
  * Best effort on purpose: a failed save must never abort a run that has
15671
15784
  * already collected rewards, and the in-memory ledger keeps serving the card
15672
15785
  * for the rest of the session either way.
15786
+ *
15787
+ * The returned promise settles when the write has landed (or failed). Callers
15788
+ * that are about to END a run await it: reported in #98, a task pass that
15789
+ * claimed 21 tasks had its process restarted before the volatile settings
15790
+ * write reached disk, so `automationEarnings` still read `credit: 0` while
15791
+ * the upstream had really paid out. Fire-and-forget is fine for a counter the
15792
+ * card will re-read in this process; it is not fine for the last write before
15793
+ * exit.
15673
15794
  */
15674
15795
  persistEarnings() {
15675
- if (this.saveEarningsFn === void 0) return;
15796
+ const save = this.saveEarningsFn;
15797
+ if (save === void 0) return Promise.resolve();
15676
15798
  const accounts = {};
15677
15799
  for (const [id, entry] of this.earnings) accounts[id] = entry;
15678
15800
  const ledger = {
@@ -15680,12 +15802,14 @@ var WorkBuddyScheduler = class {
15680
15802
  accounts
15681
15803
  };
15682
15804
  try {
15683
- const saved = this.saveEarningsFn(ledger);
15684
- if (saved !== void 0 && typeof saved.then === "function") saved.then(void 0, (error) => {
15805
+ const saved = save(ledger);
15806
+ if (saved !== void 0 && typeof saved.then === "function") return Promise.resolve(saved).then(void 0, (error) => {
15685
15807
  this.logger.warn?.("dsh-xdpool: could not persist automation earnings:", error);
15686
15808
  });
15809
+ return Promise.resolve();
15687
15810
  } catch (error) {
15688
15811
  this.logger.warn?.("dsh-xdpool: could not persist automation earnings:", error);
15812
+ return Promise.resolve();
15689
15813
  }
15690
15814
  }
15691
15815
  /**
@@ -15794,6 +15918,7 @@ var WorkBuddyScheduler = class {
15794
15918
  state.detail = detail;
15795
15919
  state.progress = progressNote;
15796
15920
  this.logger.info?.(`automation ${accountWord}: ${state.message}`);
15921
+ if (kind === "tasks" || kind === "checkin" || kind === "streak" || kind === "travel" || kind === "compensation") await this.flushEarnings();
15797
15922
  }
15798
15923
  /** Compose the one-line summary shown on the card. */
15799
15924
  summarise(kind, ok, failed, claimed, credit, energy) {
@@ -15916,10 +16041,43 @@ var WorkBuddyScheduler = class {
15916
16041
  case "expert_5": return this.expertChains(credential, "agent", task);
15917
16042
  case "Expert_team_use_3": return this.expertChains(credential, "team", task);
15918
16043
  case "Expert_lighthouse": return this.lighthouseChains(credential);
16044
+ case "expert_use_maidehao":
16045
+ case "expert_use_douyinyin":
16046
+ case "expert_use_shangjia_xuanchuan":
16047
+ case "expert_use_bowangwang":
16048
+ case "WorkBuddy_Valorant": return this.taskExpertChains(credential, task);
15919
16049
  default: return [];
15920
16050
  }
15921
16051
  }
15922
16052
  /**
16053
+ * Summon-and-use one expert NAMED BY THE TASK.
16054
+ *
16055
+ * Structurally `expertChains` minus the marketplace: the expert id is read
16056
+ * from the task's deeplink instead of by browsing `marketExpertList`, because
16057
+ * this family's experts are not listed there at all (#98 measured 500 entries
16058
+ * across both `agent` and `team`, zero hits).
16059
+ *
16060
+ * Returns `[]` — not a throw — when the task carries no usable id: the caller
16061
+ * logs "could not be built" only for exceptions, and a task shape this code
16062
+ * does not understand is information, not a fault.
16063
+ */
16064
+ async taskExpertChains(credential, task) {
16065
+ const expertId = expertIdFromJumpUrl(task.jumpUrl);
16066
+ if (expertId === void 0) {
16067
+ this.logger.warn?.(`automation events ${task.taskCode}: no expertId in jump_url, skipping`);
16068
+ return [];
16069
+ }
16070
+ const expert = expertFromTask(expertId, task.title);
16071
+ await this.client.reportDesktopEvents(credential, expertSummonEvents(expert));
16072
+ if (this.delayMs > 0 && !this.stopped) await sleep$1(this.delayMs);
16073
+ const chat = await this.client.openConversation(credential, expert.expertId);
16074
+ if (chat === void 0) throw new Error(`${task.taskCode}: no server conversation`);
16075
+ return [{
16076
+ transport: "desktop",
16077
+ events: [...expertChatEvents(expert, chat.conversationId, chat.requestId), expertActualUseEvent(expert, chat.conversationId, chat.requestId)]
16078
+ }];
16079
+ }
16080
+ /**
15923
16081
  * The summon-and-use chains for the expert tasks.
15924
16082
  *
15925
16083
  * Two steps per expert, and both are load-bearing: the summon events alone are
@@ -17259,33 +17417,6 @@ function registerPoolStatusRoute(ctx, deps) {
17259
17417
  }
17260
17418
  });
17261
17419
  /**
17262
- * The billion-context relay.
17263
- *
17264
- * GET only, loopback origin only. Its own route because it is the one
17265
- * document here that comes from ANOTHER plugin's loopback listener: the
17266
- * proxy publishes its own JSON and this only reads it, so folding it into
17267
- * the pool status document would put a third-party service on the critical
17268
- * path of every 30-second pool poll.
17269
- *
17270
- * Answered even when the proxy is absent — `state: 'missing'` with the
17271
- * install command is far more actionable than a 503, and it is the normal
17272
- * state on a machine that has not installed billion-context.
17273
- */
17274
- const disposeBillion = ctx.webServer.register({
17275
- kind: "exact",
17276
- path: POOL_BILLION_PATH,
17277
- handler: async (req, res) => {
17278
- if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
17279
- if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
17280
- if (deps.billionDocument === void 0) return json(res, 503, { error: "the billion-context relay is not available in this build" });
17281
- try {
17282
- json(res, 200, await deps.billionDocument());
17283
- } catch (error) {
17284
- json(res, 500, { error: safeMessage(error) });
17285
- }
17286
- }
17287
- });
17288
- /**
17289
17420
  * The chain list plus the roster its members are drawn from.
17290
17421
  *
17291
17422
  * GET only. Deliberately not part of the pool status document: chains change
@@ -17361,7 +17492,6 @@ function registerPoolStatusRoute(ctx, deps) {
17361
17492
  disposeRelay();
17362
17493
  disposeCombosSave();
17363
17494
  disposeCombos();
17364
- disposeBillion();
17365
17495
  disposeChannels();
17366
17496
  disposeAggregate();
17367
17497
  disposeAggregateDiscover();
@@ -17385,247 +17515,6 @@ function registerPoolStatusRoute(ctx, deps) {
17385
17515
  }, "dsh-xdpool: Web status route");
17386
17516
  }
17387
17517
  //#endregion
17388
- //#region src/billion.ts
17389
- /**
17390
- * Read-only observation of a locally running billion-context proxy.
17391
- *
17392
- * WHAT THIS IS, AND WHY IT IS NOT AN EMBEDDED COMPRESSOR
17393
- *
17394
- * billion-context is a separate DSH plugin that is very likely already
17395
- * installed on this machine — on a stock desktop profile it is mounted by the
17396
- * profile's bundle layer and runs its own loopback listener. This module never
17397
- * implements compression and never proposes to: it finds that listener and
17398
- * relays a few numbers so the card can show what the compressor is doing.
17399
- * Re-implementing folding here would mean a second, divergent answer to the
17400
- * same question and a second set of ways to get it wrong.
17401
- *
17402
- * HOW THE LISTENER IS FOUND
17403
- *
17404
- * The proxy picks an ephemeral port at spawn and records it in its own state
17405
- * directory, which we read rather than guess. Guessing would mean scanning a
17406
- * port range on every card poll, and a wrong guess is worse than no answer: it
17407
- * could read some unrelated loopback service. The recorded file is
17408
- * `<state>/billion-context/proxy-origin` and holds a JSON object with at least
17409
- * an `origin`; `port-zone.json` is the same fact keyed by lane. Both are read,
17410
- * and the origin file wins, because it is the one the proxy itself reads on
17411
- * startup.
17412
- *
17413
- * `state` is resolved the same way the proxy resolves it: `XDG_STATE_HOME` when
17414
- * set, else `<homedir>/.local/state` on every platform including Windows — not
17415
- * `%LOCALAPPDATA%`, which is what a Windows developer would reach for first and
17416
- * which would therefore be wrong.
17417
- *
17418
- * WHAT IS RELAYED
17419
- *
17420
- * Counts, sizes, timestamps. Not titles, not first-block hints, not any
17421
- * conversation material. The proxy is happy to hand over more than that, and
17422
- * taking it would put the user's own words into a document the card fetches
17423
- * every 30 seconds and that lives in the plugin's HTTP log.
17424
- *
17425
- * The one addition to that list is the proxy's own origin, so the card can link
17426
- * out to the dashboard the proxy already ships instead of redrawing it. That is
17427
- * a service address rather than conversation material, and anyone who can read
17428
- * this document can already reach that listener directly — which is why it is
17429
- * safe to publish where a title would not be.
17430
- *
17431
- * The proxy also has NO AUTHENTICATION by its own documentation — the README
17432
- * says so outright — so every request here is a hard local one, and the relay
17433
- * only ever reads `/__bili/health` and `/__bili/sessions`. Both are GETs. This
17434
- * module has no code path that could write to that proxy.
17435
- *
17436
- * @module dsh-xdpool/billion
17437
- */
17438
- /** The proxy's own management prefix. Never guess it; it is theirs. */
17439
- const BILI_PREFIX = "/__bili";
17440
- /** Loopback only. The proxy binds 127.0.0.1 and we refuse anything else. */
17441
- const LOOPBACK_HOSTS$1 = [
17442
- "127.0.0.1",
17443
- "localhost",
17444
- "[::1]",
17445
- "::1"
17446
- ];
17447
- /**
17448
- * How long one relay request may take.
17449
- *
17450
- * Short on purpose: this runs inside the card's status request, and a stalled
17451
- * third-party proxy must degrade to a panel that says "no answer" rather than
17452
- * hold the pool card hostage. The proxy answers these two endpoints in
17453
- * milliseconds; the budget is for the case where it does not.
17454
- */
17455
- const RELAY_TIMEOUT_MS = 1500;
17456
- /** How many sessions the card is shown. 50 rows is already past the fold. */
17457
- const MAX_SESSIONS = 50;
17458
- /**
17459
- * Where the proxy keeps its state directory.
17460
- *
17461
- * Mirrors the proxy's own resolution exactly: `XDG_STATE_HOME` when it holds an
17462
- * absolute path, else `<homedir>/.local/state`. Deliberately NOT
17463
- * `DSH_HOME ?? homedir()` — DSH_HOME is the profile root and a live value on a
17464
- * real desktop install, so preferring it sent this reader to
17465
- * `<profile>/.local/state/billion-context` and reported "missing" on a machine
17466
- * where the proxy was running and answering. Probed against the live proxy,
17467
- * which is the only way that mistake is visible: every unit test with a faked
17468
- * state directory passes either way.
17469
- *
17470
- * Undefined when neither resolves, because inventing a path to probe would
17471
- * turn "not installed" into a silent hunt through the filesystem.
17472
- */
17473
- function stateRoot() {
17474
- const xdg = env.XDG_STATE_HOME;
17475
- if (typeof xdg === "string" && xdg !== "") {
17476
- if (xdg.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(xdg)) return join(xdg, "billion-context");
17477
- return;
17478
- }
17479
- const home = homedir();
17480
- if (typeof home !== "string" || home === "") return void 0;
17481
- return join(home, ".local", "state", "billion-context");
17482
- }
17483
- /** A loopback http(s) origin, or undefined for anything else. */
17484
- function parseLoopbackOrigin(raw) {
17485
- let url;
17486
- try {
17487
- url = new URL(raw);
17488
- } catch {
17489
- return;
17490
- }
17491
- if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
17492
- if (!LOOPBACK_HOSTS$1.includes(url.hostname.toLowerCase())) return void 0;
17493
- return url.origin;
17494
- }
17495
- /**
17496
- * The proxy origin, from the file the proxy itself reads on startup.
17497
- *
17498
- * `port-zone.json` is the fallback because it is keyed by lane and therefore
17499
- * distinguishes the dsh proxy from, say, a concurrently running opencode one —
17500
- * but `proxy-origin` is preferred because it is the single-writer record.
17501
- */
17502
- async function readProxyOrigin() {
17503
- const root = stateRoot();
17504
- if (root === void 0) return void 0;
17505
- for (const name of ["proxy-origin", "port-zone.json"]) {
17506
- let raw;
17507
- try {
17508
- raw = await readFile(join(root, name), "utf8");
17509
- } catch {
17510
- continue;
17511
- }
17512
- if (name === "proxy-origin") {
17513
- try {
17514
- const parsed = JSON.parse(raw);
17515
- const origin = typeof parsed.origin === "string" ? parseLoopbackOrigin(parsed.origin) : void 0;
17516
- if (origin !== void 0) return origin;
17517
- } catch {}
17518
- continue;
17519
- }
17520
- try {
17521
- const port = JSON.parse(raw).lanes?.dsh;
17522
- if (typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536) return `http://127.0.0.1:${port}`;
17523
- } catch {
17524
- continue;
17525
- }
17526
- }
17527
- }
17528
- /** One GET against the proxy, with the relay budget on it. Never throws. */
17529
- async function fetchJson(origin, path) {
17530
- const controller = new AbortController();
17531
- const timer = setTimeout(() => {
17532
- controller.abort();
17533
- }, RELAY_TIMEOUT_MS);
17534
- try {
17535
- const response = await fetch(new URL(BILI_PREFIX + path, origin), {
17536
- method: "GET",
17537
- headers: { accept: "application/json" },
17538
- signal: controller.signal
17539
- });
17540
- if (!response.ok) return void 0;
17541
- return await response.json();
17542
- } catch {
17543
- return;
17544
- } finally {
17545
- clearTimeout(timer);
17546
- }
17547
- }
17548
- /** A finite number, or undefined. Guards every relayed field. */
17549
- function num(value) {
17550
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
17551
- }
17552
- /** A string, or ''. */
17553
- function str(value) {
17554
- return typeof value === "string" ? value : "";
17555
- }
17556
- /** A boolean, or false. */
17557
- function bool(value) {
17558
- return value === true;
17559
- }
17560
- /**
17561
- * One session row, reduced.
17562
- *
17563
- * Every field is defaulted rather than passed through: the proxy's document is
17564
- * a moving target across versions, and an unreadable row should render as
17565
- * zeroes we can explain, not as `undefined` reaching the card.
17566
- */
17567
- function toSession(raw) {
17568
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
17569
- const row = raw;
17570
- const id = str(row.id);
17571
- if (id === "") return void 0;
17572
- return {
17573
- id,
17574
- protocol: str(row.protocol),
17575
- live: bool(row.live),
17576
- requests: num(row.requests) ?? 0,
17577
- contextTokens: num(row.contextTokens) ?? 0,
17578
- contextTokensSource: str(row.contextTokensSource),
17579
- tokensSaved: num(row.tokensSaved) ?? 0,
17580
- inputTokens: num(row.inputTokens) ?? 0,
17581
- cachedTokens: num(row.cachedTokens) ?? 0,
17582
- outputTokens: num(row.outputTokens) ?? 0,
17583
- cacheHitPct: num(row.cacheHitPct) ?? -1,
17584
- blocks: num(row.blocks) ?? 0,
17585
- contextWindow: num(row.contextWindow) ?? 0,
17586
- lastSeen: str(row.lastSeen),
17587
- lastModel: str(row.lastModel)
17588
- };
17589
- }
17590
- /** The whole relay, safe to hand the card: it never rejects. */
17591
- async function readBillionStatus() {
17592
- const origin = await readProxyOrigin();
17593
- if (origin === void 0) return {
17594
- state: "missing",
17595
- reason: "billion-context is not running on this machine. Install it with \"dsh plugin --profile <name> add billion-context\" and this page fills in.",
17596
- sessions: [],
17597
- totalSessions: 0
17598
- };
17599
- const health = await fetchJson(origin, "/health");
17600
- if (health === null || typeof health !== "object") return {
17601
- state: "unreachable",
17602
- reason: `billion-context is recorded at ${origin} but did not answer. It may have just been restarted.`,
17603
- origin,
17604
- sessions: [],
17605
- totalSessions: 0
17606
- };
17607
- const meta = health;
17608
- const sessionsDoc = await fetchJson(origin, "/sessions");
17609
- const rawSessions = sessionsDoc !== null && typeof sessionsDoc === "object" ? sessionsDoc.sessions : void 0;
17610
- const rows = Array.isArray(rawSessions) ? rawSessions.slice(0, MAX_SESSIONS) : [];
17611
- const sessions = [];
17612
- for (const row of rows) {
17613
- const session = toSession(row);
17614
- if (session !== void 0) sessions.push(session);
17615
- }
17616
- const total = num(sessionsDoc !== null && typeof sessionsDoc === "object" ? sessionsDoc.total : void 0) ?? sessions.length;
17617
- const version = str(meta.version);
17618
- const startedAt = num(meta.startedAt);
17619
- return {
17620
- state: "ready",
17621
- origin,
17622
- ...version === "" ? {} : { version },
17623
- ...startedAt === void 0 ? {} : { startedAt },
17624
- sessions,
17625
- totalSessions: total
17626
- };
17627
- }
17628
- //#endregion
17629
17518
  //#region src/diagnostics.ts
17630
17519
  /**
17631
17520
  * A startup trace this plugin owns.
@@ -18631,6 +18520,34 @@ function createWorkBuddyShim(options) {
18631
18520
  const server = createServer((req, res) => {
18632
18521
  handle(req, res);
18633
18522
  });
18523
+ async function acquireForRequest(modelId, conversationKey, signal) {
18524
+ const ordinary = await pool.acquire(modelId, region, conversationKey);
18525
+ const reserveExempt = /* @__PURE__ */ new Set();
18526
+ if (ordinary !== void 0 || modelId === void 0 || signal.aborted) return {
18527
+ account: ordinary,
18528
+ reserveExempt
18529
+ };
18530
+ const now = Date.now();
18531
+ const held = pool.list(region).filter((account) => pool.isReserved(account.id) && !pool.isDisabled(account.id) && (account.credentialDeadUntilMs ?? 0) <= now && account.cooldownUntilMs <= now && (account.modelCooldowns[modelId] ?? 0) <= now);
18532
+ const pricingSignal = AbortSignal.any([signal, AbortSignal.timeout(5e3)]);
18533
+ await Promise.all(held.map(async (account) => {
18534
+ try {
18535
+ const price = (await client.fetchModels(account.credential, pricingSignal)).find((model) => model.id === modelId)?.creditMultiplier;
18536
+ logger?.info?.(`dsh-xdpool: reserve pricing model=${modelId} region=${region ?? "any"} price=${price ?? "unknown"} exempt=${!pricingSignal.aborted && price === 0}`);
18537
+ if (!pricingSignal.aborted && price === 0) reserveExempt.add(account.id);
18538
+ } catch {
18539
+ logger?.info?.(`dsh-xdpool: reserve pricing unavailable model=${modelId} region=${region ?? "any"} aborted=${pricingSignal.aborted}`);
18540
+ }
18541
+ }));
18542
+ if (signal.aborted) return {
18543
+ account: void 0,
18544
+ reserveExempt
18545
+ };
18546
+ return {
18547
+ account: await pool.acquire(modelId, region, conversationKey, reserveExempt),
18548
+ reserveExempt
18549
+ };
18550
+ }
18634
18551
  /**
18635
18552
  * "The same model has a paid row" sentence for a 429 body, or undefined.
18636
18553
  *
@@ -19131,9 +19048,9 @@ function createWorkBuddyShim(options) {
19131
19048
  if (signal.aborted) return;
19132
19049
  attempts.push(member.modelId);
19133
19050
  if (!member.aggregate) {
19134
- const account = await pool.acquire(member.modelId, region);
19051
+ const { account, reserveExempt } = await acquireForRequest(member.modelId, conversationKey, signal);
19135
19052
  if (account === void 0) {
19136
- const why = pool.unavailableReason(member.modelId, region);
19053
+ const why = pool.unavailableReason(member.modelId, region, reserveExempt);
19137
19054
  last = {
19138
19055
  kind: why.reason,
19139
19056
  status: why.reason === "cooling" ? 429 : 401,
@@ -19141,7 +19058,11 @@ function createWorkBuddyShim(options) {
19141
19058
  };
19142
19059
  continue;
19143
19060
  }
19144
- const prepared = client.prepareChatBodyFor(raw, region ?? "cn");
19061
+ const memberRaw = JSON.stringify({
19062
+ ...JSON.parse(raw),
19063
+ model: member.modelId
19064
+ });
19065
+ const prepared = client.prepareChatBodyFor(memberRaw, region ?? "cn");
19145
19066
  const result = await client.chatStream(account.credential, prepared, signal, conversationKey);
19146
19067
  if (result.ok) {
19147
19068
  const turnId = randomBytes(8).toString("hex");
@@ -19305,9 +19226,9 @@ function createWorkBuddyShim(options) {
19305
19226
  };
19306
19227
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
19307
19228
  if (controller.signal.aborted) return;
19308
- const account = await pool.acquire(modelId, region, conversationKey);
19229
+ const { account, reserveExempt } = await acquireForRequest(modelId, conversationKey, controller.signal);
19309
19230
  if (account === void 0) {
19310
- const why = pool.unavailableReason(modelId, region);
19231
+ const why = pool.unavailableReason(modelId, region, reserveExempt);
19311
19232
  if (why.reason === "cooling") {
19312
19233
  const subject = modelId === void 0 ? "every WorkBuddy account is rate-limited" : `every account is rate-limited for model ${modelId}`;
19313
19234
  const upstream = last === void 0 ? "" : ` — ${last.message.slice(0, 200)}`;
@@ -19320,7 +19241,7 @@ function createWorkBuddyShim(options) {
19320
19241
  return;
19321
19242
  }
19322
19243
  if (why.reason === "reserve") {
19323
- writeOpenAIError(res, 402, "reserve_floor", `every WorkBuddy account is at or below its credit reserve (${why.total} account(s)); lower the reserve on the pool card or top up credits`);
19244
+ writeOpenAIError(res, 402, "reserve_floor", `no WorkBuddy account is available; every account not excluded by a cooldown or a revoked sign-in is at or below its credit reserve (${why.total} account(s) total); lower the reserve on the pool card or top up credits`);
19324
19245
  return;
19325
19246
  }
19326
19247
  if (why.reason === "session_dead") {
@@ -19374,7 +19295,7 @@ function createWorkBuddyShim(options) {
19374
19295
  }
19375
19296
  if (isContextTooLong(last.message)) {
19376
19297
  const recovered = await recoverFromContextOverrun({
19377
- raw,
19298
+ raw: poolRaw,
19378
19299
  modelId,
19379
19300
  controller,
19380
19301
  region,
@@ -19382,6 +19303,7 @@ function createWorkBuddyShim(options) {
19382
19303
  client,
19383
19304
  pool,
19384
19305
  maxAttempts,
19306
+ acquireAccount: async () => (await acquireForRequest(modelId, conversationKey, controller.signal)).account,
19385
19307
  usage: usageContext,
19386
19308
  contextWindow: modelId === void 0 ? void 0 : catalog.current().find((m) => m.id === modelId)?.contextWindow
19387
19309
  });
@@ -19579,7 +19501,7 @@ async function recoverFromContextOverrun(options) {
19579
19501
  let compacted = messages;
19580
19502
  let compactionDetail = "";
19581
19503
  try {
19582
- const summariser = await pool.acquire(modelId, region);
19504
+ const summariser = await (options.acquireAccount?.() ?? pool.acquire(modelId, region));
19583
19505
  if (summariser === void 0) compactionDetail = "no account available to summarise with";
19584
19506
  else {
19585
19507
  const outcome = await compactWithSummary(messages, {
@@ -19612,7 +19534,7 @@ async function recoverFromContextOverrun(options) {
19612
19534
  ok: false,
19613
19535
  detail: "client disconnected"
19614
19536
  };
19615
- const account = await pool.acquire(modelId, region);
19537
+ const account = await (options.acquireAccount?.() ?? pool.acquire(modelId, region));
19616
19538
  if (account === void 0) return {
19617
19539
  ok: false,
19618
19540
  detail: "no account available after compaction"
@@ -21088,9 +21010,7 @@ function apply(ctx, config = {}) {
21088
21010
  await setSetting("disabledAccountIds", next, next);
21089
21011
  },
21090
21012
  setCreditReserve: async (accountId, reserve) => {
21091
- const next = { ...current().creditReserves ?? {} };
21092
- if (reserve > 0) next[accountId] = reserve;
21093
- else delete next[accountId];
21013
+ const next = creditReservesWith(current().creditReserves, accountId, reserve);
21094
21014
  await setSetting("creditReserves", next, next);
21095
21015
  },
21096
21016
  setAccountIgnored: async (accountId, ignored) => {
@@ -21264,7 +21184,6 @@ function apply(ctx, config = {}) {
21264
21184
  await aggregateCatalog.refresh();
21265
21185
  return combosStatusOf(readAggregateSettingsSync(), aggregateCatalog, core.catalogs.cn.current());
21266
21186
  },
21267
- billionDocument: readBillionStatus,
21268
21187
  /**
21269
21188
  * Relay state plus the gateway's model picker.
21270
21189
  *
@@ -21342,12 +21261,6 @@ function apply(ctx, config = {}) {
21342
21261
  adoptChannelPackAccounts(ctx, core.pool, ctx.logger);
21343
21262
  });
21344
21263
  });
21345
- mountBillionContext(ctx, ctx.logger).then((outcome) => {
21346
- recordStartupNote(`billion-context ${outcome.state}`);
21347
- if (outcome.state === "unavailable") ctx.logger.warn(`[xdpool] billion-context not mounted — ${outcome.reason}`);
21348
- }, (error) => {
21349
- recordStartupNote(`billion-context mount failed: ${describeError(error)}`);
21350
- });
21351
21264
  Promise.all([shims.cn.ready, shims.global.ready]).then(async () => {
21352
21265
  if (stopped) return;
21353
21266
  recordStartupNote(`both shims ready cn=${shims.cn.baseUrl()} global=${shims.global.baseUrl()}; entering registration`);