dsh-workbuddy-xdpool 1.7.2 → 1.7.3

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
@@ -288,6 +288,13 @@ interface UpstreamClientOptions {
288
288
  fetchImpl?: typeof fetch;
289
289
  /** Client version string sent to the upstream. */
290
290
  clientVersion?: string;
291
+ /**
292
+ * Override the catalog-fetch backoff, in milliseconds.
293
+ *
294
+ * Tests set this to `[]` (or tiny values) so a retry case does not spend real
295
+ * seconds sleeping. Production uses {@link CATALOG_RETRY_BACKOFF_MS}.
296
+ */
297
+ catalogRetryBackoffMs?: readonly number[];
291
298
  }
292
299
  /** Region for a login domain; an empty domain means CN (matching upstream tooling). */
293
300
  /** The two gateways WorkBuddy serves: the domestic one and the international one. */
@@ -421,6 +428,19 @@ export declare function buddyAppEvents(buddyId: string, buddyName: string): Reco
421
428
  export declare class WorkBuddyUpstreamClient {
422
429
  private readonly fetchImpl;
423
430
  private readonly clientVersion;
431
+ /** Backoff ladder for `fetchModels`; empty means a single attempt. */
432
+ private readonly catalogRetryBackoffMs;
433
+ /**
434
+ * Optional logger for retry notices.
435
+ *
436
+ * Set by the host so a retry is visible in the log with its attempt count —
437
+ * without it, a retry that eventually succeeds is invisible, and an operator
438
+ * debugging "why was the catalog slow" has nothing to look at.
439
+ */
440
+ logger: {
441
+ warn?(...args: unknown[]): void;
442
+ info?(...args: unknown[]): void;
443
+ } | undefined;
424
444
  constructor(options?: UpstreamClientOptions);
425
445
  /**
426
446
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
@@ -467,6 +487,16 @@ export declare class WorkBuddyUpstreamClient {
467
487
  * below is common to the two branches.
468
488
  */
469
489
  fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
490
+ /**
491
+ * One catalog attempt, without retries.
492
+ *
493
+ * Split out so {@link fetchModels} can retry it: the failure this guards
494
+ * against is a startup network hiccup, where several independent components
495
+ * see `fetch failed` inside the same second and the very next request
496
+ * succeeds — exactly the case a single attempt turns into "the user's model
497
+ * list is missing half its entries for the rest of the session".
498
+ */
499
+ private fetchModelsOnce;
470
500
  /** Read-only credits query, aggregated by package. Does not consume credits. */
471
501
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
472
502
  /** Query today's check-in status without changing account state. */
@@ -1063,241 +1093,283 @@ export declare class WorkBuddyAccountPool {
1063
1093
  };
1064
1094
  }
1065
1095
  //#endregion
