dsh-xdpool 2.5.0 → 2.6.0

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.d.ts CHANGED
@@ -398,6 +398,15 @@ interface WorkBuddyTask {
398
398
  claimed: boolean;
399
399
  /** Upstream marked the task locked (not yet reachable). */
400
400
  locked: boolean;
401
+ /**
402
+ * The task's own deeplink, verbatim, or '' when it has none.
403
+ *
404
+ * Kept because for the "体验XX专家" family it is the ONLY source of the
405
+ * expert id: those experts are not in the marketplace listing, and the id
406
+ * appears nowhere else (#98). Named after the upstream field so a future
407
+ * reader can find it in a captured JSON body.
408
+ */
409
+ jumpUrl: string;
401
410
  }
402
411
  /**
403
412
  * Flatten one upstream task entry.
@@ -1057,6 +1066,16 @@ export declare class WorkBuddyAccountPool {
1057
1066
  * lives on the pool and is re-applied from settings after each scan.
1058
1067
  */
1059
1068
  private disabledIds;
1069
+ /**
1070
+ * The account ids the store held after the last successful write.
1071
+ *
1072
+ * Read back on the next scan to say WHICH accounts left, not just how many.
1073
+ * The list is written wholesale, so a scan that does not find an account
1074
+ * deletes it — and that account's credential is then unrecoverable without a
1075
+ * fresh sign-in (#117). `undefined` until the first write, so the first scan
1076
+ * never reports every account as "leave".
1077
+ */
1078
+ private lastPersistedIds;
1060
1079
  /**
1061
1080
  * Account ids the user threw out of the pool for good.
1062
1081
  *
@@ -1134,6 +1153,11 @@ export declare class WorkBuddyAccountPool {
1134
1153
  * A function rather than a list because that pool is mounted asynchronously
1135
1154
  * and can be re-read on demand; `undefined` means "no such source", which is
1136
1155
  * the state every test and every pre-mount startup is in.
1156
+ *
1157
+ * The `| undefined` is explicit because `applyExternalSource` CLEARS the field by
1158
+ * assigning `undefined` — under `exactOptionalPropertyTypes` a bare `?:` means
1159
+ * "may be absent, never undefined", which made that documented clearing action
1160
+ * a type error.
1137
1161
  */
1138
1162
  private externalSource?;
1139
1163
  constructor(options?: AccountPoolOptions);
@@ -1205,7 +1229,7 @@ export declare class WorkBuddyAccountPool {
1205
1229
  * Counts are over the region's accounts, since a provider only ever sees its
1206
1230
  * own gateway.
1207
1231
  */
1208
- unavailableReason(modelId?: string, region?: WorkBuddyRegion): {
1232
+ unavailableReason(modelId?: string, region?: WorkBuddyRegion, reserveExempt?: ReadonlySet<string>): {
1209
1233
  total: number;
1210
1234
  cooling: number;
1211
1235
  disabled: number;
@@ -1295,9 +1319,11 @@ export declare class WorkBuddyAccountPool {
1295
1319
  * Scans on first use, and rescans when every known account is cooling down: a
1296
1320
  * fresh desktop login is the usual way out of an exhausted pool.
1297
1321
  *
1322
+ * `reserveExempt` is host-only: account ids whose own credentials freshly
1323
+ * confirmed a zero-credit model. It never bypasses disabled, dead credentials, cooldowns or region.
1298
1324
  * `conversationKey` is only consulted under `sticky`; other modes ignore it.
1299
1325
  */
1300
- acquire(modelId?: string, region?: WorkBuddyRegion, conversationKey?: string): Promise<WorkBuddyAccount | undefined>;
1326
+ acquire(modelId?: string, region?: WorkBuddyRegion, conversationKey?: string, reserveExempt?: ReadonlySet<string>): Promise<WorkBuddyAccount | undefined>;
1301
1327
  /** How the pool currently spreads requests. Shown on the card. */
1302
1328
  currentDistribution(): AccountDistribution;
1303
1329
  /**
@@ -1403,6 +1429,28 @@ export declare class WorkBuddyAccountPool {
1403
1429
  clearCredentialDead(accountId: string): void;
1404
1430
  /** Accounts currently kept out of rotation because their sign-in was rejected. */
1405
1431
  deadCredentials(): readonly WorkBuddyAccount[];
1432
+ /**
1433
+ * Try to bring benched accounts back WITHOUT waiting out the cooldown.
1434
+ *
1435
+ * The cooldown is a guess about how long a problem lasts, and the guess is
1436
+ * usually wrong in one direction or the other: a token revoked by a password
1437
+ * change stays dead for the full window even after the user signs in again in
1438
+ * some other client, and a transient upstream hiccup benches a perfectly good
1439
+ * account for half an hour.
1440
+ *
1441
+ * So instead of only waiting, ask. For each benched account, one cheap
1442
+ * authenticated read: if it answers, the credential works and the account goes
1443
+ * back into rotation immediately. Nothing about the failure is trusted — only a
1444
+ * successful call clears it — and a probe that fails keeps the original
1445
+ * cooldown untouched (probing must never EXTEND a bench, or a broken endpoint
1446
+ * would keep every account locked out for as long as it stayed broken).
1447
+ *
1448
+ * `probe` is injected rather than imported so this class stays free of the
1449
+ * upstream client: the pool knows about accounts, not about HTTP.
1450
+ *
1451
+ * Returns the accounts it revived, for logging and for the run summary.
1452
+ */
1453
+ reviveBenched(probe: (credential: WorkBuddyCredential) => Promise<unknown>): Promise<readonly WorkBuddyAccount[]>;
1406
1454
  /**
1407
1455
  * Refresh the account's access token when it is within the margin (or already
1408
1456
  * expired), in-flight de-duped per account. A failed refresh keeps the
@@ -1498,77 +1546,6 @@ interface PoolWebChannelModel {
1498
1546
  inputModalities?: readonly string[];
1499
1547
  description?: string;
1500
1548
  }
1501
- /**
1502
- * One billion-context session, reduced to what a dashboard may show.
1503
- *
1504
- * Counts, sizes and timestamps only. The proxy's own rows carry titles and
1505
- * first-block hints, which are the user's words; those stay on the machine that
1506
- * produced them and are not relayed.
1507
- */
1508
- interface PoolWebBillionSession {
1509
- /** The proxy's session id. Safe to show: it is a generated handle. */
1510
- id: string;
1511
- /** Which wire the session speaks; `openai` is what DSH's shim uses. */
1512
- protocol: string;
1513
- /** Whether a request hit this session in the last few seconds. */
1514
- live: boolean;
1515
- requests: number;
1516
- /** The compacted context size the proxy is holding, 0 when unknown. */
1517
- contextTokens: number;
1518
- /** Which measurement the context size came from; `usage` is the honest one. */
1519
- contextTokensSource: string;
1520
- /** Net tokens the proxy says compression saved. */
1521
- tokensSaved: number;
1522
- inputTokens: number;
1523
- cachedTokens: number;
1524
- outputTokens: number;
1525
- /** Cache hit rate 0-100, or -1 when no input tokens were ever counted. */
1526
- cacheHitPct: number;
1527
- /** How many compressed blocks this session holds. */
1528
- blocks: number;
1529
- /** The window the proxy believes the model has, 0 when unknown. */
1530
- contextWindow: number;
1531
- /** ISO timestamp of the last request, or '' when the session never had one. */
1532
- lastSeen: string;
1533
- /** The last model the session saw, or '' before the first request. */
1534
- lastModel: string;
1535
- }
1536
- /** The document {@link POOL_BILLION_PATH} answers with. */
1537
- interface PoolWebBillionStatus {
1538
- /**
1539
- * Whether a proxy was found and answered.
1540
- *
1541
- * `missing` is a normal, expected state, not an error: billion-context is a
1542
- * separate plugin, and a machine without it must get a page that explains
1543
- * that rather than a 503 or a blank panel.
1544
- */
1545
- state: 'ready' | 'missing' | 'unreachable';
1546
- /** Human text for a non-ready state; absent when ready. */
1547
- reason?: string;
1548
- /**
1549
- * The proxy's own loopback origin, absent only when none was recorded.
1550
- *
1551
- * The card needs this to hand the user a browser link: billion-context ships
1552
- * its own dashboard, including a per-session detail view, and duplicating
1553
- * those screens here would be a second and divergent copy of another
1554
- * plugin's interface. Linking out keeps one answer.
1555
- *
1556
- * This is a service address, not conversation material, and it is safe to
1557
- * publish for the same reason the route that carries it is loopback-only: a
1558
- * caller who can read this document can already reach that listener directly.
1559
- * It is present on `unreachable` too — a proxy that recorded its port and
1560
- * then went quiet is exactly the case where the URL is the only handle.
1561
- */
1562
- origin?: string;
1563
- /** The proxy's version, from its own manifest. */
1564
- version?: string;
1565
- /** When the proxy process started (epoch ms), 0 when unknown. */
1566
- startedAt?: number;
1567
- /** Sessions newest-first, capped by the route. */
1568
- sessions: readonly PoolWebBillionSession[];
1569
- /** How many sessions the proxy holds in total, including capped-out ones. */
1570
- totalSessions: number;
1571
- }
1572
1549
  /**
1573
1550
  * Where a registry provider's key goes: `apiKey`, `baseUrl`, or `enabled`.
1574
1551
  */
@@ -1577,6 +1554,35 @@ type PoolWebAggregatePatch = {
1577
1554
  providerId: string;
1578
1555
  enabled?: boolean;
1579
1556
  apiKey?: string;
1557
+ /**
1558
+ * Every key, preferred first — replaces whatever was stored.
1559
+ *
1560
+ * A REPLACEMENT rather than a delta, for the same reason `models` is: the
1561
+ * card sends the whole list it is showing, and a delta would let two open
1562
+ * tabs interleave into a set neither of them chose. An empty array clears
1563
+ * every key.
1564
+ */
1565
+ apiKeys?: readonly string[];
1566
+ /**
1567
+ * Append ONE key to the stored list.
1568
+ *
1569
+ * The card cannot send `apiKeys`, because the read route never echoes keys
1570
+ * back — it reports only `keyCount`. A replacement list from the browser
1571
+ * would therefore have to be built from placeholders, and would overwrite
1572
+ * every key the user cannot see. This field says "add this one after the
1573
+ * ones already stored", which is the only instruction the card can honestly
1574
+ * give.
1575
+ *
1576
+ * Ignored when it duplicates a stored key.
1577
+ */
1578
+ appendApiKey?: string;
1579
+ /**
1580
+ * How requests spread across those keys.
1581
+ *
1582
+ * Same vocabulary as the pool lane (`priority`, `round-robin`, `balanced`,
1583
+ * `sticky`, `expiry`); unknown values are rejected rather than stored.
1584
+ */
1585
+ distribution?: string;
1580
1586
  baseUrl?: string;
1581
1587
  /**
1582
1588
  * The chosen model set, replacing whatever this provider exposed before.
@@ -1649,6 +1655,23 @@ interface PoolWebAggregateProvider {
1649
1655
  discovered: boolean;
1650
1656
  /** When the provider was first configured. */
1651
1657
  addedAt?: string;
1658
+ /**
1659
+ * How many keys this vendor holds.
1660
+ *
1661
+ * `0` for a keyless vendor, `1` for the ordinary case, more when the user
1662
+ * pasted alternates. The card needs the COUNT rather than the keys themselves —
1663
+ * keys never leave the host — because "account usage" only means anything when
1664
+ * there is something to distribute over: with one key it would be a switch with
1665
+ * a single option and no effect, so the card hides it.
1666
+ */
1667
+ keyCount: number;
1668
+ /**
1669
+ * The usage strategy in force, when the user has chosen one.
1670
+ *
1671
+ * Absent means the documented default (priority). Shares the pool lane's
1672
+ * vocabulary so both kinds of card present one idea one way.
1673
+ */
1674
+ distribution?: string;
1652
1675
  }
1653
1676
  /** The document {@link POOL_AGGREGATE_PATH} answers with. */
1654
1677
  interface PoolWebAggregateStatus {
@@ -1747,6 +1770,19 @@ interface PoolWebComboCandidate {
1747
1770
  usable: boolean;
1748
1771
  contextWindow: number;
1749
1772
  supportsImages: boolean;
1773
+ /**
1774
+ * Other lanes that serve the SAME model, best-first, excluding this row.
1775
+ *
1776
+ * The "same model, different route" tier: when one provider rate-limits, the
1777
+ * right move is usually to reach the same model through another provider, not
1778
+ * to fall back to a different model — a different model changes the context
1779
+ * window, tool-calling behaviour and writing style.
1780
+ *
1781
+ * Present so the card can offer "also try this model via …" in one click
1782
+ * instead of making the user find the same name under another vendor and add
1783
+ * it three times. Empty when the model has only one route.
1784
+ */
1785
+ siblings: readonly string[];
1750
1786
  }
1751
1787
  /** One chain, resolved against the live roster. */
1752
1788
  interface PoolWebCombo {
@@ -1755,6 +1791,21 @@ interface PoolWebCombo {
1755
1791
  name: string;
1756
1792
  /** Members that exist right now, in order. Empty means the chain is dead. */
1757
1793
  models: readonly string[];
1794
+ /**
1795
+ * The chain as PLATFORM GROUPS, for the two-level editor.
1796
+ *
1797
+ * Always present — grouping `models` when the chain was saved flat — so the
1798
+ * editor never has to branch on which shape reached it. `models` stays the flat,
1799
+ * authoritative sequence; this is the same content arranged the way the router
1800
+ * walks it (platform order, then model order within each platform).
1801
+ *
1802
+ * Model ids here are BARE (no `provider/` prefix): the owner is the field beside
1803
+ * them, and carrying the prefix in both invites the two to disagree.
1804
+ */
1805
+ platforms: readonly {
1806
+ owner: string;
1807
+ models: readonly string[];
1808
+ }[];
1758
1809
  /** Members the user configured but which the roster no longer serves. */
1759
1810
  missing: readonly string[];
1760
1811
  strategy: 'fallback' | 'round-robin';
@@ -2197,9 +2248,40 @@ interface PoolWebAutomationJob {
2197
2248
  * what turns a row from "it ran" into "it did the things I care about".
2198
2249
  */
2199
2250
  detail?: readonly string[];
2251
+ /**
2252
+ * Per-item outcome of the last run, one entry per thing the job looked at.
2253
+ *
2254
+ * `detail` is free text and mixes counters with titles ("签到 +12",
2255
+ * "复活 Account A"), so the card can print it but cannot GROUP it. This is the
2256
+ * same information with a status attached, which is what makes "which tasks
2257
+ * are still unclaimed, and why" answerable:
2258
+ *
2259
+ * claimed — the reward was collected on this run
2260
+ * already — collected earlier today; nothing to do
2261
+ * skipped — the board would not let it be claimed (locked, no energy, …)
2262
+ * failed — the call came back an error
2263
+ *
2264
+ * Without it, "0 claimed" reads the same whether everything was already done,
2265
+ * the account had no energy, or the endpoint is broken — three situations with
2266
+ * three different fixes.
2267
+ */
2268
+ items?: readonly PoolWebAutomationItem[];
2200
2269
  /** A pending milestone worth naming, e.g. the next streak tier countdown. */
2201
2270
  progress?: string;
2202
2271
  }
2272
+ /** How one item of an automation run ended. */
2273
+ type PoolWebAutomationItemStatus = 'claimed' | 'already' | 'skipped' | 'failed';
2274
+ /** One item an automation job considered, with what happened to it. */
2275
+ interface PoolWebAutomationItem {
2276
+ /** The task board's own title, or a short label for a non-task step. */
2277
+ title: string;
2278
+ status: PoolWebAutomationItemStatus;
2279
+ /** Credits/energy collected, when the upstream reported any. */
2280
+ credit?: number;
2281
+ energy?: number;
2282
+ /** Why it was skipped or how it failed, in the upstream's words. */
2283
+ note?: string;
2284
+ }
2203
2285
  /**
2204
2286
  * Automation block on the status document.
2205
2287
  *
@@ -2324,8 +2406,23 @@ interface WorkBuddyModelInfo {
2324
2406
  * (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
2325
2407
  * authoritative while being wrong.
2326
2408
  *
2327
- * `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
2328
- * which is what turns on the free badge.
2409
+ * NO PRICES AND NO PROMO BADGES HERE. This table is what the picker shows when
2410
+ * the startup fetch fails, and a multiplier captured months ago is a price the
2411
+ * user will budget against while it is quietly wrong. Two specific harms:
2412
+ *
2413
+ * - `multiplier: 0` renders the "free" badge, so a model that was free at
2414
+ * capture time keeps claiming to be free after the promotion ends;
2415
+ * - a non-zero multiplier renders next to the model, so a stale figure reads
2416
+ * as the current rate.
2417
+ *
2418
+ * `undefined` is the honest state: the card prints no rate and no badge, which
2419
+ * is exactly true — this build does not know the price. The live fetch fills
2420
+ * both in within a second of a working startup, and `catalogSource` already
2421
+ * tells the card to say the list is the built-in one.
2422
+ *
2423
+ * Windows, output ceilings and the `craft` capability tag stay: those are
2424
+ * properties of the model, not of a campaign, and they are what makes the list
2425
+ * usable offline at all.
2329
2426
  */
2330
2427
  export declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
2331
2428
  /** Which gateway a catalog describes. Mirrors `WorkBuddyRegion` in `upstream.ts`. */
@@ -2362,11 +2459,24 @@ export declare class WorkBuddyCatalog {
2362
2459
  current(): readonly WorkBuddyModelInfo[];
2363
2460
  /**
2364
2461
  * The models DSH should actually offer, after applying the user's selection:
2365
- * disabled models are dropped, an explicit image list overrides the upstream
2366
- * capability flag, and a per-model budget caps the advertised window.
2462
+ * disabled models are dropped, an explicit image list decides image support,
2463
+ * and a per-model budget caps the advertised window.
2367
2464
  *
2368
2465
  * An absent `enabledModelIds` means "everything" — a fresh install with no
2369
2466
  * saved selection must not present an empty picker.
2467
+ *
2468
+ * IMAGE SUPPORT IS THE USER'S SET, NOT AN OR. The upstream flag is `true` for
2469
+ * essentially every model of this vendor (measured: 15 of 16 on CN, 23 of 23
2470
+ * on international), so `supportsImages || imageSet.has(id)` could only ever
2471
+ * turn the flag ON — un-ticking the box in the card changed nothing, and the
2472
+ * adapter kept advertising `['text','image']`. The checkbox existed and did
2473
+ * not work, which is worse than not offering it.
2474
+ *
2475
+ * So when the user HAS saved an image selection, that set is the answer, in
2476
+ * both directions. When they have not (undefined — a fresh install, or a
2477
+ * config written before this fix), the upstream capability stands: silently
2478
+ * disabling image input for everyone who never touched the switch would be a
2479
+ * regression, and the absent case is exactly "no opinion".
2370
2480
  */
2371
2481
  visible(): readonly WorkBuddyModelInfo[];
2372
2482
  /** Replace the catalog and notify the adapter to rebuild its model list. */
@@ -2433,7 +2543,42 @@ interface ProviderSetting {
2433
2543
  * one vendor that WAS configured indistinguishable from the rest.
2434
2544
  */
2435
2545
  enabled?: boolean;
2546
+ /**
2547
+ * The FIRST usable key, kept as the single-key spelling.
2548
+ *
2549
+ * Almost every provider needs one key, and a user pasting one must not have to
2550
+ * learn an array to do it. This field and {@link apiKeys} are kept in step by
2551
+ * `updateProviderSetting`: writing `apiKey` sets a one-entry list, and writing
2552
+ * `apiKeys` mirrors its first entry back here. Readers therefore do not have to
2553
+ * know which spelling a config file happens to hold.
2554
+ */
2436
2555
  apiKey?: string;
2556
+ /**
2557
+ * Every key this provider may rotate through.
2558
+ *
2559
+ * The pool has always rotated accounts, but a third-party vendor could only
2560
+ * ever hold ONE key, so "account usage" (priority / round-robin / balanced …)
2561
+ * had nothing to distribute over and was not offered on those cards. A vendor
2562
+ * with several keys on one account — which is how most rate limits are actually
2563
+ * dodged — could not express that at all.
2564
+ *
2565
+ * The first entry is the preferred one; the rest are alternates in order.
2566
+ * Absent or empty means "no key configured", which is the same state `apiKey`
2567
+ * has always described.
2568
+ */
2569
+ apiKeys?: readonly string[];
2570
+ /**
2571
+ * How requests spread across those keys.
2572
+ *
2573
+ * The same five strategies the pool lane offers, and deliberately the same
2574
+ * vocabulary rather than a second set of names for one idea. Absent means
2575
+ * `priority`: use the preferred key until the vendor refuses it, then move on.
2576
+ *
2577
+ * Only meaningful when {@link apiKeys} holds more than one entry — with a single
2578
+ * key there is nothing to distribute over, which is why the card hides the row
2579
+ * in that case.
2580
+ */
2581
+ distribution?: string;
2437
2582
  /** User override for the base URL, for providers whose host is per-deployment. */
2438
2583
  baseUrl?: string;
2439
2584
  /**
@@ -2470,13 +2615,45 @@ interface AggregateSettings {
2470
2615
  /** Chain strategies. Anything else on disk falls back to `fallback`. */
2471
2616
  declare const COMBO_STRATEGIES: readonly ['fallback', 'round-robin'];
2472
2617
  type ComboStrategy = (typeof COMBO_STRATEGIES)[number];
2618
+ /**
2619
+ * One platform's contribution to a chain: which lane, and which of its models.
2620
+ *
2621
+ * Chains are edited as a TWO-LEVEL list — platform order, then model order inside
2622
+ * each platform — because that is the order the router actually walks and the
2623
+ * order a reader thinks in ("try the bytes one, then Qoder; if both fail, go to
2624
+ * CodeBuddy"). A flat member list expresses the same sequence but cannot show the
2625
+ * grouping, so re-ordering "all of WorkBuddy's models together" meant moving them
2626
+ * one at a time and hoping none was missed.
2627
+ *
2628
+ * `owner` is `workbuddy` for a pool model, or an aggregate provider id. Model ids
2629
+ * are BARE here (no `provider/` prefix): the prefix is derived from the owner when
2630
+ * the chain is flattened, so storing it twice would let the two disagree.
2631
+ */
2632
+ interface ComboPlatform {
2633
+ owner: string;
2634
+ models: readonly string[];
2635
+ }
2473
2636
  /** A chain as the user configured it, before the catalogs are consulted. */
2474
2637
  interface ComboSetting {
2475
2638
  /** `combo:<slug>`; derived from the name when absent or malformed. */
2476
2639
  id?: string;
2477
2640
  name: string;
2478
- /** Member model ids, in the order the user put them. */
2641
+ /**
2642
+ * Member model ids in the order the user put them — the FLATTENED, authoritative
2643
+ * sequence the router walks.
2644
+ *
2645
+ * Kept as the single source of truth so every resolver, the picker row and the
2646
+ * shim keep reading one field. A chain saved with `platforms` also carries this,
2647
+ * derived from them, so an older build and an older config file both keep working.
2648
+ */
2479
2649
  models: readonly string[];
2650
+ /**
2651
+ * The same chain as platform groups, when it was edited that way.
2652
+ *
2653
+ * Absent for a chain written before this existed, or saved flat. The editor
2654
+ * groups such a chain by owner on the fly, so it opens correctly either way.
2655
+ */
2656
+ platforms?: readonly ComboPlatform[];
2480
2657
  /** How the chain advances; `fallback` restarts from the top each turn. */
2481
2658
  strategy?: ComboStrategy;
2482
2659
  /** Consecutive turns one member serves before `round-robin` advances. */
@@ -2553,60 +2730,6 @@ interface RelaySetting {
2553
2730
  models?: string[];
2554
2731
  }
2555
2732
  //#endregion
2556
- //#region src/aggregate.d.ts
2557
- /** Registry `format` → the wire vocabulary this module speaks. */
2558
- type AggregateWire = 'chat' | 'responses' | 'messages';
2559
- /** One model the aggregate lane offers. */
2560
- interface AggregateModelRow {
2561
- /** Composite id: `providerId/modelId`, which is also the picker id. */
2562
- id: string;
2563
- /** Display name: the provider's own name plus the row's label. */
2564
- name: string;
2565
- providerId: string;
2566
- modelId: string;
2567
- contextWindow: number;
2568
- maxOutputTokens: number;
2569
- supportsImages: boolean;
2570
- wire: AggregateWire;
2571
- /** `static` from the registry, `discovered` from a live listing. */
2572
- source: 'static' | 'discovered';
2573
- }
2574
- /** One stored aggregate row set, refreshed on change, for the shim and adapter. */
2575
- declare class AggregateCatalog {
2576
- private readonly read;
2577
- private readonly poolRows;
2578
- private rows;
2579
- private chainRows;
2580
- /**
2581
- * @param read The settings document.
2582
- * @param poolRows The pool's own rows, needed only to resolve a chain member
2583
- * that names a WorkBuddy model. A chain may mix pool models with aggregate
2584
- * ones, so without this a perfectly valid chain would lose its pool half.
2585
- */
2586
- constructor(read: () => AggregateStore | Promise<AggregateStore>, poolRows?: () => readonly WorkBuddyModelInfo[]);
2587
- /**
2588
- * Every row this catalog offers: third-party rows first, then chain rows.
2589
- *
2590
- * Chains come last so a picker listing stays readable — a chain is a
2591
- * convenience over models the user already has listed, not a new vendor.
2592
- */
2593
- current(): readonly (AggregateModelRow | ComboModelRow)[];
2594
- /** Only the third-party rows, which is what the aggregate resolver needs. */
2595
- providerRows(): readonly AggregateModelRow[];
2596
- /** Only the chain rows. */
2597
- comboRows(): readonly ComboModelRow[];
2598
- /** Re-read the store and notify listeners when the roster actually changed. */
2599
- refresh(): Promise<readonly (AggregateModelRow | ComboModelRow)[]>;
2600
- private listeners;
2601
- onChange(listener: () => void): () => void;
2602
- /** One third-party row by composite id, or undefined. */
2603
- find(id: string): AggregateModelRow | undefined;
2604
- /** One chain row by combo id, or undefined. */
2605
- findCombo(id: string): ComboModelRow | undefined;
2606
- /** Whether any provider or chain is serving anything right now. */
2607
- hasRows(): boolean;
2608
- }
2609
- //#endregion
2610
2733
  //#region src/stats.d.ts
2611
2734
  /**
2612
2735
  * Per-call usage accounting for the chat path.
@@ -2647,7 +2770,18 @@ interface TokenUsage {
2647
2770
  }
2648
2771
  /** One upstream HTTP round trip, successful or not. */
2649
2772
  interface UsageCall {
2650
- /** Which gateway served it: `cn` or `global`. */
2773
+ /**
2774
+ * Which CHANNEL served the call: `workbuddy`, `free`, or a provider id.
2775
+ *
2776
+ * Until this existed the ledger could only say `地区` (domestic /
2777
+ * international), which is a property of the WorkBuddy pool and of nothing
2778
+ * else. Every row therefore read `国内版` even when the call had gone to a
2779
+ * third-party vendor or to the keyless lane, and the only honest reading of the
2780
+ * table was "the other channels are not recorded at all" — which was true:
2781
+ * only the pool and the free lane ever called `record`.
2782
+ */
2783
+ channel: string;
2784
+ /** Which WorkBuddy gateway served it: `cn` or `global`; empty for other lanes. */
2651
2785
  region: string;
2652
2786
  /** The model id the request asked for. */
2653
2787
  model: string;
@@ -2681,6 +2815,14 @@ interface UsageCall {
2681
2815
  }
2682
2816
  /** One row of the per-model aggregate. */
2683
2817
  interface UsageRow {
2818
+ /**
2819
+ * The channel this row is about: `workbuddy`, `free`, or a provider id.
2820
+ *
2821
+ * Part of the grouping key, not just a label. Two vendors can publish the same
2822
+ * model id, and the pool and the free lane can both serve `glm-5.3` — summing
2823
+ * them into one row would attribute a third-party vendor's tokens to WorkBuddy.
2824
+ */
2825
+ channel: string;
2684
2826
  region: string;
2685
2827
  model: string;
2686
2828
  /** Physical upstream calls. */
@@ -2706,6 +2848,14 @@ interface UsageRow {
2706
2848
  interface UsageAccountRow {
2707
2849
  /** Display label of the account, as the pool prints it. */
2708
2850
  account: string;
2851
+ /**
2852
+ * The channel this account belongs to.
2853
+ *
2854
+ * Part of the grouping key: a provider's key and a pool account are different
2855
+ * things even when both are labelled, and `free` is not a WorkBuddy account at
2856
+ * all.
2857
+ */
2858
+ channel: string;
2709
2859
  /** Region the account belongs to; an account serves one gateway. */
2710
2860
  region: string;
2711
2861
  /** Physical calls this account served. */
@@ -2817,6 +2967,60 @@ declare class UsageRecorder {
2817
2967
  private days;
2818
2968
  }
2819
2969
  //#endregion
2970
+ //#region src/aggregate.d.ts
2971
+ /** Registry `format` → the wire vocabulary this module speaks. */
2972
+ type AggregateWire = 'chat' | 'responses' | 'messages';
2973
+ /** One model the aggregate lane offers. */
2974
+ interface AggregateModelRow {
2975
+ /** Composite id: `providerId/modelId`, which is also the picker id. */
2976
+ id: string;
2977
+ /** Display name: the provider's own name plus the row's label. */
2978
+ name: string;
2979
+ providerId: string;
2980
+ modelId: string;
2981
+ contextWindow: number;
2982
+ maxOutputTokens: number;
2983
+ supportsImages: boolean;
2984
+ wire: AggregateWire;
2985
+ /** `static` from the registry, `discovered` from a live listing. */
2986
+ source: 'static' | 'discovered';
2987
+ }
2988
+ /** One stored aggregate row set, refreshed on change, for the shim and adapter. */
2989
+ declare class AggregateCatalog {
2990
+ private readonly read;
2991
+ private readonly poolRows;
2992
+ private rows;
2993
+ private chainRows;
2994
+ /**
2995
+ * @param read The settings document.
2996
+ * @param poolRows The pool's own rows, needed only to resolve a chain member
2997
+ * that names a WorkBuddy model. A chain may mix pool models with aggregate
2998
+ * ones, so without this a perfectly valid chain would lose its pool half.
2999
+ */
3000
+ constructor(read: () => AggregateStore | Promise<AggregateStore>, poolRows?: () => readonly WorkBuddyModelInfo[]);
3001
+ /**
3002
+ * Every row this catalog offers: third-party rows first, then chain rows.
3003
+ *
3004
+ * Chains come last so a picker listing stays readable — a chain is a
3005
+ * convenience over models the user already has listed, not a new vendor.
3006
+ */
3007
+ current(): readonly (AggregateModelRow | ComboModelRow)[];
3008
+ /** Only the third-party rows, which is what the aggregate resolver needs. */
3009
+ providerRows(): readonly AggregateModelRow[];
3010
+ /** Only the chain rows. */
3011
+ comboRows(): readonly ComboModelRow[];
3012
+ /** Re-read the store and notify listeners when the roster actually changed. */
3013
+ refresh(): Promise<readonly (AggregateModelRow | ComboModelRow)[]>;
3014
+ private listeners;
3015
+ onChange(listener: () => void): () => void;
3016
+ /** One third-party row by composite id, or undefined. */
3017
+ find(id: string): AggregateModelRow | undefined;
3018
+ /** One chain row by combo id, or undefined. */
3019
+ findCombo(id: string): ComboModelRow | undefined;
3020
+ /** Whether any provider or chain is serving anything right now. */
3021
+ hasRows(): boolean;
3022
+ }
3023
+ //#endregion
2820
3024
  //#region src/channel-lane.d.ts
2821
3025
  /** One model as the gateway lists and routes it. */
2822
3026
  interface ChannelLaneRow {
@@ -3220,6 +3424,16 @@ interface AutomationJobState {
3220
3424
  * is the question the panel exists to answer.
3221
3425
  */
3222
3426
  detail?: readonly string[];
3427
+ /**
3428
+ * The same work with a status per item.
3429
+ *
3430
+ * `detail` is a flat list of sentences, so the card can print it but cannot
3431
+ * group it. This carries `claimed` / `already` / `skipped` / `failed` per
3432
+ * thing the job looked at, which is what separates "everything was already
3433
+ * done" from "nothing was claimable" from "the endpoint is down" — three
3434
+ * outcomes that all print as "0 claimed".
3435
+ */
3436
+ items?: readonly PoolWebAutomationItem[];
3223
3437
  /**
3224
3438
  * A pending milestone worth naming, when there is one.
3225
3439
  *
@@ -3405,6 +3619,16 @@ export declare class WorkBuddyScheduler {
3405
3619
  /** Host hooks that persist the ledger across restarts. */
3406
3620
  private readonly loadEarnings;
3407
3621
  private saveEarningsFn;
3622
+ /**
3623
+ * The most recent ledger write that may still be in flight.
3624
+ *
3625
+ * `persistEarnings` is synchronous from its callers' point of view, but the
3626
+ * host's settings write is not: it is an async document update. Keeping the
3627
+ * promise here is what lets {@link flushEarnings} wait for the write that
3628
+ * matters — the last one before a restart — without making every counter
3629
+ * update await disk.
3630
+ */
3631
+ private pendingEarningsWrite;
3408
3632
  constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
3409
3633
  /** Apply a new configuration; safe to call while running. */
3410
3634
  /**
@@ -3415,6 +3639,16 @@ export declare class WorkBuddyScheduler {
3415
3639
  * would have nowhere to go.
3416
3640
  */
3417
3641
  setEarningsPersistence(save: (ledger: AutomationLedger) => void | Promise<void>): void;
3642
+ /**
3643
+ * Wait for the ledger write that is (or was last) in flight.
3644
+ *
3645
+ * Called at the END of a task pass, right after the last claim, so the rewards
3646
+ * that were just collected are on disk before a restart can lose them. It is
3647
+ * deliberately NOT awaited on every counter bump: the settings document write
3648
+ * is the slow part, and paying it per account would slow the pass for no
3649
+ * benefit while the process is still alive.
3650
+ */
3651
+ flushEarnings(): Promise<void>;
3418
3652
  /**
3419
3653
  * Fold a previously persisted ledger back in, when it belongs to today.
3420
3654
  *
@@ -3528,6 +3762,14 @@ export declare class WorkBuddyScheduler {
3528
3762
  * Best effort on purpose: a failed save must never abort a run that has
3529
3763
  * already collected rewards, and the in-memory ledger keeps serving the card
3530
3764
  * for the rest of the session either way.
3765
+ *
3766
+ * The returned promise settles when the write has landed (or failed). Callers
3767
+ * that are about to END a run await it: reported in #98, a task pass that
3768
+ * claimed 21 tasks had its process restarted before the volatile settings
3769
+ * write reached disk, so `automationEarnings` still read `credit: 0` while
3770
+ * the upstream had really paid out. Fire-and-forget is fine for a counter the
3771
+ * card will re-read in this process; it is not fine for the last write before
3772
+ * exit.
3531
3773
  */
3532
3774
  private persistEarnings;
3533
3775
  /**
@@ -3554,6 +3796,10 @@ export declare class WorkBuddyScheduler {
3554
3796
  *
3555
3797
  * Disabled accounts are excluded here rather than filtered by the caller so a
3556
3798
  * card switch takes effect on the next pass without any event plumbing.
3799
+ *
3800
+ * `job` narrows the set by region scope: the growth jobs are CN-only while
3801
+ * check-in runs on both gateways, so eligibility cannot be decided without
3802
+ * knowing which job is about to run.
3557
3803
  */
3558
3804
  private accountsInOrder;
3559
3805
  /**
@@ -3584,6 +3830,38 @@ export declare class WorkBuddyScheduler {
3584
3830
  * this is async.
3585
3831
  */
3586
3832
  private chainsFor;
3833
+ /**
3834
+ * Summon-and-use one expert NAMED BY THE TASK.
3835
+ *
3836
+ * Structurally `expertChains` minus the marketplace: the expert id is read
3837
+ * from the task's deeplink instead of by browsing `marketExpertList`, because
3838
+ * this family's experts are not listed there at all (#98 measured 500 entries
3839
+ * across both `agent` and `team`, zero hits).
3840
+ *
3841
+ * Returns `[]` — not a throw — when the task carries no usable id: the caller
3842
+ * logs "could not be built" only for exceptions, and a task shape this code
3843
+ * does not understand is information, not a fault.
3844
+ */
3845
+ /**
3846
+ * Find the expert a task is about, by matching its title against the
3847
+ * marketplace.
3848
+ *
3849
+ * The marketplace listing is ~500 entries across `agent` and `team`, and this
3850
+ * only runs for a task whose deeplink carried no id — at most a handful per
3851
+ * run — so the cost is acceptable. It is not cached because a freshly
3852
+ * published expert must become findable without a restart.
3853
+ *
3854
+ * Matching rules, all of them strict on purpose:
3855
+ * - the task's title must appear IN the expert's name or profession, or the
3856
+ * expert's display name must appear in the title (both directions, since
3857
+ * the board and the marketplace word things differently);
3858
+ * - comparison ignores case, whitespace and the punctuation vendors sprinkle
3859
+ * through names;
3860
+ * - several matches means NO answer. Summoning the wrong agent scores 0, so a
3861
+ * guess costs an upstream call and still fails — worse than declining.
3862
+ */
3863
+ private expertIdByKeyword;
3864
+ private taskExpertChains;
3587
3865
  /**
3588
3866
  * The summon-and-use chains for the expert tasks.
3589
3867
  *
@@ -3979,21 +4257,21 @@ interface PoolStatusRouteOptions {
3979
4257
  * a chain name, and does what it names still exist" — and answering them from
3980
4258
  * two snapshots could show a member that the chains no longer resolve.
3981
4259
  */
3982
- combosDocument?: () => PoolWebCombosStatus;
4260
+ /**
4261
+ * The routing document.
4262
+ *
4263
+ * May return a PROMISE: login-channel rows come from the host's
4264
+ * `llm.listModels`, which is asynchronous, and a chain that cannot name TRAE or
4265
+ * Qoder is precisely the defect this document had. A synchronous build keeps
4266
+ * returning the object directly, so both shapes are accepted rather than forcing
4267
+ * every caller to become async.
4268
+ */
4269
+ combosDocument?: () => PoolWebCombosStatus | Promise<PoolWebCombosStatus>;
3983
4270
  /**
3984
4271
  * Replace the chain table and refresh the running catalog, then re-answer the
3985
4272
  * document. The catalog refresh is what makes a new chain pickable at once.
3986
4273
  */
3987
4274
  saveCombos?: (raw: unknown) => Promise<PoolWebCombosStatus>;
3988
- /**
3989
- * What the locally running billion-context proxy is doing, read-only.
3990
- *
3991
- * Absent when the module could not be wired: the route then answers 503
3992
- * rather than pretending a third-party proxy reported nothing. It never
3993
- * rejects — a missing proxy is a normal state with its own `reason`, because
3994
- * billion-context is a separate optional plugin.
3995
- */
3996
- billionDocument?: () => Promise<PoolWebBillionStatus>;
3997
4275
  /**
3998
4276
  * The relay's current state, or undefined when this build has no relay.
3999
4277
  *
@@ -4110,6 +4388,15 @@ export interface Config {
4110
4388
  * does not wipe what the automation already earned.
4111
4389
  */
4112
4390
  automationEarnings?: AutomationLedger;
4391
+ /**
4392
+ * Whether the shared 免密 free lane contributes models.
4393
+ *
4394
+ * Absent reads as OFF. It is the only lane that defaults off: it is a shared
4395
+ * third-party endpoint with no credential of the user's own, so leaving it on
4396
+ * dropped a group into the model picker that nothing in the UI could remove
4397
+ * (the aggregate switch does not govern it — it is not an aggregate provider).
4398
+ */
4399
+ freeLane?: boolean;
4113
4400
  }
4114
4401
  /** One region's saved model selection. */
4115
4402
  export interface ModelSelectionConfig {