1066
- //#region src/catalog.d.ts
1067
- /** One model the provider exposes. */
1068
- interface WorkBuddyModelInfo {
1096
+ //#region src/status-paths.d.ts
1097
+ /**
1098
+ * Node-free constants and types shared by the Host and browser halves of the
1099
+ * WorkBuddy XD Pool settings card.
1100
+ *
1101
+ * Pool's runtime state already lives in `src/status.ts` (`buildStatus` /
1102
+ * `WorkBuddyStatus`); this module only carves the cross-domain (Host→browser)
1103
+ * JSON document into a shape that stays token-free and matches what the
1104
+ * browser card renders. Route paths are plugin-owned and mounted on the Host's
1105
+ * same-origin web server (see `src/web-status.ts`).
1106
+ *
1107
+ * @module dsh-workbuddy-xdpool/status-paths
1108
+ */
1109
+ /** Plugin-owned read-only pool status endpoint (account rows + models + shim). */
1110
+ export declare const POOL_STATUS_PATH = "/plugins/dsh-workbuddy-xdpool/status";
1111
+ /** Plugin-owned local account rescan endpoint (re-read desktop snapshots). */
1112
+ export declare const POOL_RESCAN_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/rescan";
1113
+ /** Plugin-owned cooldown reset endpoint (clear all 429 cooldowns). */
1114
+ export declare const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset";
1115
+ /** Plugin-owned daily check-in action endpoint (claim today's reward). */
1116
+ export declare const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
1117
+ /** Plugin-owned model-selection save endpoint (writes the settings section). */
1118
+ export declare const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
1119
+ /**
1120
+ * Throw one account out of the pool for good, or take it back.
1121
+ *
1122
+ * Separate from the disable route because the semantics differ: disabling is a
1123
+ * rotation preference the account survives, ignoring survives the account.
1124
+ */
1125
+ export declare const POOL_ACCOUNT_IGNORE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/ignored";
1126
+ /** Run one automation job immediately, so the card can verify it on demand. */
1127
+ export declare const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
1128
+ /** Set or clear one account's reserved-credit floor. */
1129
+ export declare const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
1130
+ /** One account's row, token-free. */
1131
+ interface PoolWebAccount {
1132
+ id: string;
1133
+ label: string;
1134
+ nickname?: string;
1135
+ domain: string;
1136
+ /** ISO timestamp; absent when the credential carries no expiry. */
1137
+ expiresAt?: string;
1138
+ /** Account-wide cooldown (every model blocked); only after a no-model penalize. */
1139
+ cooling: boolean;
1140
+ /** ISO timestamp when the account-wide 429 cooldown lifts; only while cooling. */
1141
+ cooldownUntil?: string;
1142
+ /**
1143
+ * Per-model cooldowns currently active. The account is NOT `cooling` while a
1144
+ * model is limited — its other models still serve — but each entry tells the
1145
+ * card which model is out until when (e.g. `hy4-preview` cooling to 10:14,
1146
+ * `hy3` normal).
1147
+ */
1148
+ modelCooldowns?: ReadonlyArray<{
1149
+ modelId: string;
1150
+ until: string;
1151
+ }>;
1152
+ /**
1153
+ * Whether the user switched this account off. A disabled account never
1154
+ * serves a request, but it stays listed so the card can switch it back on.
1155
+ */
1156
+ disabled: boolean;
1157
+ rateLimitHits: number;
1158
+ /**
1159
+ * Credits the user asked to keep for this account. The pool stops picking the
1160
+ * account once its balance reaches the reserve, so this many credits survive.
1161
+ * 0 means the account may be spent down as before.
1162
+ */
1163
+ creditReserve: number;
1164
+ /**
1165
+ * Whether the account is held back purely by its reserve right now. Kept
1166
+ * distinct from `cooling`: a reserved account is healthy and simply
1167
+ * protected, which is a different thing to tell the user than rate-limited.
1168
+ */
1169
+ reserved: boolean;
1170
+ /**
1171
+ * What the automation earned for this account today. Absent when it earned
1172
+ * nothing (or the automation never ran for it), so the card can stay quiet
1173
+ * instead of printing a row of zeroes.
1174
+ */
1175
+ automationToday?: PoolWebAutomationEarnings;
1176
+ /** ISO timestamp of the last successful use (best-effort pool bookkeeping). */
1177
+ lastUsedAt?: string;
1178
+ /** Aggregated credit summary for the account, read-only. */
1179
+ credits?: PoolWebCredits;
1180
+ creditsError?: string;
1181
+ /**
1182
+ * Today's check-in state for this account, read-only. Present only when the
1183
+ * per-account check-in probe succeeded and the program is active. The card
1184
+ * renders one claim button per account, so a multi-account pool can collect
1185
+ * every account's daily reward without switching accounts by hand.
1186
+ */
1187
+ checkin?: PoolWebCheckin;
1188
+ checkinError?: string;
1189
+ }
1190
+ /** One credit package (as surfaced by the pool's upstream client), node-free. */
1191
+ interface PoolWebCreditPackage {
1192
+ packageName: string;
1193
+ remain?: number;
1194
+ size?: number;
1195
+ /** CapacityType 4 — refreshed each cycle and never expires. */
1196
+ monthly?: boolean;
1197
+ /** Next cycle refresh point, ms. */
1198
+ cycleRefreshMs?: number;
1199
+ /** One-off expiry, ms. */
1200
+ expiresAtMs?: number;
1201
+ }
1202
+ /** Aggregated credit answer the card renders under one account. */
1203
+ interface PoolWebCredits {
1204
+ total?: number;
1205
+ packages: readonly PoolWebCreditPackage[];
1206
+ /** Credits expiring within 3 days. */
1207
+ expiringSoon?: number;
1208
+ /** When the nearest package expires, ms. */
1209
+ nearestExpiryMs?: number;
1210
+ }
1211
+ /**
1212
+ * Daily check-in state the card renders under one account's credits. Mirrors
1213
+ * the upstream activity endpoint, minus anything the browser does not need.
1214
+ */
1215
+ interface PoolWebCheckin {
1216
+ /** The activity is running; a claim button is offered only while true. */
1217
+ active: boolean;
1218
+ /** Already collected today — the button renders as a done state. */
1219
+ todayCheckedIn: boolean;
1220
+ /** Consecutive days checked in. */
1221
+ streakDays: number;
1222
+ /** Credits a single day grants. */
1223
+ dailyCredit: number;
1224
+ /** Credits collected today (0 before claiming). */
1225
+ todayCredit: number;
1226
+ /** Today is a streak milestone day. */
1227
+ isStreakDay: boolean;
1228
+ /** The day count the next milestone lands on. */
1229
+ nextStreakDay: number;
1230
+ /** Bonus credits granted on a milestone day. */
1231
+ streakBonusCredit: number;
1232
+ }
1233
+ /** Result of one claim, so the card can confirm what was collected. */
1234
+ interface PoolWebCheckinClaim {
1235
+ credit: number;
1236
+ streakDays: number;
1237
+ isStreakDay: boolean;
1238
+ }
1239
+ /** One model the pool exposes to DSH, with cost / free tags. */
1240
+ interface PoolWebModel {
1069
1241
  id: string;
1070
- /** Display name; the multiplier is appended for the picker. */
1071
1242
  name: string;
1072
- contextWindow: number;
1073
- maxOutputTokens: number;
1074
- /** Relative credit cost, e.g. 0.79 for `x0.79`. */
1243
+ /** Relative credit cost, e.g. 0.79 for x0.79. */
1075
1244
  multiplier?: number;
1076
- /** Upstream-declared thinking levels. */
1077
- supportedEfforts?: readonly string[];
1078
- supportsImages: boolean;
1079
1245
  /** Upstream tags: free / limited-free / night-discount. */
1080
1246
  tags?: readonly string[];
1247
+ /** Effective image support after the user's per-model toggle. */
1248
+ supportsImages: boolean;
1249
+ /** Effective context window after the user's budget cap. */
1250
+ contextWindow: number;
1251
+ /** The window the upstream advertises, before any cap. */
1252
+ nativeContextWindow: number;
1253
+ /** Upstream output ceiling, so the card can show both limits. */
1254
+ maxOutputTokens: number;
1255
+ /** Thinking levels the upstream declares, when it declares any. */
1256
+ supportedEfforts?: readonly string[];
1257
+ /** Whether this model is currently enabled in the picker. */
1258
+ enabled: boolean;
1081
1259
  }
1082
1260
  /**
1083
- * Static fallback used before the first live catalog fetch, and whenever the
1084
- * upstream cannot be reached.
1261
+ * Body of the account ignore/unignore route: exactly one account per request.
1085
1262
  *
1086
- * The multipliers are carried on purpose. Without them the provider's model
1087
- * picker silently loses every rate and every free badge the moment the live
1088
- * fetch fails — which reads to the user as "the plugin broke my model list"
1089
- * rather than "the upstream is unreachable". The values are the ones the two
1090
- * gateways actually advertise for these ids (`credits: "x0.79 credits"` and so
1091
- * on), so a fallback row looks the same as a live one.
1263
+ * `ignored: true` throws the account out of the pool for good (its credential is
1264
+ * not even read on the next scan, and a fresh desktop sign-in will not bring it
1265
+ * back). `false` restores it, at which point the next scan discovers it again.
1266
+ */
1267
+ interface PoolWebAccountIgnore {
1268
+ /** Pool account id, as reported in `PoolWebAccount.id`. */
1269
+ accountId: string;
1270
+ /** `true` ignores the account permanently; `false` takes it back. */
1271
+ ignored: boolean;
1272
+ }
1273
+ /**
1274
+ * One account the user has thrown out of the pool.
1092
1275
  *
1093
- * `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
1094
- * which is what turns on the free badge.
1276
+ * Kept on the status document so the card can list what was ignored and offer a
1277
+ * way back: without that, "ignored" is a one-way door the user cannot inspect or
1278
+ * undo from the UI, which is how a hidden list becomes a support burden.
1095
1279
  */
1096
- export declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
1097
- /** Live catalog with a static fallback behind it. */
1098
- export declare class WorkBuddyCatalog {
1099
- private models;
1100
- private listeners;
1101
- /** User's model selection. Empty object = follow the catalog unfiltered. */
1102
- private selection;
1103
- current(): readonly WorkBuddyModelInfo[];
1104
- /**
1105
- * The models DSH should actually offer, after applying the user's selection:
1106
- * disabled models are dropped, an explicit image list overrides the upstream
1107
- * capability flag, and a per-model budget caps the advertised window.
1108
- *
1109
- * An absent `enabledModelIds` means "everything" — a fresh install with no
1110
- * saved selection must not present an empty picker.
1111
- */
1112
- visible(): readonly WorkBuddyModelInfo[];
1113
- /** Replace the catalog and notify the adapter to rebuild its model list. */
1114
- update(models: readonly WorkBuddyModelInfo[]): void;
1115
- /** Restore the static fallback, e.g. when the upstream stops answering. */
1116
- reset(): void;
1117
- /** Replace the user's selection; the adapter rebuilds from `visible()`. */
1118
- applySelection(selection: ModelSelection): void;
1119
- /** The selection currently in force, for the card's save round-trip. */
1120
- currentSelection(): ModelSelection;
1121
- onChange(listener: () => void): () => void;
1122
- find(id: string): WorkBuddyModelInfo | undefined;
1123
- /** Replace the catalog from the live upstream list; keeps the fallback if empty. */
1124
- updateFromUpstream(models: readonly WorkBuddyUpstreamModel[]): void;
1125
- private notify;
1280
+ interface PoolWebIgnoredAccount {
1281
+ /** Pool account id, the same key `PoolWebAccount.id` uses. */
1282
+ id: string;
1283
+ /** Human label captured at ignore time, so the row reads without a rescan. */
1284
+ label: string;
1285
+ /** ISO timestamp of when it was ignored. */
1286
+ ignoredAt: string;
1126
1287
  }
1127
- /** The user's model selection, as stored in the settings section. */
1128
- interface ModelSelection {
1129
- /** Absent = every model in the catalog is offered. */
1288
+ interface PoolWebModelSelection {
1289
+ /** Absent = every model is enabled. */
1130
1290
  enabledModelIds?: readonly string[];
1131
1291
  /** Absent = each model follows its upstream image capability. */
1132
1292
  imageModelIds?: readonly string[];
1133
1293
  /** Per-model context-window cap, keyed by model id. */
1134
1294
  contextBudgets?: Readonly<Record<string, number | undefined>>;
1135
1295
  }
1136
- //#endregion
1137
- //#region src/shim.d.ts
1138
- interface ShimLogger {
1139
- info?(...args: unknown[]): void;
1140
- warn(...args: unknown[]): void;
1141
- error(...args: unknown[]): void;
1142
- }
1143
- interface WorkBuddyShim {
1144
- ready: Promise<void>;
1145
- baseUrl(): string;
1146
- token(): string;
1147
- close(): Promise<void>;
1148
- }
1149
- interface WorkBuddyShimOptions {
1150
- pool: WorkBuddyAccountPool;
1151
- client: WorkBuddyUpstreamClient;
1152
- catalog: WorkBuddyCatalog;
1153
- logger?: ShimLogger;
1296
+ /** The JSON document the pool card renders. */
1297
+ interface PoolWebStatus {
1298
+ ok: boolean;
1299
+ accounts: readonly PoolWebAccount[];
1300
+ /** The next account the pool would use (rotation cursor). */
1301
+ activeAccountId?: string;
1302
+ cooling: number;
1303
+ models: readonly PoolWebModel[];
1304
+ /** The saved selection the card diffs its draft against. */
1305
+ selection: PoolWebModelSelection;
1154
1306
  /**
1155
- * Restrict this shim to one gateway. Two shims run side by side — one
1156
- * per region — and each must only ever draw accounts that belong to its
1157
- * own gateway. Absent means "every account" (a single-region deployment).
1307
+ * How the pool spreads requests: `priority` drains one account before
1308
+ * moving on, `round-robin` splits the spend evenly.
1158
1309
  */
1159
- region?: WorkBuddyRegion;
1160
- /** Max accounts to try per request before giving up. */
1161
- maxAttempts?: number;
1162
- }
1163
- export declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddyShim;
1164
- //#endregion
1165
- //#region src/adapter.d.ts
1166
- /** Provider route this bundle owns. */
1167
- /** Provider route this bundle owns for the domestic (CN) gateway. */
1168
- export declare const WORKBUDDY_POOL_PROVIDER = "workbuddy-xdpool";
1169
- interface WorkBuddyAdapterOptions {
1170
- shim: WorkBuddyShim;
1171
- catalog: WorkBuddyCatalog;
1172
- /** Plugin context; the pi-ai adapter reads `attachments`/`fs` from it. */
1173
- ctx: Context$1;
1174
- providerId?: string;
1175
- displayName?: string;
1176
- }
1177
- /** What {@link createWorkBuddyAdapter} hands back. */
1178
- interface WorkBuddyAdapter {
1179
- providerId: string;
1180
- displayName: string;
1181
- adapter: PiAiAdapter;
1182
- /** Rebuild the pi-ai model list from the current catalog. */
1183
- buildModels: () => Model<Api>[];
1184
- /** Rebuild the adapter's provider snapshot; call after a catalog update. */
1185
- invalidate: () => void;
1186
- }
1187
- /**
1188
- * Assemble the adapter. `getModels` re-reads the live catalog, and every
1189
- * model's `baseUrl` is re-resolved per read so the shim's ephemeral port
1190
- * applies from the first snapshot after startup. Call only after `shim.ready`.
1191
- */
1192
- export declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
1193
- //#endregion
1194
- //#region src/scheduler.d.ts
1195
- /** How often the loop wakes to look for a due job. */
1196
- /**
1197
- * How long to wait for event scoring to land before re-reading the task list.
1198
- *
1199
- * Scoring is asynchronous on the upstream side, so an immediate re-read still
1200
- * shows the old progress and the claim pass would skip a task that is in fact
1201
- * now claimable. Measured: the chain is reflected by about eight seconds.
1202
- */
1203
- export declare const EVENT_SCORE_WAIT_MS = 9000;
1204
- export declare const AUTOMATION_TICK_MS = 60000;
1205
- /** Which jobs the automation runs, and when. */
1206
- /**
1207
- * The persisted daily earnings ledger.
1208
- *
1209
- * `date` is the local day the counters belong to: a ledger from a previous day
1210
- * is discarded on load rather than carried forward as "today".
1211
- */
1212
- interface AutomationLedger {
1213
- date: string;
1214
- accounts: Record<string, AutomationAccountEarnings>;
1215
- }
1216
- interface AutomationOptions {
1217
- /** Master switch; false stops every job. */
1218
- enabled?: boolean;
1219
- /** Hour list for the daily check-in job. */
1220
- checkinHours?: readonly number[];
1221
- /** Hour list for the task-centre job (accept + claim). */
1222
- taskHours?: readonly number[];
1223
- /** Hour list for the activity-report job. */
1224
- reportHours?: readonly number[];
1225
- /** Hour list for the streak-redemption job. */
1226
- streakHours?: readonly number[];
1227
- /** Hour list for the buddy travel job. */
1228
- travelHours?: readonly number[];
1229
- /** Per-account serial delay, in milliseconds. */
1230
- accountDelayMs?: number;
1231
- /** Override the gap between expert chains, in milliseconds (tests use 0). */
1232
- expertGapMs?: number;
1233
- /** Override the event-scoring wait, in milliseconds (tests use 0). */
1234
- eventScoreWaitMs?: number;
1235
- /** Override the clock, for tests. */
1236
- now?: () => Date;
1237
- /** Logger; defaults to a no-op so tests stay quiet. */
1238
- logger?: SchedulerLogger;
1310
+ distribution: PoolDistribution;
1311
+ /** Which region this document describes. */
1312
+ region: PoolRegion;
1313
+ /** Every region holding at least one account, in display order. */
1314
+ regions: readonly PoolRegion[];
1315
+ shim: {
1316
+ running: boolean;
1317
+ baseUrl?: string;
1318
+ };
1319
+ /** Daily-points automation state, so the card can show what ran and when. */
1320
+ automation: PoolWebAutomation;
1321
+ /** Per-account credit floors currently in force, keyed by account id. */
1322
+ creditReserves: Readonly<Record<string, number>>;
1239
1323
  /**
1240
- * Persist the daily earnings ledger, and restore it on construction.
1324
+ * Accounts thrown out of the pool, in the order they were ignored.
1241
1325
  *
1242
- * The ledger cannot live only in memory: a host restart mid-day would wipe
1243
- * what the automation already earned, and the card would show nothing for
1244
- * rewards that really were collected.
1326
+ * Reported so the card can show the list and offer a way back. These accounts
1327
+ * are NOT in `accounts`: they are filtered out before their credentials are
1328
+ * read, which is the whole point of the feature.
1245
1329
  */
1246
- loadEarnings?: () => AutomationLedger | undefined;
1247
- saveEarnings?: (ledger: AutomationLedger) => void;
1248
- }
1249
- /** Logger surface, kept structural so any host logger fits. */
1250
- interface SchedulerLogger {
1251
- info?(...args: unknown[]): void;
1252
- warn?(...args: unknown[]): void;
1253
- }
1254
- /** One job's last-run record, surfaced on the status document. */
1255
- interface AutomationJobState {
1256
- /** `YYYY-MM-DD` of the last completed run, or undefined if it never ran. */
1257
- lastRunDate?: string;
1330
+ ignored: readonly PoolWebIgnoredAccount[];
1258
1331
  /**
1259
- * The scheduled SLOT of the last run, as `YYYY-MM-DDTHH`.
1332
+ * Where THIS region's model list came from.
1260
1333
  *
1261
- * The tick de-duplicates on this rather than on the date: keying on the date
1262
- * alone caps every job at one run a day, which is wrong for a job with two
1263
- * time points — blocking the cat loop's second pass would leave the cat out
1264
- * until tomorrow. Per slot a job runs once in each configured hour, while a
1265
- * repeat tick inside the same hour is still refused.
1334
+ * `live` = fetched from the gateway (or a cached fetch survived a restart).
1335
+ * `fallback` = the built-in static table, which means the startup fetch
1336
+ * failed and the user is looking at a SHORTER, possibly stale roster — models
1337
+ * they had enabled can be missing from it entirely.
1338
+ *
1339
+ * Surfaced because the failure used to be log-only: the picker quietly lost
1340
+ * half its entries and nothing on screen said why, so it read as "the plugin
1341
+ * deleted my models".
1266
1342
  */
1267
- lastRunSlot?: string;
1343
+ catalogSource?: PoolWebCatalogSource;
1344
+ /** When the live catalog was last successfully fetched, ISO. */
1345
+ catalogUpdatedAt?: string;
1346
+ /** Why the last fetch failed, when it did. Redacted and length-capped. */
1347
+ catalogError?: string;
1348
+ }
1349
+ /** How a region's model list was obtained. */
1350
+ type PoolWebCatalogSource = 'live' | 'fallback';
1351
+ /** One automation job's last run, as shown on the card. */
1352
+ interface PoolWebAutomationJob {
1353
+ /** `YYYY-MM-DD` of the last run in this process, if it has run. */
1354
+ lastRunDate?: string;
1268
1355
  /**
1269
- * Every configured slot consumed TODAY, as `YYYY-MM-DDTHH`.
1270
- *
1271
- * This is what actually gates a re-run, and it is a SET rather than a single
1272
- * slot because one field could not express the contract: a job configured for
1273
- * `[9, 21]` runs twice, and a single "last slot" value can only remember one
1274
- * of them — the second run erased the first, so the 9 o'clock candidate looked
1275
- * unconsumed again and the job re-fired every hour after 21:00.
1276
- *
1277
- * The values are the CONFIGURED hours that were spent, never the clock time
1278
- * the run happened to finish at. Storing the finish time was the original
1279
- * defect: the reader compared it against a configured candidate, so the two
1280
- * almost never matched and the gate stayed open all day.
1356
+ * Epoch ms of the last run, so the card can show the TIME.
1281
1357
  *
1282
- * In-memory like the rest of `states`: this is a per-process record, and the
1283
- * catch-up design deliberately re-runs an hour that passed while DSH was
1284
- * closed. See `automationEarnings` for the ledger that DOES persist.
1358
+ * Carried because a date-only stamp cannot tell one run from eight: every
1359
+ * repeat inside the same day rendered as the identical `2026-09-28 · 2`,
1360
+ * which is what kept a "re-runs every hour" defect invisible on the card.
1285
1361
  */
1286
- firedSlots?: readonly string[];
1362
+ lastRunAtMs?: number;
1287
1363
  /**
1288
- * The clock slot the last run STARTED in (`YYYY-MM-DDTHH`).
1364
+ * Configured slots consumed today, as `YYYY-MM-DDTHH`.
1289
1365
  *
1290
- * Separate from {@link firedSlots} on purpose: this is a throttle ("do not
1291
- * start twice inside the same hour"), while `firedSlots` is the schedule
1292
- * ledger. Conflating the two is what let a catch-up run at 13:00 erase the
1293
- * record of the 9 o'clock slot.
1366
+ * Shown so "which of today's hours already ran" is answerable at a glance
1367
+ * rather than inferred from a counter.
1294
1368
  */
1295
- lastFiredHour?: string;
1296
- /** Epoch ms of the last completed run. */
1297
- lastRunAtMs?: number;
1298
- /** Accounts that completed without throwing. */
1369
+ firedSlots?: readonly string[];
1370
+ /** Accounts that finished without error on the last run. */
1299
1371
  ok: number;
1300
- /** Accounts that threw (each one skipped, the run continued). */
1372
+ /** Accounts that failed on the last run (each one skipped, the run continued). */
1301
1373
  failed: number;
1302
1374
  /** Credits claimed by the task job on the last run. */
1303
1375
  credit: number;
@@ -1305,843 +1377,865 @@ interface AutomationJobState {
1305
1377
  energy: number;
1306
1378
  /** Tasks claimed by the task job on the last run. */
1307
1379
  claimed: number;
1308
- /** Human-readable summary of the last run. */
1380
+ /** One-line summary of the last run. */
1381
+ message?: string;
1309
1382
  /**
1310
- * What this run actually did, in the words of the task board.
1383
+ * What the last run actually did, in the words of the task board.
1311
1384
  *
1312
- * `message` is a count ("3 accounts, 5 tasks claimed"); this is the list a
1313
- * person can check off — the reward titles the pass collected. A row showing
1314
- * only a bare number cannot answer "did it do the thing I care about", which
1315
- * is the question the panel exists to answer.
1385
+ * `message` is a count; this is the list a person can check off, which is
1386
+ * what turns a row from "it ran" into "it did the things I care about".
1316
1387
  */
1317
1388
  detail?: readonly string[];
1318
- /**
1319
- * A pending milestone worth naming, when there is one.
1320
- *
1321
- * Streak tiers are why this exists: every tier reads `locked` until enough
1322
- * consecutive days accumulate, and "locked" on its own reads as "broken"
1323
- * rather than "come back in four days".
1324
- */
1389
+ /** A pending milestone worth naming, e.g. the next streak tier countdown. */
1325
1390
  progress?: string;
1326
- message?: string;
1327
1391
  }
1328
1392
  /**
1329
- * What the automation earned for ONE account today.
1393
+ * Automation block on the status document.
1330
1394
  *
1331
- * Reset at the local day boundary alongside the per-job "already ran today"
1332
- * guard, so the card shows today rather than a running total that never
1333
- * answers "did it do anything for this account recently".
1395
+ * Carries the schedule and each job's last outcome so the card can answer
1396
+ * "is it on, when does it run, and what did it last do" without reaching into
1397
+ * the scheduler itself.
1334
1398
  */
1335
- interface AutomationAccountEarnings {
1336
- /** Credits the automation claimed from the task centre today. */
1399
+ interface PoolWebAutomation {
1400
+ /** Master switch, mirrored from the saved config. */
1401
+ enabled: boolean;
1402
+ /** Whether the loop is currently running. */
1403
+ running: boolean;
1404
+ /** Configured hours per job, so the card can show the schedule. */
1405
+ checkinHours: readonly number[];
1406
+ reportHours: readonly number[];
1407
+ taskHours: readonly number[];
1408
+ streakHours: readonly number[];
1409
+ travelHours: readonly number[];
1410
+ jobs: {
1411
+ checkin: PoolWebAutomationJob;
1412
+ report: PoolWebAutomationJob;
1413
+ tasks: PoolWebAutomationJob;
1414
+ streak: PoolWebAutomationJob;
1415
+ travel: PoolWebAutomationJob;
1416
+ };
1417
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
1418
+ claimableSeen: number;
1419
+ /**
1420
+ * Whether a manual run is in flight. The card polls this to know when to
1421
+ * stop showing progress and report the result.
1422
+ */
1423
+ runInProgress: boolean;
1424
+ /**
1425
+ * Per-account credits/energy/tasks the automation earned TODAY, keyed by
1426
+ * account id. An account that earned nothing is simply absent, so the card
1427
+ * can say "nothing yet" instead of showing a bare zero.
1428
+ */
1429
+ earningsToday: Readonly<Record<string, PoolWebAutomationEarnings>>;
1430
+ }
1431
+ /** Today's automation take for one account. */
1432
+ interface PoolWebAutomationEarnings {
1433
+ /** Credits claimed from the task centre today. */
1337
1434
  credit: number;
1338
1435
  /** Energy claimed from the task centre today. */
1339
1436
  energy: number;
1340
1437
  /** Tasks claimed today. */
1341
1438
  claimed: number;
1342
- /**
1343
- * Credits the automation collected from check-in today.
1344
- *
1345
- * Kept separate from `credit` because they are different achievements and the
1346
- * card shows them on their own lines: "the automation claimed 3 tasks" and
1347
- * "the automation checked in" are not the same claim to the user.
1348
- */
1439
+ /** Credits collected from check-in today. */
1349
1440
  checkinCredit: number;
1350
- /** Credits from streak redemption + lottery today. */
1441
+ /** Credits from streak redemption and the lottery today. */
1351
1442
  bonusCredit: number;
1352
- /** Credits from the buddy adoption / travel loop today. */
1443
+ /** Credits from buddy adoption and the travel loop today. */
1353
1444
  travelCredit: number;
1354
- /** Local date the counters belong to. */
1445
+ /** Local date the counters belong to (YYYY-MM-DD). */
1355
1446
  date: string;
1356
1447
  }
1357
- /** What ONE account gained during a single run. */
1358
- interface AutomationAccountGain {
1359
- credit: number;
1360
- energy: number;
1361
- claimed: number;
1362
- checkinCredit: number;
1363
- bonusCredit: number;
1364
- travelCredit: number;
1365
- }
1366
- /** Totals from running the whole ordered pass at once. */
1367
- interface AutomationRunSummary {
1368
- /** How many jobs actually ran (a job with no hour is skipped). */
1369
- jobsRun: number;
1370
- /** Accounts that finished without error, summed across jobs. */
1371
- okCount: number;
1372
- /** Accounts that failed, summed across jobs. */
1373
- failed: number;
1374
- credit: number;
1375
- energy: number;
1376
- claimed: number;
1377
- /**
1378
- * What each account gained during THIS run, keyed by account id. Only
1379
- * accounts that gained something appear.
1380
- */
1381
- accounts: Readonly<Record<string, AutomationAccountGain>>;
1382
- }
1383
- /** Automation snapshot for the status document and the card. */
1384
- interface AutomationStatus {
1385
- enabled: boolean;
1386
- /** Whether the loop is running. */
1387
- running: boolean;
1388
- checkinHours: readonly number[];
1389
- taskHours: readonly number[];
1390
- reportHours: readonly number[];
1391
- streakHours: readonly number[];
1392
- travelHours: readonly number[];
1393
- jobs: {
1394
- checkin: AutomationJobState;
1395
- report: AutomationJobState;
1396
- tasks: AutomationJobState;
1397
- streak: AutomationJobState;
1398
- travel: AutomationJobState;
1399
- };
1400
- /** Claimable tasks seen on the most recent task pass, across accounts. */
1401
- claimableSeen: number;
1402
- /**
1403
- * Per-account totals for today, keyed by account id. Only accounts that
1404
- * actually earned something appear, so the card can render "no earnings"
1405
- * as an absence rather than a zero it has to explain.
1406
- */
1407
- earningsToday: Readonly<Record<string, AutomationAccountEarnings>>;
1408
- /**
1409
- * Whether a manual run is in flight right now.
1410
- *
1411
- * A run takes tens of seconds (one upstream call per account per job, plus
1412
- * the scoring wait), which is far too long for the card to hold a request
1413
- * open. The button starts a run and the panel polls this flag instead.
1414
- */
1415
- runInProgress: boolean;
1416
- }
1417
- /** The three plus one job kinds, in a stable order. */
1418
- type AutomationJobKind = 'checkin' | 'tasks' | 'report' | 'streak' | 'travel';
1419
- /** The four jobs in the order a tick runs them: report before tasks, always. */
1420
- export declare const AUTOMATION_JOB_KINDS: readonly AutomationJobKind[];
1421
- /** Reject anything that is not a job kind, so a route cannot name an unknown job. */
1422
- export declare function isAutomationJobKind(value: unknown): value is AutomationJobKind;
1423
- export declare function dayKey(date: Date, timeZone?: string): string;
1424
1448
  /**
1425
- * Whether `now`'s local hour is one of `hours`.
1449
+ * The two gateways, matching the provider ids the host registers. `cn` is the
1450
+ * domestic gateway (`copilot.tencent.com` / `codebuddy.cn`); `global` is the
1451
+ * international one (`workbuddy.ai`).
1452
+ */
1453
+ type PoolRegion = 'cn' | 'global';
1454
+ /** How the pool spreads requests across its accounts. */
1455
+ type PoolDistribution = 'priority' | 'round-robin' | 'balanced';
1456
+ /**
1457
+ * The schedule every automation job falls back to.
1426
1458
  *
1427
- * The reference panel computes a `nextFire` instant and sleeps until it; this
1428
- * loop instead wakes every minute and asks "is any job due now". Both fire at
1429
- * the top of a configured hour, but the polling form cannot miss a slot to a
1430
- * suspended process — a laptop that slept through 10:00 still runs the job the
1431
- * moment it wakes, on the same day.
1459
+ * Shared by both halves on purpose. The host uses it when a configured hour
1460
+ * list arrives empty (the settings schema materializes "never configured" into
1461
+ * `[]`), and the card uses it when it writes the `automation` block back, so a
1462
+ * document that already holds an empty list is healed instead of being saved
1463
+ * back as an unrunnable schedule.
1464
+ *
1465
+ * This lives here rather than in `scheduler.ts` because the browser half cannot
1466
+ * import the host module: `scheduler.ts` pulls in `node:crypto` and the whole
1467
+ * upstream client, none of which exists in the browser bundle. Two hand-written
1468
+ * copies would drift, and the drift is invisible — the card would write a
1469
+ * schedule the scheduler does not run.
1432
1470
  */
1433
- export declare function isFireHour(now: Date, hours: readonly number[]): boolean;
1471
+ export declare const DEFAULT_AUTOMATION_HOURS: {
1472
+ readonly checkin: readonly [9];
1473
+ readonly report: readonly [10];
1474
+ readonly tasks: readonly [11];
1475
+ readonly streak: readonly [12];
1476
+ readonly travel: readonly [9, 21];
1477
+ };
1478
+ //#endregion
1479
+ //#region src/catalog.d.ts
1480
+ /** One model the provider exposes. */
1481
+ interface WorkBuddyModelInfo {
1482
+ id: string;
1483
+ /** Display name; the multiplier is appended for the picker. */
1484
+ name: string;
1485
+ contextWindow: number;
1486
+ maxOutputTokens: number;
1487
+ /** Relative credit cost, e.g. 0.79 for `x0.79`. */
1488
+ multiplier?: number;
1489
+ /** Upstream-declared thinking levels. */
1490
+ supportedEfforts?: readonly string[];
1491
+ supportsImages: boolean;
1492
+ /** Upstream tags: free / limited-free / night-discount. */
1493
+ tags?: readonly string[];
1494
+ }
1434
1495
  /**
1435
- * The points automation.
1496
+ * Static fallback used before the first live catalog fetch, and whenever the
1497
+ * upstream cannot be reached.
1436
1498
  *
1437
- * Owns a single timer loop. Construction is inert — nothing runs until
1438
- * {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
1439
- * twice is harmless.
1499
+ * This table is the DOMESTIC (CN) roster, and it is kept in step with what
1500
+ * `copilot.tencent.com` actually advertises. It matters more than a "just in
1501
+ * case" list usually would: a failed startup fetch falls back to it, and the
1502
+ * user sees that as "half my models were deleted" — `deepseek-v4.1-flash`
1503
+ * and friends simply vanishing from the picker, with no visible explanation.
1504
+ *
1505
+ * Every row below was captured from the live endpoint, INCLUDING the window
1506
+ * sizes: the previous version carried a 32K window for `hy3` when the gateway
1507
+ * says 192K, and listed `kimi-k3` under an id the gateway no longer uses
1508
+ * (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
1509
+ * authoritative while being wrong.
1510
+ *
1511
+ * `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
1512
+ * which is what turns on the free badge.
1440
1513
  */
1441
- export declare class WorkBuddyScheduler {
1442
- private readonly pool;
1443
- private readonly client;
1444
- private readonly logger;
1445
- private readonly now;
1446
- private readonly delayMs;
1447
- /**
1448
- * How long to wait for event scoring before re-reading the task list.
1449
- * Tests set 0 so a pass does not spend nine real seconds per account.
1450
- */
1451
- private readonly eventScoreWaitMs;
1452
- /**
1453
- * Gap between two expert summon chains.
1454
- * Tests set 0 so a pass does not spend six real seconds per expert.
1455
- */
1456
- private readonly expertGapMs;
1457
- private enabled;
1458
- private checkinHours;
1459
- private taskHours;
1460
- private reportHours;
1461
- private streakHours;
1462
- private travelHours;
1463
- private timer;
1464
- private running;
1465
- /** Guards against a slow run overlapping the next tick. */
1466
- private busy;
1467
- /** True while a manual run is in flight, so the card can poll it. */
1468
- private runInFlight;
1469
- /**
1470
- * Set once {@link stop} is called.
1471
- *
1472
- * Deliberately false before `start`: the loop is not running yet, but a
1473
- * manual `tick` must still work. `stop` is what makes a run abandon the
1474
- * accounts it has not reached yet.
1475
- */
1476
- private stopped;
1477
- private readonly states;
1478
- private claimableSeen;
1479
- /**
1480
- * Credits/energy/tasks earned per account TODAY, keyed by account id.
1481
- *
1482
- * Cleared whenever the day key rolls over, so the card always answers
1483
- * "what did the automation get for THIS account today".
1484
- */
1485
- private earnings;
1486
- /** Day key the counters above belong to. */
1487
- private earningsDate;
1488
- /** Host hooks that persist the ledger across restarts. */
1489
- private readonly loadEarnings;
1490
- private saveEarningsFn;
1491
- constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
1492
- /** Apply a new configuration; safe to call while running. */
1514
+ export declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
1515
+ /** Live catalog with a static fallback behind it. */
1516
+ export declare class WorkBuddyCatalog {
1517
+ private models;
1518
+ private listeners;
1519
+ /** User's model selection. Empty object = follow the catalog unfiltered. */
1520
+ private selection;
1493
1521
  /**
1494
- * Install the persistence hook once the host settings service is available.
1522
+ * Whether `models` came from the gateway or from the static table.
1495
1523
  *
1496
- * Separate from the constructor because the scheduler is built with the pool,
1497
- * long before the settings section exists; a ledger written before that point
1498
- * would have nowhere to go.
1524
+ * Tracked so the card can SAY which it is showing. A failed fetch used to be
1525
+ * invisible on screen: the picker simply held fewer models than before, which
1526
+ * the user reasonably read as "the plugin deleted my models" rather than "the
1527
+ * network hiccuped at startup".
1499
1528
  */
1500
- setEarningsPersistence(save: (ledger: AutomationLedger) => void | Promise<void>): void;
1529
+ private source;
1530
+ /** When the live list last landed. */
1531
+ private lastUpdatedAt;
1532
+ /** Why the last fetch failed, for display. */
1533
+ private lastError;
1534
+ current(): readonly WorkBuddyModelInfo[];
1501
1535
  /**
1502
- * Fold a previously persisted ledger back in, when it belongs to today.
1536
+ * The models DSH should actually offer, after applying the user's selection:
1537
+ * disabled models are dropped, an explicit image list overrides the upstream
1538
+ * capability flag, and a per-model budget caps the advertised window.
1503
1539
  *
1504
- * Used after the settings document becomes readable, which happens after
1505
- * construction; a ledger from an earlier day is ignored so the counters never
1506
- * claim yesterday as today.
1540
+ * An absent `enabledModelIds` means "everything" — a fresh install with no
1541
+ * saved selection must not present an empty picker.
1507
1542
  */
1508
- applyEarningsLedger(ledger: AutomationLedger): void;
1509
- applyConfig(options: AutomationOptions): void;
1510
- /** Hours for one job, used by the loop and the status document. */
1511
- private hoursOf;
1512
- /** Start the loop. Idempotent. */
1513
- start(): void;
1514
- /** Stop the loop. Idempotent, and safe before `start`. */
1515
- stop(): void;
1516
- /** Snapshot for the status document. */
1543
+ visible(): readonly WorkBuddyModelInfo[];
1544
+ /** Replace the catalog and notify the adapter to rebuild its model list. */
1545
+ update(models: readonly WorkBuddyModelInfo[]): void;
1546
+ /** Restore the static fallback, e.g. when the upstream stops answering. */
1547
+ reset(): void;
1548
+ /** Replace the user's selection; the adapter rebuilds from `visible()`. */
1549
+ applySelection(selection: ModelSelection): void;
1550
+ /** The selection currently in force, for the card's save round-trip. */
1551
+ currentSelection(): ModelSelection;
1517
1552
  /**
1518
- * Run one job immediately, regardless of the clock.
1519
- *
1520
- * Exists so the automation can be verified from the card without waiting for
1521
- * its hour. A manual run is recorded exactly like a scheduled one, so the
1522
- * timer will not repeat it later the same day: every job is idempotent, but a
1523
- * second pass would still be wasted upstream calls.
1553
+ * Whether this catalog is serving live data or the built-in table.
1524
1554
  *
1525
- * `force` ignores the already-ran-today guard, which is what pressing the
1526
- * button a second time means.
1555
+ * `fallback` is not an error state, but it IS a degraded one: the user is
1556
+ * looking at a shorter roster than the gateway offers, so the card says so
1557
+ * and offers a retry instead of letting them wonder where the models went.
1527
1558
  */
1528
- runNow(kind: AutomationJobKind, force?: boolean): Promise<AutomationJobState>;
1559
+ currentSource(): PoolWebCatalogSource;
1529
1560
  /**
1530
- * Run every job once, in the scheduled order.
1561
+ * When the live list last landed, if it ever did.
1531
1562
  *
1532
- * Order matters and is not configurable: the activity report has to land
1533
- * before the task pass reads task progress, or the pass sees counters the
1534
- * report would have moved. This is what the card's single button calls.
1563
+ * Named `catalogUpdatedAt()` rather than `updatedAt()` because the class
1564
+ * already had an `updatedAt` member; two members of the same name is a
1565
+ * compile error, and the awkwardness is a useful signal that the concept is
1566
+ * "when THIS catalog was refreshed", not a generic timestamp.
1535
1567
  */
1568
+ catalogUpdatedAt(): string | undefined;
1569
+ /** Why the last fetch failed, if it did. */
1570
+ lastFetchError(): string | undefined;
1571
+ onChange(listener: () => void): () => void;
1572
+ find(id: string): WorkBuddyModelInfo | undefined;
1573
+ /** Replace the catalog from the live upstream list; keeps the fallback if empty. */
1574
+ updateFromUpstream(models: readonly WorkBuddyUpstreamModel[]): void;
1536
1575
  /**
1537
- * Start a full pass in the background and return immediately.
1538
- *
1539
- * A pass takes tens of seconds - one upstream round trip per account per job,
1540
- * plus the scoring wait - which is far too long to hold the card request open:
1541
- * the browser or the host web server would time out, and the user would see
1542
- * a hung button for a run that is actually working.
1576
+ * Record that a fetch attempt failed, leaving the current list in place.
1543
1577
  *
1544
- * Returns whether a run started. A second call while one is in flight is
1545
- * ignored rather than queued: pressing the button twice means hurry up, and
1546
- * the run already under way covers it.
1547
- */
1548
- startRunAll(): boolean;
1549
- runAll(): Promise<AutomationRunSummary>;
1550
- status(): AutomationStatus;
1551
- /**
1552
- * One poll: run every due job, serially.
1553
- *
1554
- * Serial by design — the jobs share the same accounts and the upstream
1555
- * rate-limits per account, so overlapping passes would only trip that limit.
1556
- * A job that throws is recorded and the loop continues.
1578
+ * The list is deliberately NOT reset here: a refresh that fails should keep
1579
+ * whatever working catalog is already loaded, rather than demoting a healthy
1580
+ * session to the static table because one retry ran out.
1557
1581
  */
1582
+ noteFetchFailure(message: string): void;
1583
+ private notify;
1584
+ }
1585
+ /** The user's model selection, as stored in the settings section. */
1586
+ interface ModelSelection {
1587
+ /** Absent = every model in the catalog is offered. */
1588
+ enabledModelIds?: readonly string[];
1589
+ /** Absent = each model follows its upstream image capability. */
1590
+ imageModelIds?: readonly string[];
1591
+ /** Per-model context-window cap, keyed by model id. */
1592
+ contextBudgets?: Readonly<Record<string, number | undefined>>;
1593
+ }
1594
+ //#endregion
1595
+ //#region src/shim.d.ts
1596
+ interface ShimLogger {
1597
+ info?(...args: unknown[]): void;
1598
+ warn(...args: unknown[]): void;
1599
+ error(...args: unknown[]): void;
1600
+ }
1601
+ interface WorkBuddyShim {
1602
+ ready: Promise<void>;
1603
+ baseUrl(): string;
1604
+ token(): string;
1605
+ close(): Promise<void>;
1606
+ }
1607
+ interface WorkBuddyShimOptions {
1608
+ pool: WorkBuddyAccountPool;
1609
+ client: WorkBuddyUpstreamClient;
1610
+ catalog: WorkBuddyCatalog;
1611
+ logger?: ShimLogger;
1558
1612
  /**
1559
- * Whether `kind` is due at `now`: its earliest configured hour has passed in
1560
- * the scheduling timezone, and no hour of today has been consumed yet.
1561
- *
1562
- * Hours are consumed per SLOT (one entry per configured hour), so a job with
1563
- * two hours still runs twice a day — but a job whose hour passed while DSH was
1564
- * closed runs immediately on the next tick instead of waiting for tomorrow.
1613
+ * Restrict this shim to one gateway. Two shims run side by side — one
1614
+ * per region — and each must only ever draw accounts that belong to its
1615
+ * own gateway. Absent means "every account" (a single-region deployment).
1565
1616
  */
1617
+ region?: WorkBuddyRegion;
1618
+ /** Max accounts to try per request before giving up. */
1619
+ maxAttempts?: number;
1620
+ }
1621
+ export declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddyShim;
1622
+ //#endregion
1623
+ //#region src/adapter.d.ts
1624
+ /** Provider route this bundle owns. */
1625
+ /** Provider route this bundle owns for the domestic (CN) gateway. */
1626
+ export declare const WORKBUDDY_POOL_PROVIDER = "workbuddy-xdpool";
1627
+ interface WorkBuddyAdapterOptions {
1628
+ shim: WorkBuddyShim;
1629
+ catalog: WorkBuddyCatalog;
1630
+ /** Plugin context; the pi-ai adapter reads `attachments`/`fs` from it. */
1631
+ ctx: Context$1;
1632
+ providerId?: string;
1633
+ displayName?: string;
1634
+ }
1635
+ /** What {@link createWorkBuddyAdapter} hands back. */
1636
+ interface WorkBuddyAdapter {
1637
+ providerId: string;
1638
+ displayName: string;
1639
+ adapter: PiAiAdapter;
1640
+ /** Rebuild the pi-ai model list from the current catalog. */
1641
+ buildModels: () => Model<Api>[];
1642
+ /** Rebuild the adapter's provider snapshot; call after a catalog update. */
1643
+ invalidate: () => void;
1644
+ }
1645
+ /**
1646
+ * Assemble the adapter. `getModels` re-reads the live catalog, and every
1647
+ * model's `baseUrl` is re-resolved per read so the shim's ephemeral port
1648
+ * applies from the first snapshot after startup. Call only after `shim.ready`.
1649
+ */
1650
+ export declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
1651
+ //#endregion
1652
+ //#region src/scheduler.d.ts
1653
+ /** How often the loop wakes to look for a due job. */
1654
+ /**
1655
+ * How long to wait for event scoring to land before re-reading the task list.
1656
+ *
1657
+ * Scoring is asynchronous on the upstream side, so an immediate re-read still
1658
+ * shows the old progress and the claim pass would skip a task that is in fact
1659
+ * now claimable. Measured: the chain is reflected by about eight seconds.
1660
+ */
1661
+ export declare const EVENT_SCORE_WAIT_MS = 9000;
1662
+ export declare const AUTOMATION_TICK_MS = 60000;
1663
+ /** Which jobs the automation runs, and when. */
1664
+ /**
1665
+ * The persisted daily earnings ledger.
1666
+ *
1667
+ * `date` is the local day the counters belong to: a ledger from a previous day
1668
+ * is discarded on load rather than carried forward as "today".
1669
+ */
1670
+ interface AutomationLedger {
1671
+ date: string;
1672
+ accounts: Record<string, AutomationAccountEarnings>;
1673
+ }
1674
+ interface AutomationOptions {
1675
+ /** Master switch; false stops every job. */
1676
+ enabled?: boolean;
1677
+ /** Hour list for the daily check-in job. */
1678
+ checkinHours?: readonly number[];
1679
+ /** Hour list for the task-centre job (accept + claim). */
1680
+ taskHours?: readonly number[];
1681
+ /** Hour list for the activity-report job. */
1682
+ reportHours?: readonly number[];
1683
+ /** Hour list for the streak-redemption job. */
1684
+ streakHours?: readonly number[];
1685
+ /** Hour list for the buddy travel job. */
1686
+ travelHours?: readonly number[];
1687
+ /** Per-account serial delay, in milliseconds. */
1688
+ accountDelayMs?: number;
1689
+ /** Override the gap between expert chains, in milliseconds (tests use 0). */
1690
+ expertGapMs?: number;
1691
+ /** Override the event-scoring wait, in milliseconds (tests use 0). */
1692
+ eventScoreWaitMs?: number;
1693
+ /** Override the clock, for tests. */
1694
+ now?: () => Date;
1695
+ /** Logger; defaults to a no-op so tests stay quiet. */
1696
+ logger?: SchedulerLogger;
1566
1697
  /**
1567
- * The configured slot `kind` should consume at `now`, or undefined when none.
1568
- *
1569
- * Returns the SLOT STRING (not a boolean) because the caller must record the
1570
- * same value it acted on. Returning a boolean was the original bug's enabler:
1571
- * the tick asked "is it due", then `runJob` independently wrote "what time is
1572
- * it now" — and those two answers were almost never equal.
1573
- *
1574
- * CATCH-UP semantics are deliberate: a candidate fires once its hour has
1575
- * PASSED and its slot is still unconsumed, so a laptop that slept through
1576
- * 10:00 still runs the job when it wakes, on the same day.
1698
+ * Persist the daily earnings ledger, and restore it on construction.
1577
1699
  *
1578
- * The EARLIEST unconsumed candidate wins, which is what keeps a two-hour job
1579
- * (`travelHours: [9, 21]`) whole: consuming the earliest due slot leaves the
1580
- * later one for its own hour.
1581
- */
1582
- private dueSlot;
1583
- /** Today's consumed slots for one job, as a set. */
1584
- private firedSlotsOf;
1585
- /** Record one consumed slot on a job's state. */
1586
- private consumeSlot;
1587
- private tick;
1588
- /** Run one job against every eligible account and record the outcome. */
1589
- /**
1590
- * Add one account's take to today's counters, resetting first if the day
1591
- * rolled over. Called from the task pass, which is the only job that earns.
1700
+ * The ledger cannot live only in memory: a host restart mid-day would wipe
1701
+ * what the automation already earned, and the card would show nothing for
1702
+ * rewards that really were collected.
1592
1703
  */
1704
+ loadEarnings?: () => AutomationLedger | undefined;
1705
+ saveEarnings?: (ledger: AutomationLedger) => void;
1706
+ }
1707
+ /** Logger surface, kept structural so any host logger fits. */
1708
+ interface SchedulerLogger {
1709
+ info?(...args: unknown[]): void;
1710
+ warn?(...args: unknown[]): void;
1711
+ }
1712
+ /** One job's last-run record, surfaced on the status document. */
1713
+ interface AutomationJobState {
1714
+ /** `YYYY-MM-DD` of the last completed run, or undefined if it never ran. */
1715
+ lastRunDate?: string;
1593
1716
  /**
1594
- * Today's per-account earnings, as a plain object for the status document.
1717
+ * The scheduled SLOT of the last run, as `YYYY-MM-DDTHH`.
1595
1718
  *
1596
- * Rolls the day first so a status read just after midnight does not report
1597
- * yesterday's totals under today's date.
1598
- */
1599
- private earningsSnapshot;
1600
- /**
1601
- * Add one account take to today counters, resetting first if the day rolled
1602
- * over. Every source is tracked separately so the card can show what earned
1603
- * what, rather than one opaque total.
1719
+ * The tick de-duplicates on this rather than on the date: keying on the date
1720
+ * alone caps every job at one run a day, which is wrong for a job with two
1721
+ * time points — blocking the cat loop's second pass would leave the cat out
1722
+ * until tomorrow. Per slot a job runs once in each configured hour, while a
1723
+ * repeat tick inside the same hour is still refused.
1604
1724
  */
1605
- private recordEarnings;
1606
- /** Clear the per-account counters when the local day changes. */
1607
- private rollEarnings;
1725
+ lastRunSlot?: string;
1608
1726
  /**
1609
- * Write the ledger through the host hook, when one was supplied.
1727
+ * Every configured slot consumed TODAY, as `YYYY-MM-DDTHH`.
1610
1728
  *
1611
- * Best effort on purpose: a failed save must never abort a run that has
1612
- * already collected rewards, and the in-memory ledger keeps serving the card
1613
- * for the rest of the session either way.
1614
- */
1615
- private persistEarnings;
1616
- /**
1617
- * Run one job against every eligible account and record the outcome.
1729
+ * This is what actually gates a re-run, and it is a SET rather than a single
1730
+ * slot because one field could not express the contract: a job configured for
1731
+ * `[9, 21]` runs twice, and a single "last slot" value can only remember one
1732
+ * of them — the second run erased the first, so the 9 o'clock candidate looked
1733
+ * unconsumed again and the job re-fired every hour after 21:00.
1618
1734
  *
1619
- * `consumedSlot` is the configured slot this run spends (`dueSlot`'s answer).
1620
- * It is passed IN rather than recomputed here so the value recorded is exactly
1621
- * the value the schedule decided on — the defect this replaces wrote
1622
- * `slotKey(now)` instead, i.e. the clock time the run finished at, which
1623
- * almost never equals the configured candidate the gate had compared against.
1735
+ * The values are the CONFIGURED hours that were spent, never the clock time
1736
+ * the run happened to finish at. Storing the finish time was the original
1737
+ * defect: the reader compared it against a configured candidate, so the two
1738
+ * almost never matched and the gate stayed open all day.
1624
1739
  *
1625
- * The task job runs in TWO passes. The first sends the event chains that light
1626
- * up client-scored tasks; the second collects rewards. They are separate
1627
- * because scoring lands asynchronously — a chain sent and claimed within the
1628
- * same breath finds the task still un-scored — and because sending is fast
1629
- * while claiming wants the whole pool to have been lit up first. Splitting
1630
- * them costs one shared wait instead of one wait per account.
1740
+ * In-memory like the rest of `states`: this is a per-process record, and the
1741
+ * catch-up design deliberately re-runs an hour that passed while DSH was
1742
+ * closed. See `automationEarnings` for the ledger that DOES persist.
1631
1743
  */
1632
- private runJob;
1633
- /** Compose the one-line summary shown on the card. */
1634
- private summarise;
1744
+ firedSlots?: readonly string[];
1635
1745
  /**
1636
- * Accounts to run against, in pool order.
1746
+ * The clock slot the last run STARTED in (`YYYY-MM-DDTHH`).
1637
1747
  *
1638
- * Disabled accounts are excluded here rather than filtered by the caller so a
1639
- * card switch takes effect on the next pass without any event plumbing.
1748
+ * Separate from {@link firedSlots} on purpose: this is a throttle ("do not
1749
+ * start twice inside the same hour"), while `firedSlots` is the schedule
1750
+ * ledger. Conflating the two is what let a catch-up run at 13:00 erase the
1751
+ * record of the 9 o'clock slot.
1640
1752
  */
1641
- private accountsInOrder;
1753
+ lastFiredHour?: string;
1754
+ /** Epoch ms of the last completed run. */
1755
+ lastRunAtMs?: number;
1756
+ /** Accounts that completed without throwing. */
1757
+ ok: number;
1758
+ /** Accounts that threw (each one skipped, the run continued). */
1759
+ failed: number;
1760
+ /** Credits claimed by the task job on the last run. */
1761
+ credit: number;
1762
+ /** Energy claimed by the task job on the last run. */
1763
+ energy: number;
1764
+ /** Tasks claimed by the task job on the last run. */
1765
+ claimed: number;
1766
+ /** Human-readable summary of the last run. */
1642
1767
  /**
1643
- * Send one activity report, then verify it landed.
1768
+ * What this run actually did, in the words of the task board.
1644
1769
  *
1645
- * The upstream answers 200 even when it drops the event, so the streak is
1646
- * read back as the oracle: `days > 0` means it counted. A failed read-back is
1647
- * logged and treated as a suspicious result, never as a retry — the report is
1648
- * idempotent per day, and hammering it is exactly what the one-a-day quota
1649
- * exists to avoid.
1770
+ * `message` is a count ("3 accounts, 5 tasks claimed"); this is the list a
1771
+ * person can check off — the reward titles the pass collected. A row showing
1772
+ * only a bare number cannot answer "did it do the thing I care about", which
1773
+ * is the question the panel exists to answer.
1650
1774
  */
1651
- private reportOne;
1652
- private sendEventChains;
1775
+ detail?: readonly string[];
1653
1776
  /**
1654
- * Send one chain on the channel it was built for.
1777
+ * A pending milestone worth naming, when there is one.
1655
1778
  *
1656
- * The transport is not a detail of the sender: the scorer keys different
1657
- * tasks to different fingerprint families, so a web-scored event posted as a
1658
- * desktop event is accepted and then ignored.
1779
+ * Streak tiers are why this exists: every tier reads `locked` until enough
1780
+ * consecutive days accumulate, and "locked" on its own reads as "broken"
1781
+ * rather than "come back in four days".
1659
1782
  */
1660
- private sendChain;
1783
+ progress?: string;
1784
+ message?: string;
1785
+ }
1786
+ /**
1787
+ * What the automation earned for ONE account today.
1788
+ *
1789
+ * Reset at the local day boundary alongside the per-job "already ran today"
1790
+ * guard, so the card shows today rather than a running total that never
1791
+ * answers "did it do anything for this account recently".
1792
+ */
1793
+ interface AutomationAccountEarnings {
1794
+ /** Credits the automation claimed from the task centre today. */
1795
+ credit: number;
1796
+ /** Energy claimed from the task centre today. */
1797
+ energy: number;
1798
+ /** Tasks claimed today. */
1799
+ claimed: number;
1661
1800
  /**
1662
- * Build every chain that scores one task.
1801
+ * Credits the automation collected from check-in today.
1663
1802
  *
1664
- * Most tasks need a single chain; `template_5` needs five, because the scorer
1665
- * counts distinct `template_used` events rather than a boolean. The two tasks
1666
- * that join a conversation (skill, expert) open a real one first, which is why
1667
- * this is async.
1803
+ * Kept separate from `credit` because they are different achievements and the
1804
+ * card shows them on their own lines: "the automation claimed 3 tasks" and
1805
+ * "the automation checked in" are not the same claim to the user.
1668
1806
  */
1669
- private chainsFor;
1807
+ checkinCredit: number;
1808
+ /** Credits from streak redemption + lottery today. */
1809
+ bonusCredit: number;
1810
+ /** Credits from the buddy adoption / travel loop today. */
1811
+ travelCredit: number;
1812
+ /** Local date the counters belong to. */
1813
+ date: string;
1814
+ }
1815
+ /** What ONE account gained during a single run. */
1816
+ interface AutomationAccountGain {
1817
+ credit: number;
1818
+ energy: number;
1819
+ claimed: number;
1820
+ checkinCredit: number;
1821
+ bonusCredit: number;
1822
+ travelCredit: number;
1823
+ }
1824
+ /** Totals from running the whole ordered pass at once. */
1825
+ interface AutomationRunSummary {
1826
+ /** How many jobs actually ran (a job with no hour is skipped). */
1827
+ jobsRun: number;
1828
+ /** Accounts that finished without error, summed across jobs. */
1829
+ okCount: number;
1830
+ /** Accounts that failed, summed across jobs. */
1831
+ failed: number;
1832
+ credit: number;
1833
+ energy: number;
1834
+ claimed: number;
1670
1835
  /**
1671
- * The summon-and-use chains for the expert tasks.
1836
+ * What each account gained during THIS run, keyed by account id. Only
1837
+ * accounts that gained something appear.
1838
+ */
1839
+ accounts: Readonly<Record<string, AutomationAccountGain>>;
1840
+ }
1841
+ /** Automation snapshot for the status document and the card. */
1842
+ interface AutomationStatus {
1843
+ enabled: boolean;
1844
+ /** Whether the loop is running. */
1845
+ running: boolean;
1846
+ checkinHours: readonly number[];
1847
+ taskHours: readonly number[];
1848
+ reportHours: readonly number[];
1849
+ streakHours: readonly number[];
1850
+ travelHours: readonly number[];
1851
+ jobs: {
1852
+ checkin: AutomationJobState;
1853
+ report: AutomationJobState;
1854
+ tasks: AutomationJobState;
1855
+ streak: AutomationJobState;
1856
+ travel: AutomationJobState;
1857
+ };
1858
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
1859
+ claimableSeen: number;
1860
+ /**
1861
+ * Per-account totals for today, keyed by account id. Only accounts that
1862
+ * actually earned something appear, so the card can render "no earnings"
1863
+ * as an absence rather than a zero it has to explain.
1864
+ */
1865
+ earningsToday: Readonly<Record<string, AutomationAccountEarnings>>;
1866
+ /**
1867
+ * Whether a manual run is in flight right now.
1672
1868
  *
1673
- * Two steps per expert, and both are load-bearing: the summon events alone are
1674
- * impressions, and a use event on its own scores nothing because the scorer
1675
- * looks the conversation up. Only a real chat with `X-Expert-Id` produces an
1676
- * id it will accept.
1869
+ * A run takes tens of seconds (one upstream call per account per job, plus
1870
+ * the scoring wait), which is far too long for the card to hold a request
1871
+ * open. The button starts a run and the panel polls this flag instead.
1677
1872
  */
1678
- private expertChains;
1873
+ runInProgress: boolean;
1874
+ }
1875
+ /** The three plus one job kinds, in a stable order. */
1876
+ type AutomationJobKind = 'checkin' | 'tasks' | 'report' | 'streak' | 'travel';
1877
+ /** The four jobs in the order a tick runs them: report before tasks, always. */
1878
+ export declare const AUTOMATION_JOB_KINDS: readonly AutomationJobKind[];
1879
+ /** Reject anything that is not a job kind, so a route cannot name an unknown job. */
1880
+ export declare function isAutomationJobKind(value: unknown): value is AutomationJobKind;
1881
+ export declare function dayKey(date: Date, timeZone?: string): string;
1882
+ /**
1883
+ * Whether `now`'s local hour is one of `hours`.
1884
+ *
1885
+ * The reference panel computes a `nextFire` instant and sleeps until it; this
1886
+ * loop instead wakes every minute and asks "is any job due now". Both fire at
1887
+ * the top of a configured hour, but the polling form cannot miss a slot to a
1888
+ * suspended process — a laptop that slept through 10:00 still runs the job the
1889
+ * moment it wakes, on the same day.
1890
+ */
1891
+ export declare function isFireHour(now: Date, hours: readonly number[]): boolean;
1892
+ /**
1893
+ * The points automation.
1894
+ *
1895
+ * Owns a single timer loop. Construction is inert — nothing runs until
1896
+ * {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
1897
+ * twice is harmless.
1898
+ */
1899
+ export declare class WorkBuddyScheduler {
1900
+ private readonly pool;
1901
+ private readonly client;
1902
+ private readonly logger;
1903
+ private readonly now;
1904
+ private readonly delayMs;
1679
1905
  /**
1680
- * The 腾讯轻量云 expert chain.
1906
+ * How long to wait for event scoring before re-reading the task list.
1907
+ * Tests set 0 so a pass does not spend nine real seconds per account.
1908
+ */
1909
+ private readonly eventScoreWaitMs;
1910
+ /**
1911
+ * Gap between two expert summon chains.
1912
+ * Tests set 0 so a pass does not spend six real seconds per expert.
1913
+ */
1914
+ private readonly expertGapMs;
1915
+ private enabled;
1916
+ private checkinHours;
1917
+ private taskHours;
1918
+ private reportHours;
1919
+ private streakHours;
1920
+ private travelHours;
1921
+ private timer;
1922
+ private running;
1923
+ /** Guards against a slow run overlapping the next tick. */
1924
+ private busy;
1925
+ /** True while a manual run is in flight, so the card can poll it. */
1926
+ private runInFlight;
1927
+ /**
1928
+ * Set once {@link stop} is called.
1681
1929
  *
1682
- * Structurally the same as the expert task, with two differences the scorer
1683
- * checks: `agent_task_created` has to name the expert, and the use event has
1684
- * to report `mode: 'LOCAL'` with an empty type and zero cost — that is what
1685
- * the lighthouse criterion looks for.
1930
+ * Deliberately false before `start`: the loop is not running yet, but a
1931
+ * manual `tick` must still work. `stop` is what makes a run abandon the
1932
+ * accounts it has not reached yet.
1686
1933
  */
1687
- private lighthouseChains;
1934
+ private stopped;
1935
+ private readonly states;
1936
+ private claimableSeen;
1688
1937
  /**
1689
- * The task-centre pass for one account.
1938
+ * Credits/energy/tasks earned per account TODAY, keyed by account id.
1690
1939
  *
1691
- * Order matters: enrich first (enrol in everything open), then claim. Both
1692
- * halves are idempotent — accepting an already-accepted task succeeds, and a
1693
- * repeat claim answers `already_claimed` — so a pass that dies halfway is
1694
- * safe to replay on the next tick.
1940
+ * Cleared whenever the day key rolls over, so the card always answers
1941
+ * "what did the automation get for THIS account today".
1695
1942
  */
1696
- private runTasks;
1943
+ private earnings;
1944
+ /** Day key the counters above belong to. */
1945
+ private earningsDate;
1946
+ /** Host hooks that persist the ledger across restarts. */
1947
+ private readonly loadEarnings;
1948
+ private saveEarningsFn;
1949
+ constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
1950
+ /** Apply a new configuration; safe to call while running. */
1697
1951
  /**
1698
- * Streak redemption plus the lottery it unlocks.
1952
+ * Install the persistence hook once the host settings service is available.
1699
1953
  *
1700
- * Tiers unlock on consecutive active days (7/14/28). Redeeming one pays
1701
- * credits, energy, a makeup card and — the part nothing else grants — lottery
1702
- * draws, so the draw runs straight after and only for the chances in hand.
1954
+ * Separate from the constructor because the scheduler is built with the pool,
1955
+ * long before the settings section exists; a ledger written before that point
1956
+ * would have nowhere to go.
1957
+ */
1958
+ setEarningsPersistence(save: (ledger: AutomationLedger) => void | Promise<void>): void;
1959
+ /**
1960
+ * Fold a previously persisted ledger back in, when it belongs to today.
1703
1961
  *
1704
- * Everything here is idempotent: a tier already claimed is skipped by its
1705
- * status, and a draw consumes one chance, so a replay cannot double-spend.
1962
+ * Used after the settings document becomes readable, which happens after
1963
+ * construction; a ledger from an earlier day is ignored so the counters never
1964
+ * claim yesterday as today.
1706
1965
  */
1707
- private redeemStreak;
1966
+ applyEarningsLedger(ledger: AutomationLedger): void;
1967
+ applyConfig(options: AutomationOptions): void;
1968
+ /** Hours for one job, used by the loop and the status document. */
1969
+ private hoursOf;
1970
+ /** Start the loop. Idempotent. */
1971
+ start(): void;
1972
+ /** Stop the loop. Idempotent, and safe before `start`. */
1973
+ stop(): void;
1974
+ /** Snapshot for the status document. */
1708
1975
  /**
1709
- * One trip through the buddy travel loop for an account.
1976
+ * Run one job immediately, regardless of the clock.
1710
1977
  *
1711
- * A single pass advances the state machine by at most one step: a trip
1712
- * that has arrived is collected, and an idle buddy is sent out. A buddy
1713
- * already travelling is left alone — there is nothing to do until it lands.
1978
+ * Exists so the automation can be verified from the card without waiting for
1979
+ * its hour. A manual run is recorded exactly like a scheduled one, so the
1980
+ * timer will not repeat it later the same day: every job is idempotent, but a
1981
+ * second pass would still be wasted upstream calls.
1714
1982
  *
1715
- * Measured against the live upstream: the departed trip reports
1716
- * `dailyLimitReached` immediately, so the once-a-day limit needs no local
1717
- * bookkeeping.
1983
+ * `force` ignores the already-ran-today guard, which is what pressing the
1984
+ * button a second time means.
1718
1985
  */
1719
- private runTravel;
1720
- }
1721
- //#endregion
1722
- //#region src/status.d.ts
1723
- /** One account's status row. */
1724
- interface AccountStatus {
1725
- id: string;
1726
- label: string;
1727
- nickname?: string;
1728
- domain: string;
1729
- /** ISO timestamp when the access token expires. */
1730
- expiresAt?: string;
1731
- /** Account-wide cooldown (every model blocked). */
1732
- cooling: boolean;
1733
- cooldownUntil?: string;
1734
- /** Per-model cooldowns active right now (modelId → ISO until); the account
1735
- * itself is not `cooling` while only some models are limited. */
1736
- modelCooldowns?: readonly {
1737
- modelId: string;
1738
- until: string;
1739
- }[];
1740
- rateLimitHits: number;
1741
- /** Read-only aggregated credit summary for the account. */
1742
- credits?: WorkBuddyCredits;
1743
- creditsError?: string;
1744
- sourcePath: string;
1745
- }
1746
- /** Whole-plugin status document. */
1747
- interface WorkBuddyStatus {
1748
- ok: boolean;
1749
- accounts: AccountStatus[];
1750
- activeAccountId?: string;
1751
- cooling: number;
1752
- models: {
1753
- id: string;
1754
- name: string;
1755
- multiplier?: number;
1756
- tags?: readonly string[];
1757
- }[];
1758
- shim: {
1759
- running: boolean;
1760
- baseUrl?: string;
1761
- };
1762
- }
1763
- interface StatusOptions {
1764
- pool: WorkBuddyAccountPool;
1765
- /** The catalog to report. Regional callers pass their own region's. */
1766
- catalog: WorkBuddyCatalog;
1767
- client: WorkBuddyUpstreamClient;
1768
- shim?: {
1769
- running: boolean;
1770
- baseUrl?: string;
1771
- };
1772
- /** Query credits per account. Off for cheap diagnostics runs. */
1773
- includeCredits?: boolean;
1774
- }
1775
- /** Build the status document. Never throws. */
1776
- export declare function buildStatus(options: StatusOptions): Promise<WorkBuddyStatus>;
1777
- /** Format the status document for a terminal. */
1778
- export declare function formatStatus(status: WorkBuddyStatus): string;
1779
- /** Format the per-model credit multipliers. */
1780
- export declare function formatRates(status: WorkBuddyStatus): string;
1781
- //#endregion
1782
- //#region src/status-paths.d.ts
1783
- /**
1784
- * Node-free constants and types shared by the Host and browser halves of the
1785
- * WorkBuddy XD Pool settings card.
1786
- *
1787
- * Pool's runtime state already lives in `src/status.ts` (`buildStatus` /
1788
- * `WorkBuddyStatus`); this module only carves the cross-domain (Host→browser)
1789
- * JSON document into a shape that stays token-free and matches what the
1790
- * browser card renders. Route paths are plugin-owned and mounted on the Host's
1791
- * same-origin web server (see `src/web-status.ts`).
1792
- *
1793
- * @module dsh-workbuddy-xdpool/status-paths
1794
- */
1795
- /** Plugin-owned read-only pool status endpoint (account rows + models + shim). */
1796
- export declare const POOL_STATUS_PATH = "/plugins/dsh-workbuddy-xdpool/status";
1797
- /** Plugin-owned local account rescan endpoint (re-read desktop snapshots). */
1798
- export declare const POOL_RESCAN_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/rescan";
1799
- /** Plugin-owned cooldown reset endpoint (clear all 429 cooldowns). */
1800
- export declare const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset";
1801
- /** Plugin-owned daily check-in action endpoint (claim today's reward). */
1802
- export declare const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
1803
- /** Plugin-owned model-selection save endpoint (writes the settings section). */
1804
- export declare const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
1805
- /**
1806
- * Throw one account out of the pool for good, or take it back.
1807
- *
1808
- * Separate from the disable route because the semantics differ: disabling is a
1809
- * rotation preference the account survives, ignoring survives the account.
1810
- */
1811
- export declare const POOL_ACCOUNT_IGNORE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/ignored";
1812
- /** Run one automation job immediately, so the card can verify it on demand. */
1813
- export declare const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
1814
- /** Set or clear one account's reserved-credit floor. */
1815
- export declare const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
1816
- /** One account's row, token-free. */
1817
- interface PoolWebAccount {
1818
- id: string;
1819
- label: string;
1820
- nickname?: string;
1821
- domain: string;
1822
- /** ISO timestamp; absent when the credential carries no expiry. */
1823
- expiresAt?: string;
1824
- /** Account-wide cooldown (every model blocked); only after a no-model penalize. */
1825
- cooling: boolean;
1826
- /** ISO timestamp when the account-wide 429 cooldown lifts; only while cooling. */
1827
- cooldownUntil?: string;
1986
+ runNow(kind: AutomationJobKind, force?: boolean): Promise<AutomationJobState>;
1828
1987
  /**
1829
- * Per-model cooldowns currently active. The account is NOT `cooling` while a
1830
- * model is limited — its other models still serve — but each entry tells the
1831
- * card which model is out until when (e.g. `hy4-preview` cooling to 10:14,
1832
- * `hy3` normal).
1988
+ * Run every job once, in the scheduled order.
1989
+ *
1990
+ * Order matters and is not configurable: the activity report has to land
1991
+ * before the task pass reads task progress, or the pass sees counters the
1992
+ * report would have moved. This is what the card's single button calls.
1833
1993
  */
1834
- modelCooldowns?: ReadonlyArray<{
1835
- modelId: string;
1836
- until: string;
1837
- }>;
1838
1994
  /**
1839
- * Whether the user switched this account off. A disabled account never
1840
- * serves a request, but it stays listed so the card can switch it back on.
1995
+ * Start a full pass in the background and return immediately.
1996
+ *
1997
+ * A pass takes tens of seconds - one upstream round trip per account per job,
1998
+ * plus the scoring wait - which is far too long to hold the card request open:
1999
+ * the browser or the host web server would time out, and the user would see
2000
+ * a hung button for a run that is actually working.
2001
+ *
2002
+ * Returns whether a run started. A second call while one is in flight is
2003
+ * ignored rather than queued: pressing the button twice means hurry up, and
2004
+ * the run already under way covers it.
1841
2005
  */
1842
- disabled: boolean;
1843
- rateLimitHits: number;
2006
+ startRunAll(): boolean;
2007
+ runAll(): Promise<AutomationRunSummary>;
2008
+ status(): AutomationStatus;
1844
2009
  /**
1845
- * Credits the user asked to keep for this account. The pool stops picking the
1846
- * account once its balance reaches the reserve, so this many credits survive.
1847
- * 0 means the account may be spent down as before.
2010
+ * One poll: run every due job, serially.
2011
+ *
2012
+ * Serial by design — the jobs share the same accounts and the upstream
2013
+ * rate-limits per account, so overlapping passes would only trip that limit.
2014
+ * A job that throws is recorded and the loop continues.
1848
2015
  */
1849
- creditReserve: number;
1850
2016
  /**
1851
- * Whether the account is held back purely by its reserve right now. Kept
1852
- * distinct from `cooling`: a reserved account is healthy and simply
1853
- * protected, which is a different thing to tell the user than rate-limited.
2017
+ * Whether `kind` is due at `now`: its earliest configured hour has passed in
2018
+ * the scheduling timezone, and no hour of today has been consumed yet.
2019
+ *
2020
+ * Hours are consumed per SLOT (one entry per configured hour), so a job with
2021
+ * two hours still runs twice a day — but a job whose hour passed while DSH was
2022
+ * closed runs immediately on the next tick instead of waiting for tomorrow.
1854
2023
  */
1855
- reserved: boolean;
1856
2024
  /**
1857
- * What the automation earned for this account today. Absent when it earned
1858
- * nothing (or the automation never ran for it), so the card can stay quiet
1859
- * instead of printing a row of zeroes.
2025
+ * The configured slot `kind` should consume at `now`, or undefined when none.
2026
+ *
2027
+ * Returns the SLOT STRING (not a boolean) because the caller must record the
2028
+ * same value it acted on. Returning a boolean was the original bug's enabler:
2029
+ * the tick asked "is it due", then `runJob` independently wrote "what time is
2030
+ * it now" — and those two answers were almost never equal.
2031
+ *
2032
+ * CATCH-UP semantics are deliberate: a candidate fires once its hour has
2033
+ * PASSED and its slot is still unconsumed, so a laptop that slept through
2034
+ * 10:00 still runs the job when it wakes, on the same day.
2035
+ *
2036
+ * The EARLIEST unconsumed candidate wins, which is what keeps a two-hour job
2037
+ * (`travelHours: [9, 21]`) whole: consuming the earliest due slot leaves the
2038
+ * later one for its own hour.
1860
2039
  */
1861
- automationToday?: PoolWebAutomationEarnings;
1862
- /** ISO timestamp of the last successful use (best-effort pool bookkeeping). */
1863
- lastUsedAt?: string;
1864
- /** Aggregated credit summary for the account, read-only. */
1865
- credits?: PoolWebCredits;
1866
- creditsError?: string;
2040
+ private dueSlot;
2041
+ /** Today's consumed slots for one job, as a set. */
2042
+ private firedSlotsOf;
2043
+ /** Record one consumed slot on a job's state. */
2044
+ private consumeSlot;
2045
+ private tick;
2046
+ /** Run one job against every eligible account and record the outcome. */
1867
2047
  /**
1868
- * Today's check-in state for this account, read-only. Present only when the
1869
- * per-account check-in probe succeeded and the program is active. The card
1870
- * renders one claim button per account, so a multi-account pool can collect
1871
- * every account's daily reward without switching accounts by hand.
2048
+ * Add one account's take to today's counters, resetting first if the day
2049
+ * rolled over. Called from the task pass, which is the only job that earns.
1872
2050
  */
1873
- checkin?: PoolWebCheckin;
1874
- checkinError?: string;
1875
- }
1876
- /** One credit package (as surfaced by the pool's upstream client), node-free. */
1877
- interface PoolWebCreditPackage {
1878
- packageName: string;
1879
- remain?: number;
1880
- size?: number;
1881
- /** CapacityType 4 — refreshed each cycle and never expires. */
1882
- monthly?: boolean;
1883
- /** Next cycle refresh point, ms. */
1884
- cycleRefreshMs?: number;
1885
- /** One-off expiry, ms. */
1886
- expiresAtMs?: number;
1887
- }
1888
- /** Aggregated credit answer the card renders under one account. */
1889
- interface PoolWebCredits {
1890
- total?: number;
1891
- packages: readonly PoolWebCreditPackage[];
1892
- /** Credits expiring within 3 days. */
1893
- expiringSoon?: number;
1894
- /** When the nearest package expires, ms. */
1895
- nearestExpiryMs?: number;
1896
- }
1897
- /**
1898
- * Daily check-in state the card renders under one account's credits. Mirrors
1899
- * the upstream activity endpoint, minus anything the browser does not need.
1900
- */
1901
- interface PoolWebCheckin {
1902
- /** The activity is running; a claim button is offered only while true. */
1903
- active: boolean;
1904
- /** Already collected today — the button renders as a done state. */
1905
- todayCheckedIn: boolean;
1906
- /** Consecutive days checked in. */
1907
- streakDays: number;
1908
- /** Credits a single day grants. */
1909
- dailyCredit: number;
1910
- /** Credits collected today (0 before claiming). */
1911
- todayCredit: number;
1912
- /** Today is a streak milestone day. */
1913
- isStreakDay: boolean;
1914
- /** The day count the next milestone lands on. */
1915
- nextStreakDay: number;
1916
- /** Bonus credits granted on a milestone day. */
1917
- streakBonusCredit: number;
1918
- }
1919
- /** Result of one claim, so the card can confirm what was collected. */
1920
- interface PoolWebCheckinClaim {
1921
- credit: number;
1922
- streakDays: number;
1923
- isStreakDay: boolean;
1924
- }
1925
- /** One model the pool exposes to DSH, with cost / free tags. */
1926
- interface PoolWebModel {
1927
- id: string;
1928
- name: string;
1929
- /** Relative credit cost, e.g. 0.79 for x0.79. */
1930
- multiplier?: number;
1931
- /** Upstream tags: free / limited-free / night-discount. */
1932
- tags?: readonly string[];
1933
- /** Effective image support after the user's per-model toggle. */
1934
- supportsImages: boolean;
1935
- /** Effective context window after the user's budget cap. */
1936
- contextWindow: number;
1937
- /** The window the upstream advertises, before any cap. */
1938
- nativeContextWindow: number;
1939
- /** Upstream output ceiling, so the card can show both limits. */
1940
- maxOutputTokens: number;
1941
- /** Thinking levels the upstream declares, when it declares any. */
1942
- supportedEfforts?: readonly string[];
1943
- /** Whether this model is currently enabled in the picker. */
1944
- enabled: boolean;
1945
- }
1946
- /**
1947
- * Body of the account ignore/unignore route: exactly one account per request.
1948
- *
1949
- * `ignored: true` throws the account out of the pool for good (its credential is
1950
- * not even read on the next scan, and a fresh desktop sign-in will not bring it
1951
- * back). `false` restores it, at which point the next scan discovers it again.
1952
- */
1953
- interface PoolWebAccountIgnore {
1954
- /** Pool account id, as reported in `PoolWebAccount.id`. */
1955
- accountId: string;
1956
- /** `true` ignores the account permanently; `false` takes it back. */
1957
- ignored: boolean;
1958
- }
1959
- /**
1960
- * One account the user has thrown out of the pool.
1961
- *
1962
- * Kept on the status document so the card can list what was ignored and offer a
1963
- * way back: without that, "ignored" is a one-way door the user cannot inspect or
1964
- * undo from the UI, which is how a hidden list becomes a support burden.
1965
- */
1966
- interface PoolWebIgnoredAccount {
1967
- /** Pool account id, the same key `PoolWebAccount.id` uses. */
1968
- id: string;
1969
- /** Human label captured at ignore time, so the row reads without a rescan. */
1970
- label: string;
1971
- /** ISO timestamp of when it was ignored. */
1972
- ignoredAt: string;
1973
- }
1974
- interface PoolWebModelSelection {
1975
- /** Absent = every model is enabled. */
1976
- enabledModelIds?: readonly string[];
1977
- /** Absent = each model follows its upstream image capability. */
1978
- imageModelIds?: readonly string[];
1979
- /** Per-model context-window cap, keyed by model id. */
1980
- contextBudgets?: Readonly<Record<string, number | undefined>>;
1981
- }
1982
- /** The JSON document the pool card renders. */
1983
- interface PoolWebStatus {
1984
- ok: boolean;
1985
- accounts: readonly PoolWebAccount[];
1986
- /** The next account the pool would use (rotation cursor). */
1987
- activeAccountId?: string;
1988
- cooling: number;
1989
- models: readonly PoolWebModel[];
1990
- /** The saved selection the card diffs its draft against. */
1991
- selection: PoolWebModelSelection;
1992
2051
  /**
1993
- * How the pool spreads requests: `priority` drains one account before
1994
- * moving on, `round-robin` splits the spend evenly.
2052
+ * Today's per-account earnings, as a plain object for the status document.
2053
+ *
2054
+ * Rolls the day first so a status read just after midnight does not report
2055
+ * yesterday's totals under today's date.
2056
+ */
2057
+ private earningsSnapshot;
2058
+ /**
2059
+ * Add one account take to today counters, resetting first if the day rolled
2060
+ * over. Every source is tracked separately so the card can show what earned
2061
+ * what, rather than one opaque total.
2062
+ */
2063
+ private recordEarnings;
2064
+ /** Clear the per-account counters when the local day changes. */
2065
+ private rollEarnings;
2066
+ /**
2067
+ * Write the ledger through the host hook, when one was supplied.
2068
+ *
2069
+ * Best effort on purpose: a failed save must never abort a run that has
2070
+ * already collected rewards, and the in-memory ledger keeps serving the card
2071
+ * for the rest of the session either way.
2072
+ */
2073
+ private persistEarnings;
2074
+ /**
2075
+ * Run one job against every eligible account and record the outcome.
2076
+ *
2077
+ * `consumedSlot` is the configured slot this run spends (`dueSlot`'s answer).
2078
+ * It is passed IN rather than recomputed here so the value recorded is exactly
2079
+ * the value the schedule decided on — the defect this replaces wrote
2080
+ * `slotKey(now)` instead, i.e. the clock time the run finished at, which
2081
+ * almost never equals the configured candidate the gate had compared against.
2082
+ *
2083
+ * The task job runs in TWO passes. The first sends the event chains that light
2084
+ * up client-scored tasks; the second collects rewards. They are separate
2085
+ * because scoring lands asynchronously — a chain sent and claimed within the
2086
+ * same breath finds the task still un-scored — and because sending is fast
2087
+ * while claiming wants the whole pool to have been lit up first. Splitting
2088
+ * them costs one shared wait instead of one wait per account.
2089
+ */
2090
+ private runJob;
2091
+ /** Compose the one-line summary shown on the card. */
2092
+ private summarise;
2093
+ /**
2094
+ * Accounts to run against, in pool order.
2095
+ *
2096
+ * Disabled accounts are excluded here rather than filtered by the caller so a
2097
+ * card switch takes effect on the next pass without any event plumbing.
2098
+ */
2099
+ private accountsInOrder;
2100
+ /**
2101
+ * Send one activity report, then verify it landed.
2102
+ *
2103
+ * The upstream answers 200 even when it drops the event, so the streak is
2104
+ * read back as the oracle: `days > 0` means it counted. A failed read-back is
2105
+ * logged and treated as a suspicious result, never as a retry — the report is
2106
+ * idempotent per day, and hammering it is exactly what the one-a-day quota
2107
+ * exists to avoid.
1995
2108
  */
1996
- distribution: PoolDistribution;
1997
- /** Which region this document describes. */
1998
- region: PoolRegion;
1999
- /** Every region holding at least one account, in display order. */
2000
- regions: readonly PoolRegion[];
2001
- shim: {
2002
- running: boolean;
2003
- baseUrl?: string;
2004
- };
2005
- /** Daily-points automation state, so the card can show what ran and when. */
2006
- automation: PoolWebAutomation;
2007
- /** Per-account credit floors currently in force, keyed by account id. */
2008
- creditReserves: Readonly<Record<string, number>>;
2109
+ private reportOne;
2110
+ private sendEventChains;
2009
2111
  /**
2010
- * Accounts thrown out of the pool, in the order they were ignored.
2112
+ * Send one chain on the channel it was built for.
2011
2113
  *
2012
- * Reported so the card can show the list and offer a way back. These accounts
2013
- * are NOT in `accounts`: they are filtered out before their credentials are
2014
- * read, which is the whole point of the feature.
2114
+ * The transport is not a detail of the sender: the scorer keys different
2115
+ * tasks to different fingerprint families, so a web-scored event posted as a
2116
+ * desktop event is accepted and then ignored.
2015
2117
  */
2016
- ignored: readonly PoolWebIgnoredAccount[];
2017
- }
2018
- /** One automation job's last run, as shown on the card. */
2019
- interface PoolWebAutomationJob {
2020
- /** `YYYY-MM-DD` of the last run in this process, if it has run. */
2021
- lastRunDate?: string;
2118
+ private sendChain;
2022
2119
  /**
2023
- * Epoch ms of the last run, so the card can show the TIME.
2120
+ * Build every chain that scores one task.
2024
2121
  *
2025
- * Carried because a date-only stamp cannot tell one run from eight: every
2026
- * repeat inside the same day rendered as the identical `2026-09-28 · 2`,
2027
- * which is what kept a "re-runs every hour" defect invisible on the card.
2122
+ * Most tasks need a single chain; `template_5` needs five, because the scorer
2123
+ * counts distinct `template_used` events rather than a boolean. The two tasks
2124
+ * that join a conversation (skill, expert) open a real one first, which is why
2125
+ * this is async.
2028
2126
  */
2029
- lastRunAtMs?: number;
2127
+ private chainsFor;
2030
2128
  /**
2031
- * Configured slots consumed today, as `YYYY-MM-DDTHH`.
2129
+ * The summon-and-use chains for the expert tasks.
2032
2130
  *
2033
- * Shown so "which of today's hours already ran" is answerable at a glance
2034
- * rather than inferred from a counter.
2131
+ * Two steps per expert, and both are load-bearing: the summon events alone are
2132
+ * impressions, and a use event on its own scores nothing because the scorer
2133
+ * looks the conversation up. Only a real chat with `X-Expert-Id` produces an
2134
+ * id it will accept.
2035
2135
  */
2036
- firedSlots?: readonly string[];
2037
- /** Accounts that finished without error on the last run. */
2038
- ok: number;
2039
- /** Accounts that failed on the last run (each one skipped, the run continued). */
2040
- failed: number;
2041
- /** Credits claimed by the task job on the last run. */
2042
- credit: number;
2043
- /** Energy claimed by the task job on the last run. */
2044
- energy: number;
2045
- /** Tasks claimed by the task job on the last run. */
2046
- claimed: number;
2047
- /** One-line summary of the last run. */
2048
- message?: string;
2136
+ private expertChains;
2049
2137
  /**
2050
- * What the last run actually did, in the words of the task board.
2138
+ * The 腾讯轻量云 expert chain.
2051
2139
  *
2052
- * `message` is a count; this is the list a person can check off, which is
2053
- * what turns a row from "it ran" into "it did the things I care about".
2140
+ * Structurally the same as the expert task, with two differences the scorer
2141
+ * checks: `agent_task_created` has to name the expert, and the use event has
2142
+ * to report `mode: 'LOCAL'` with an empty type and zero cost — that is what
2143
+ * the lighthouse criterion looks for.
2054
2144
  */
2055
- detail?: readonly string[];
2056
- /** A pending milestone worth naming, e.g. the next streak tier countdown. */
2057
- progress?: string;
2058
- }
2059
- /**
2060
- * Automation block on the status document.
2061
- *
2062
- * Carries the schedule and each job's last outcome so the card can answer
2063
- * "is it on, when does it run, and what did it last do" without reaching into
2064
- * the scheduler itself.
2065
- */
2066
- interface PoolWebAutomation {
2067
- /** Master switch, mirrored from the saved config. */
2068
- enabled: boolean;
2069
- /** Whether the loop is currently running. */
2070
- running: boolean;
2071
- /** Configured hours per job, so the card can show the schedule. */
2072
- checkinHours: readonly number[];
2073
- reportHours: readonly number[];
2074
- taskHours: readonly number[];
2075
- streakHours: readonly number[];
2076
- travelHours: readonly number[];
2077
- jobs: {
2078
- checkin: PoolWebAutomationJob;
2079
- report: PoolWebAutomationJob;
2080
- tasks: PoolWebAutomationJob;
2081
- streak: PoolWebAutomationJob;
2082
- travel: PoolWebAutomationJob;
2083
- };
2084
- /** Claimable tasks seen on the most recent task pass, across accounts. */
2085
- claimableSeen: number;
2145
+ private lighthouseChains;
2086
2146
  /**
2087
- * Whether a manual run is in flight. The card polls this to know when to
2088
- * stop showing progress and report the result.
2147
+ * The task-centre pass for one account.
2148
+ *
2149
+ * Order matters: enrich first (enrol in everything open), then claim. Both
2150
+ * halves are idempotent — accepting an already-accepted task succeeds, and a
2151
+ * repeat claim answers `already_claimed` — so a pass that dies halfway is
2152
+ * safe to replay on the next tick.
2089
2153
  */
2090
- runInProgress: boolean;
2154
+ private runTasks;
2091
2155
  /**
2092
- * Per-account credits/energy/tasks the automation earned TODAY, keyed by
2093
- * account id. An account that earned nothing is simply absent, so the card
2094
- * can say "nothing yet" instead of showing a bare zero.
2156
+ * Streak redemption plus the lottery it unlocks.
2157
+ *
2158
+ * Tiers unlock on consecutive active days (7/14/28). Redeeming one pays
2159
+ * credits, energy, a makeup card and — the part nothing else grants — lottery
2160
+ * draws, so the draw runs straight after and only for the chances in hand.
2161
+ *
2162
+ * Everything here is idempotent: a tier already claimed is skipped by its
2163
+ * status, and a draw consumes one chance, so a replay cannot double-spend.
2095
2164
  */
2096
- earningsToday: Readonly<Record<string, PoolWebAutomationEarnings>>;
2165
+ private redeemStreak;
2166
+ /**
2167
+ * One trip through the buddy travel loop for an account.
2168
+ *
2169
+ * A single pass advances the state machine by at most one step: a trip
2170
+ * that has arrived is collected, and an idle buddy is sent out. A buddy
2171
+ * already travelling is left alone — there is nothing to do until it lands.
2172
+ *
2173
+ * Measured against the live upstream: the departed trip reports
2174
+ * `dailyLimitReached` immediately, so the once-a-day limit needs no local
2175
+ * bookkeeping.
2176
+ */
2177
+ private runTravel;
2097
2178
  }
2098
- /** Today's automation take for one account. */
2099
- interface PoolWebAutomationEarnings {
2100
- /** Credits claimed from the task centre today. */
2101
- credit: number;
2102
- /** Energy claimed from the task centre today. */
2103
- energy: number;
2104
- /** Tasks claimed today. */
2105
- claimed: number;
2106
- /** Credits collected from check-in today. */
2107
- checkinCredit: number;
2108
- /** Credits from streak redemption and the lottery today. */
2109
- bonusCredit: number;
2110
- /** Credits from buddy adoption and the travel loop today. */
2111
- travelCredit: number;
2112
- /** Local date the counters belong to (YYYY-MM-DD). */
2113
- date: string;
2179
+ //#endregion
2180
+ //#region src/status.d.ts
2181
+ /** One account's status row. */
2182
+ interface AccountStatus {
2183
+ id: string;
2184
+ label: string;
2185
+ nickname?: string;
2186
+ domain: string;
2187
+ /** ISO timestamp when the access token expires. */
2188
+ expiresAt?: string;
2189
+ /** Account-wide cooldown (every model blocked). */
2190
+ cooling: boolean;
2191
+ cooldownUntil?: string;
2192
+ /** Per-model cooldowns active right now (modelId → ISO until); the account
2193
+ * itself is not `cooling` while only some models are limited. */
2194
+ modelCooldowns?: readonly {
2195
+ modelId: string;
2196
+ until: string;
2197
+ }[];
2198
+ rateLimitHits: number;
2199
+ /** Read-only aggregated credit summary for the account. */
2200
+ credits?: WorkBuddyCredits;
2201
+ creditsError?: string;
2202
+ sourcePath: string;
2114
2203
  }
2115
- /**
2116
- * The two gateways, matching the provider ids the host registers. `cn` is the
2117
- * domestic gateway (`copilot.tencent.com` / `codebuddy.cn`); `global` is the
2118
- * international one (`workbuddy.ai`).
2119
- */
2120
- type PoolRegion = 'cn' | 'global';
2121
- /** How the pool spreads requests across its accounts. */
2122
- type PoolDistribution = 'priority' | 'round-robin' | 'balanced';
2123
- /**
2124
- * The schedule every automation job falls back to.
2125
- *
2126
- * Shared by both halves on purpose. The host uses it when a configured hour
2127
- * list arrives empty (the settings schema materializes "never configured" into
2128
- * `[]`), and the card uses it when it writes the `automation` block back, so a
2129
- * document that already holds an empty list is healed instead of being saved
2130
- * back as an unrunnable schedule.
2131
- *
2132
- * This lives here rather than in `scheduler.ts` because the browser half cannot
2133
- * import the host module: `scheduler.ts` pulls in `node:crypto` and the whole
2134
- * upstream client, none of which exists in the browser bundle. Two hand-written
2135
- * copies would drift, and the drift is invisible — the card would write a
2136
- * schedule the scheduler does not run.
2137
- */
2138
- export declare const DEFAULT_AUTOMATION_HOURS: {
2139
- readonly checkin: readonly [9];
2140
- readonly report: readonly [10];
2141
- readonly tasks: readonly [11];
2142
- readonly streak: readonly [12];
2143
- readonly travel: readonly [9, 21];
2144
- };
2204
+ /** Whole-plugin status document. */
2205
+ interface WorkBuddyStatus {
2206
+ ok: boolean;
2207
+ accounts: AccountStatus[];
2208
+ activeAccountId?: string;
2209
+ cooling: number;
2210
+ models: {
2211
+ id: string;
2212
+ name: string;
2213
+ multiplier?: number;
2214
+ tags?: readonly string[];
2215
+ }[];
2216
+ shim: {
2217
+ running: boolean;
2218
+ baseUrl?: string;
2219
+ };
2220
+ }
2221
+ interface StatusOptions {
2222
+ pool: WorkBuddyAccountPool;
2223
+ /** The catalog to report. Regional callers pass their own region's. */
2224
+ catalog: WorkBuddyCatalog;
2225
+ client: WorkBuddyUpstreamClient;
2226
+ shim?: {
2227
+ running: boolean;
2228
+ baseUrl?: string;
2229
+ };
2230
+ /** Query credits per account. Off for cheap diagnostics runs. */
2231
+ includeCredits?: boolean;
2232
+ }
2233
+ /** Build the status document. Never throws. */
2234
+ export declare function buildStatus(options: StatusOptions): Promise<WorkBuddyStatus>;
2235
+ /** Format the status document for a terminal. */
2236
+ export declare function formatStatus(status: WorkBuddyStatus): string;
2237
+ /** Format the per-model credit multipliers. */
2238
+ export declare function formatRates(status: WorkBuddyStatus): string;
2145
2239
  //#endregion
2146
2240
  //#region src/ignored.d.ts
2147
2241
  /** Directory holding this plugin's own state (imported snapshots, ignore list). */
@@ -2271,6 +2365,19 @@ interface PoolStatusRouteOptions {
2271
2365
  * disk, including edits made by the CLI while the card is open.
2272
2366
  */
2273
2367
  ignoredAccounts?: () => readonly PoolWebIgnoredAccount[];
2368
+ /**
2369
+ * Re-fetch the model catalog for BOTH regions and report what landed.
2370
+ *
2371
+ * Absent when the host did not wire it: the route then answers 503 rather
2372
+ * than pretending the refresh happened.
2373
+ */
2374
+ refreshCatalog?: () => Promise<{
2375
+ regions: Readonly<Record<PoolRegion, {
2376
+ source: PoolWebCatalogSource;
2377
+ models: number;
2378
+ error?: string;
2379
+ }>>;
2380
+ }>;
2274
2381
  }
2275
2382
  /**
2276
2383
  * Assemble the card's status document. Per-account credits and check-in state