dsh-workbuddy-xdpool 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -3,6 +3,167 @@ import { Api, Model } from "@earendil-works/pi-ai";
3
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
4
  import { Context, Context as Context$1 } from "@deepseek-ai/cordis";
5
5
  import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
6
+ //#region src/task-events.d.ts
7
+ /**
8
+ * The event chains that light up the client-scored daily tasks.
9
+ *
10
+ * The task board advertises things like "open an app", "use five templates" or
11
+ * "summon a platform expert". Those read as actions only a human at the desktop
12
+ * app can take, but the scorer does not check the app — it checks the event
13
+ * stream. Replaying the same events, with the same desktop fingerprint, scores
14
+ * the task. Every chain here was measured against the live upstream.
15
+ *
16
+ * Kept apart from the scheduler because a chain is pure data: the scheduler
17
+ * decides *when* to send one, this module decides *what* one is. A few chains
18
+ * need a real server-side id first (the expert family), which is why the builder
19
+ * is allowed to be async and to call the upstream.
20
+ */
21
+ /**
22
+ * How a chain reaches the upstream.
23
+ *
24
+ * There are three fingerprint families and the scorer keys different tasks to
25
+ * different ones, so the transport is part of the chain rather than a detail of
26
+ * the sender: `Buddy_App` needs the desktop fingerprint, while `Library_read` is
27
+ * only scored when it arrives with the WEB fingerprint.
28
+ */
29
+ type TaskEventTransport = 'desktop' | 'web';
30
+ /** A ready-to-send chain, plus the channel it must go out on. */
31
+ interface TaskEventChain {
32
+ transport: TaskEventTransport;
33
+ /** Desktop chains: the event array. */
34
+ events?: readonly Record<string, unknown>[];
35
+ /** Web chains: the single page event to report. */
36
+ web?: {
37
+ eventCode: string;
38
+ pageUrl: string;
39
+ elementId: string;
40
+ elementName: string;
41
+ };
42
+ }
43
+ /**
44
+ * The app the buddy chain enters.
45
+ *
46
+ * One chain lights up two tasks: `Buddy_App` (open any app) and `Buddy_App_QQ`
47
+ * (the QQ-specific one), because this is a QQ-hosted app. Measured 0/1 → 1/1 on
48
+ * both from a single chain.
49
+ */
50
+ export declare const BUDDY_APP_ID = "cb_y5Dy46tPQGGWtueMxXbe";
51
+ export declare const BUDDY_APP_NAME = "企鹅教师助手";
52
+ /**
53
+ * The buddy-app chain (two tasks).
54
+ *
55
+ * Five clicks in the order a user would make them: discover the app, see it,
56
+ * enter it, confirm the account link, skip the second binding step.
57
+ */
58
+ export declare function buddyAppChain(): TaskEventChain;
59
+ /**
60
+ * The design-canvas chain (`create_canvas`, +300 — the joint largest reward).
61
+ *
62
+ * The two canvas events ride the same metrics channel as everything else, so no
63
+ * real canvas is ever created; the chat chain in front supplies the ids they
64
+ * reference. Measured 1/1 on three accounts.
65
+ */
66
+ export declare function canvasChain(): TaskEventChain;
67
+ /**
68
+ * The scheduled-task event (`automation_1`).
69
+ *
70
+ * One event is the whole chain — measured 1/1 on two accounts. The name is only
71
+ * for the server's own records, so a generated one is fine.
72
+ */
73
+ export declare function automationChain(): TaskEventChain;
74
+ /**
75
+ * The plain chat chain (`RichMeow_Chat`, and the base of the template chain).
76
+ *
77
+ * Measured: this chain alone lights `RichMeow_Chat`.
78
+ */
79
+ export declare function chatChain(): TaskEventChain;
80
+ /**
81
+ * The "same as this case" chain (`playbook_prompt`).
82
+ *
83
+ * The scorer watches `playbook_prompt_send` — sending the prompt that the
84
+ * inspiration case pre-fills — not the card impression or the button click, so
85
+ * the whole click path is replayed for realism but the send is what counts.
86
+ */
87
+ export declare function playbookChain(caseId?: string, caseName?: string): TaskEventChain;
88
+ /** The inspiration case the reference panel sends a prompt for. */
89
+ export declare const PLAYBOOK_CASE_ID = "pm-gtm-launch-plan";
90
+ export declare const PLAYBOOK_CASE_NAME = "新产品上市 GTM 发布计划一页纸";
91
+ /**
92
+ * The five templates the reference panel cycles through, as `[id, name]`.
93
+ *
94
+ * The upstream does not check that these templates exist — only that five
95
+ * distinct `template_used` events arrive — so they are the reference set.
96
+ */
97
+ export declare const TEMPLATE_PRESETS: readonly (readonly [string, string])[];
98
+ /**
99
+ * One "created a task from a template" chain (`template_5`, +100 for five).
100
+ *
101
+ * Each group is a chat chain (which supplies the ids the template events join
102
+ * on) plus `agent_task_created_with_template` and `template_used`. Measured:
103
+ * five groups in one report scored 5/5.
104
+ */
105
+ export declare function templateChain(templateId: string, templateName: string): TaskEventChain;
106
+ /** Every template group, ready to send in order. */
107
+ export declare function templateChains(): readonly TaskEventChain[];
108
+ /**
109
+ * The library-introduction click (`Library_read`).
110
+ *
111
+ * Scored on the WEB fingerprint — the same event posted with the desktop
112
+ * fingerprint scores nothing — so this chain returns a web transport and the
113
+ * scheduler routes it through `reportWebEvent`.
114
+ */
115
+ export declare function libraryReadChain(): TaskEventChain;
116
+ /** The document the library click is reported against. */
117
+ export declare const LIBRARY_DOC_URL = "https://www.workbuddy.cn/space/d/o0KWYeynteVv06UnAZqIFm";
118
+ /** The theme key `Hp_Appearance` is scored on (和平精英激战金秋). */
119
+ export declare const APPEARANCE_THEME_KEY = "theme-tkmw7j";
120
+ /** The skill `skill_1` is scored on. */
121
+ export declare const SKILL_ID = "skill_2097350077599879168";
122
+ export declare const SKILL_NAME = "润泽小馆·日报撰写";
123
+ /** The 腾讯轻量云 expert `Expert_lighthouse` is scored on. */
124
+ export declare const LIGHTHOUSE_EXPERT_ID = "ex_2cvvUZQhDyeJ";
125
+ /**
126
+ * Build the `skill_1` chain from a REAL conversation.
127
+ *
128
+ * Unlike the template and canvas chains, this one is verified against the
129
+ * conversation it names, so the caller must first open a real chat and hand the
130
+ * server-side ids in.
131
+ */
132
+ export declare function skillChain(conversationId: string, requestId: string): TaskEventChain;
133
+ /** The theme-apply event `Hp_Appearance` is scored on. */
134
+ export declare function appearanceChain(themeKey?: string): TaskEventChain;
135
+ /** One expert from the platform's marketplace, as the summon chains need it. */
136
+ interface MarketExpert {
137
+ expertId: string;
138
+ expertType: string;
139
+ displayName: string;
140
+ profession: string;
141
+ version: string;
142
+ categories: readonly string[];
143
+ }
144
+ /**
145
+ * The three "summon an expert" events (`expert_summon_click` and friends).
146
+ *
147
+ * Paid before the conversation, in the order the app emits them.
148
+ */
149
+ export declare function expertSummonEvents(expert: MarketExpert): Record<string, unknown>[];
150
+ /** `mode` for the `expert_actual_use` payload; the scorer checks it. */
151
+ type ExpertUseMode = 'craft' | 'LOCAL';
152
+ /**
153
+ * The "an expert really answered" event, which is what the expert tasks count.
154
+ *
155
+ * The `requestId` must be the SERVER's id for a real chat: a made-up one scores
156
+ * nothing, because the scorer looks the conversation up.
157
+ */
158
+ export declare function expertActualUseEvent(expert: MarketExpert, conversationId: string, requestId: string, mode?: ExpertUseMode): Record<string, unknown>;
159
+ /**
160
+ * The chat chain for an expert conversation.
161
+ *
162
+ * `agent_task_created` carries the expert fields the scorer reads to attribute
163
+ * the conversation to that expert.
164
+ */
165
+ export declare function expertChatEvents(expert: MarketExpert, conversationId: string, requestId: string): Record<string, unknown>[];
166
+ //#endregion
6
167
  //#region src/upstream.d.ts
7
168
  /** Upstream failure classes the shim maps onto distinct HTTP answers. */
8
169
  type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
@@ -103,6 +264,45 @@ export declare function classifyUpstreamError(status: number, body: string): Ups
103
264
  */
104
265
  export declare function parseRateLimitReset(body: string): number | undefined;
105
266
  /** One growth-centre task, flattened from the upstream's loosely-shaped entry. */
267
+ /** One streak tier and what redeeming it pays. */
268
+ interface WorkBuddyStreakTier {
269
+ /** Tier key, e.g. `7d`. */
270
+ tier: string;
271
+ /** Login days the tier needs. */
272
+ days: number;
273
+ credit: number;
274
+ energy: number;
275
+ cards: number;
276
+ /** Lottery draws the tier grants. */
277
+ chances: number;
278
+ /** `locked` / `unlocked` / `claimed`. */
279
+ status: string;
280
+ }
281
+ /** The growth streak as a whole: progress, tiers, makeup cards. */
282
+ interface WorkBuddyStreakStatus {
283
+ /** Current consecutive active days. */
284
+ days: number;
285
+ /** Days active this month. */
286
+ monthTotalDays: number;
287
+ /** Next tier key, e.g. `7d`. */
288
+ nextTier: string;
289
+ /** Days still needed for `nextTier`. */
290
+ nextTierRemaining: number;
291
+ /** Makeup cards in hand. */
292
+ makeupCards: number;
293
+ tiers: readonly WorkBuddyStreakTier[];
294
+ }
295
+ /** One buddy trip state. */
296
+ interface WorkBuddyTravelState {
297
+ /** `idle` (can depart) / `traveling` / `arrived` (can claim). */
298
+ state: string;
299
+ /** Trip id, required to claim an arrived trip. */
300
+ recordId: number;
301
+ /** The once-a-day depart limit has been used. */
302
+ dailyLimitReached: boolean;
303
+ /** Credits an arrived trip pays. */
304
+ rewardCredit: number;
305
+ }
106
306
  interface WorkBuddyTask {
107
307
  /** Upstream task code; the claim path is built from it. */
108
308
  taskCode: string;
@@ -128,6 +328,55 @@ interface WorkBuddyTask {
128
328
  /** Upstream marked the task locked (not yet reachable). */
129
329
  locked: boolean;
130
330
  }
331
+ /**
332
+ * Flatten one upstream task entry.
333
+ *
334
+ * Progress is reported two ways depending on the task: flat `current`/`target`
335
+ * fields, or a nested `progress: {current, target}`. The nested form wins when
336
+ * it carries anything, because a task that reports both puts the live counter
337
+ * there. Entries without a usable `task_code` are dropped — without one the
338
+ * claim path cannot be built.
339
+ */
340
+ /**
341
+ * The event chain that scores the two Buddy-app tasks.
342
+ *
343
+ * Both tasks accept the same chain, and the chain is a pure value: building it
344
+ * needs no client, so callers (and tests) can hold one without a live upstream.
345
+ *
346
+ * The task text says "upgrade to the desktop client and open it from the app
347
+ * launcher". The scorer does not watch the UI — it watches this event sequence
348
+ * with the desktop fingerprint, which is why the sequence is what gets sent.
349
+ *
350
+ * Measured against the live upstream: progress 0/1 → 1/1 claimable in ~8s.
351
+ */
352
+ /**
353
+ * A complete "desktop client ran a request successfully" event chain.
354
+ *
355
+ * Six events in the order the real client emits them: task created, message
356
+ * send, request send, message response, message status, request response.
357
+ * Several tasks are scored off this chain (or one that embeds it), because what
358
+ * they measure is "a real request completed", which the client only proves
359
+ * through this exact sequence.
360
+ *
361
+ * Measured: this chain alone lights up `RichMeow_Chat`.
362
+ */
363
+ export declare function desktopChatEvents(conversationId: string, requestId: string, messageId: string, modelId?: string, modelName?: string): Record<string, unknown>[];
364
+ /**
365
+ * A chat chain plus the two canvas events that score `create_canvas`.
366
+ *
367
+ * Worth +300, the joint largest task on the board. The canvas events ride
368
+ * the same metrics channel as everything else, so no real canvas is needed.
369
+ *
370
+ * Measured: three accounts scored 1/1 from this sequence.
371
+ */
372
+ export declare function desktopCanvasEvents(conversationId: string, requestId: string): Record<string, unknown>[];
373
+ /**
374
+ * The single event that scores `automation_1` (a scheduled task was created).
375
+ *
376
+ * Measured: two accounts lit it with this event alone.
377
+ */
378
+ export declare function desktopAutomationCreatedEvent(name: string): Record<string, unknown>;
379
+ export declare function buddyAppEvents(buddyId: string, buddyName: string): Record<string, unknown>[];
131
380
  export declare class WorkBuddyUpstreamClient {
132
381
  private readonly fetchImpl;
133
382
  private readonly clientVersion;
@@ -165,17 +414,97 @@ export declare class WorkBuddyUpstreamClient {
165
414
  fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
166
415
  /** Claim today's check-in reward. The browser route guards this mutation. */
167
416
  claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
417
+ /**
418
+ * Public fingerprint fields every desktop event carries.
419
+ *
420
+ * The upstream scores a desktop-fingerprinted task only when the event looks
421
+ * like it came from the desktop client: the same field set, the same stable
422
+ * device ids, the same build. A partial map is accepted with 200 and scores
423
+ * nothing, so these are copied wholesale rather than trimmed.
424
+ *
425
+ * `machineId`/`sessionId` are DERIVED from the account uid, never random: a
426
+ * new device id on every call is itself a signal that the traffic is not a
427
+ * real client.
428
+ */
429
+ desktopFingerprint(credential: WorkBuddyCredential): Record<string, unknown>;
430
+ /**
431
+ * Send desktop-fingerprinted events to the growth system.
432
+ *
433
+ * The body is an ARRAY of events, and every event carries the full desktop
434
+ * fingerprint plus its own business fields. Different tasks recognise
435
+ * different fingerprint families (CLI / desktop / web), which is why this is
436
+ * separate from {@link reportActivity}: they are not interchangeable.
437
+ *
438
+ * Business fields win over the fingerprint, so a caller can override a device
439
+ * id to align with a real install.
440
+ */
441
+ reportDesktopEvents(credential: WorkBuddyCredential, events: readonly Record<string, unknown>[]): Promise<void>;
442
+ /**
443
+ * Report one chat-activity event to the growth system.
444
+ *
445
+ * The body is an ARRAY holding a single `chat_request_send` event, and every
446
+ * field is filled in: a three-field minimal event is accepted with 200 and
447
+ * then silently dropped, so the full shape is load-bearing rather than
448
+ * cosmetic. `userId` is the one field the server actually keys on — without
449
+ * it the request still answers 200 and scores nothing.
450
+ *
451
+ * One report per account per day is the quota the reference panel settled on;
452
+ * a single report lights the growth streak and unlocks the `first_buddy`
453
+ * family, which is why this runs before the task-centre pass.
454
+
455
+ /**
456
+ * Send a WEB-fingerprinted event.
457
+ *
458
+ * A third fingerprint family, alongside CLI and desktop: a browser shape
459
+ * posted to the web origin with x-client-platform: web. Page-behaviour
460
+ * tasks such as `Library_read` are scored on it. Measured:
461
+ * `library_doc_intro_click` scored about four seconds after landing.
462
+ */
463
+ reportWebEvent(credential: WorkBuddyCredential, eventCode: string, pageUrl: string, elementId: string, elementName: string): Promise<void>;
464
+ /**
465
+ * Apply an appearance theme on the account.
466
+ *
467
+ * The theme task is scored on the `appearance_skin_apply` event, not on this
468
+ * call — but the event alone is not enough either. The pair is what a real
469
+ * client produces: it PATCHes the account's selected skin, then reports the
470
+ * event as the settings page closes. Measured on the reference panel after
471
+ * the earlier "the API alone does not score" reading was corrected.
472
+ */
473
+ setAppearanceTheme(credential: WorkBuddyCredential, resourceKey: string): Promise<void>;
474
+ /**
475
+ * The platform's expert marketplace.
476
+ *
477
+ * Needed before any expert can be summoned: the scorer verifies that the
478
+ * expert id exists on the platform, so a made-up id scores nothing. The
479
+ * response carries the display fields the summon events replay.
480
+ */
481
+ marketExpertList(credential: WorkBuddyCredential, expertType?: 'agent' | 'team' | ''): Promise<readonly MarketExpert[]>;
482
+ /**
483
+ * Open a REAL desktop conversation and return the ids the server assigned.
484
+ *
485
+ * The expert and skill tasks join their events to a conversation the server
486
+ * has actually seen, so a locally invented `requestId` scores nothing. This
487
+ * starts a chat, reads the server's id out of the SSE stream, then drops the
488
+ * rest of the stream — the answer itself is irrelevant, only its identity is.
489
+ *
490
+ * Returns `undefined` instead of throwing when the conversation cannot be
491
+ * opened or carries no recognisable id, because every caller is a best-effort
492
+ * task chain.
493
+ */
494
+ openConversation(credential: WorkBuddyCredential, expertId?: string, signal?: AbortSignal): Promise<{
495
+ conversationId: string;
496
+ requestId: string;
497
+ } | undefined>;
168
498
  /**
169
499
  * Report one chat-activity event to the growth system.
170
500
  *
171
- * The body is an ARRAY holding a single `chat_request_send` event, and every
501
+ * The body is an ARRAY holding a single chat_request_send event, and every
172
502
  * field is filled in: a three-field minimal event is accepted with 200 and
173
503
  * then silently dropped, so the full shape is load-bearing rather than
174
- * cosmetic. `userId` is the one field the server actually keys on — without
175
- * it the request still answers 200 and scores nothing.
504
+ * cosmetic. userId is the one field the server actually keys on.
176
505
  *
177
- * One report per account per day is the quota the reference panel settled on;
178
- * a single report lights the growth streak and unlocks the `first_buddy`
506
+ * One report per account per day is the quota the reference panel settled
507
+ * on; a single report lights the growth streak and unlocks the first_buddy
179
508
  * family, which is why this runs before the task-centre pass.
180
509
  */
181
510
  reportActivity(credential: WorkBuddyCredential, conversationId?: string): Promise<void>;
@@ -199,6 +528,81 @@ export declare class WorkBuddyUpstreamClient {
199
528
  * Returns 0 only when the field is genuinely absent.
200
529
  */
201
530
  growthStreakDays(credential: WorkBuddyCredential): Promise<number>;
531
+ /**
532
+ * The full streak picture: days, tier unlock state, and what each tier pays.
533
+ *
534
+ * Read before redeeming, because the tier state is the only honest answer to
535
+ * "is there anything to claim": the redeem endpoint answers 403 for a locked
536
+ * tier, which is indistinguishable from a real failure once the response is
537
+ * just an error.
538
+ */
539
+ growthStreakFull(credential: WorkBuddyCredential): Promise<WorkBuddyStreakStatus>;
540
+ /**
541
+ * Redeem one unlocked streak tier.
542
+ *
543
+ * A locked tier answers 403 ("连续登录天数不足"); callers check the status from
544
+ * {@link growthStreakFull} first, so this only throws for genuine failures.
545
+ * The client token is the upstream's idempotency key — a fresh one per attempt
546
+ * keeps a retry from being read as a duplicate of the last one.
547
+ */
548
+ redeemStreakTier(credential: WorkBuddyCredential, tier: string): Promise<void>;
549
+ /** How many lottery draws are available right now. */
550
+ lotteryChances(credential: WorkBuddyCredential): Promise<number>;
551
+ /**
552
+ * Draw the lottery once.
553
+ *
554
+ * Returns the raw prize payload: its shape is set by the running campaign, so
555
+ * it is passed through rather than modelled.
556
+ */
557
+ lotteryDraw(credential: WorkBuddyCredential): Promise<unknown>;
558
+ /**
559
+ * The buddy profile, or undefined when the account has no buddy yet.
560
+ *
561
+ * `data.buddy` is null / absent / an empty object depending on how far the
562
+ * account got, and all three mean the same thing to a caller: adopt first.
563
+ */
564
+ buddyInfo(credential: WorkBuddyCredential): Promise<{
565
+ instanceId: number;
566
+ name: string;
567
+ } | undefined>;
568
+ /** Agree to the buddy terms. Idempotent upstream. */
569
+ buddyAgree(credential: WorkBuddyCredential): Promise<void>;
570
+ /**
571
+ * Adopt the first buddy.
572
+ *
573
+ * Gated upstream on having reported activity that day: without it the answer
574
+ * is 400 "first_buddy task not completed yet". Callers treat that as "not yet"
575
+ * rather than an error, which is why it is thrown as-is for them to classify.
576
+ */
577
+ buddyAdoptFirst(credential: WorkBuddyCredential): Promise<void>;
578
+ /** Current travel state for the account's buddy. */
579
+ buddyTravelStatus(credential: WorkBuddyCredential): Promise<WorkBuddyTravelState>;
580
+ /**
581
+ * Send the buddy travelling.
582
+ *
583
+ * The location is always 4 (古镇客栈): the four locations have identical
584
+ * reward and duration ranges, so there is nothing to optimise.
585
+ */
586
+ buddyTravelDepart(credential: WorkBuddyCredential, locationId?: number): Promise<void>;
587
+ /**
588
+ * Collect an arrived trip's reward.
589
+ *
590
+ * `recordId` is required and comes from the status read; the upstream rejects
591
+ * a claim without it.
592
+ */
593
+ buddyTravelClaim(credential: WorkBuddyCredential, recordId: number): Promise<number>;
594
+ /** Whether yesterday is a gap in the activity heatmap. */
595
+ heatmapYesterdayMissed(credential: WorkBuddyCredential): Promise<boolean>;
596
+ /** Spend one makeup card on a date. Idempotent for an already-filled date. */
597
+ useMakeupCard(credential: WorkBuddyCredential, date: string): Promise<void>;
598
+ /**
599
+ * Call a growth-domain endpoint and return its unwrapped `data`.
600
+ *
601
+ * These endpoints live on the chat host with the billing header set, and
602
+ * carry the same envelope as everything else. Centralised here because every
603
+ * growth call needs the identical envelope check.
604
+ */
605
+ private growthJson;
202
606
  /** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
203
607
  credits(credential: WorkBuddyCredential): Promise<{
204
608
  ok: true;
@@ -378,6 +782,25 @@ export declare class WorkBuddyAccountPool {
378
782
  * lives on the pool and is re-applied from settings after each scan.
379
783
  */
380
784
  private disabledIds;
785
+ /**
786
+ * Per-account credit floor, keyed by account id. 0 (or absent) means "spend
787
+ * it all".
788
+ *
789
+ * A reserved balance is protection, not a hard limit the upstream knows
790
+ * about: the pool simply stops picking that account once its last known
791
+ * balance is at or below the floor, so the user keeps a cushion instead of
792
+ * draining every account to zero.
793
+ */
794
+ private creditReserves;
795
+ /**
796
+ * Last known credit balance per account, epoch ms aside.
797
+ *
798
+ * Refreshed in the background after a successful request, so a pick can
799
+ * consult it. An account with no reading is treated as usable: refusing to
800
+ * pick an account just because its balance has not been checked yet would
801
+ * strand a healthy pool, and the first 402 still cools it as before.
802
+ */
803
+ private creditBalances;
381
804
  /**
382
805
  * Last time each account served a request, epoch ms. Drives the idle term
383
806
  * of the priority-mode weighting below: an account that just served loses to
@@ -401,6 +824,8 @@ export declare class WorkBuddyAccountPool {
401
824
  exhaustCooldownMs?: number;
402
825
  distribution?: AccountDistribution;
403
826
  disabledAccountIds?: readonly string[];
827
+ /** Per-account credit floor, keyed by account id. Absent keeps the current map. */
828
+ creditReserves?: Readonly<Record<string, number>>;
404
829
  }): void;
405
830
  /** Rescan the auth directories and merge newly discovered accounts. */
406
831
  scan(): Promise<WorkBuddyAccount[]>;
@@ -470,6 +895,33 @@ export declare class WorkBuddyAccountPool {
470
895
  * not count as used for the account that was merely tried.
471
896
  */
472
897
  noteServed(accountId: string): void;
898
+ /**
899
+ * Record an account latest known credit balance.
900
+ *
901
+ * Called after a request and by the card balance refresh, so the reserve
902
+ * check has something to compare against. A reading for an unknown account is
903
+ * dropped: `scan()` rebuilds the account list and a stale id would otherwise
904
+ * accumulate forever.
905
+ */
906
+ noteCredits(accountId: string, balance: number): void;
907
+ /** Last known balance for one account, or undefined when never read. */
908
+ creditsOf(accountId: string): number | undefined;
909
+ /** The credit floor the user set for one account; 0 when unset. */
910
+ creditReserveOf(accountId: string): number;
911
+ /**
912
+ * Replace every reserve. Called from settings on each apply, so the map
913
+ * mirrors the saved document exactly instead of accumulating old keys.
914
+ */
915
+ setCreditReserves(reserves: Readonly<Record<string, number>>): void;
916
+ /** Every reserve currently in force, keyed by account id. */
917
+ creditReservesInOrder(): Record<string, number>;
918
+ /**
919
+ * Whether an account is held back only by its reserve.
920
+ *
921
+ * Separates "resting to protect credits" from every other reason an account
922
+ * is out of rotation, which is what the card shows the user.
923
+ */
924
+ isReserved(accountId: string): boolean;
473
925
  /**
474
926
  * The account that served the most recent request, if any.
475
927
  *
@@ -636,7 +1088,27 @@ interface WorkBuddyAdapter {
636
1088
  export declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
637
1089
  //#endregion
638
1090
  //#region src/scheduler.d.ts
1091
+ /** How often the loop wakes to look for a due job. */
1092
+ /**
1093
+ * How long to wait for event scoring to land before re-reading the task list.
1094
+ *
1095
+ * Scoring is asynchronous on the upstream side, so an immediate re-read still
1096
+ * shows the old progress and the claim pass would skip a task that is in fact
1097
+ * now claimable. Measured: the chain is reflected by about eight seconds.
1098
+ */
1099
+ export declare const EVENT_SCORE_WAIT_MS = 9000;
1100
+ export declare const AUTOMATION_TICK_MS = 60000;
639
1101
  /** Which jobs the automation runs, and when. */
1102
+ /**
1103
+ * The persisted daily earnings ledger.
1104
+ *
1105
+ * `date` is the local day the counters belong to: a ledger from a previous day
1106
+ * is discarded on load rather than carried forward as "today".
1107
+ */
1108
+ interface AutomationLedger {
1109
+ date: string;
1110
+ accounts: Record<string, AutomationAccountEarnings>;
1111
+ }
640
1112
  interface AutomationOptions {
641
1113
  /** Master switch; false stops every job. */
642
1114
  enabled?: boolean;
@@ -648,12 +1120,27 @@ interface AutomationOptions {
648
1120
  reportHours?: readonly number[];
649
1121
  /** Hour list for the streak-redemption job. */
650
1122
  streakHours?: readonly number[];
1123
+ /** Hour list for the buddy travel job. */
1124
+ travelHours?: readonly number[];
651
1125
  /** Per-account serial delay, in milliseconds. */
652
1126
  accountDelayMs?: number;
1127
+ /** Override the gap between expert chains, in milliseconds (tests use 0). */
1128
+ expertGapMs?: number;
1129
+ /** Override the event-scoring wait, in milliseconds (tests use 0). */
1130
+ eventScoreWaitMs?: number;
653
1131
  /** Override the clock, for tests. */
654
1132
  now?: () => Date;
655
1133
  /** Logger; defaults to a no-op so tests stay quiet. */
656
1134
  logger?: SchedulerLogger;
1135
+ /**
1136
+ * Persist the daily earnings ledger, and restore it on construction.
1137
+ *
1138
+ * The ledger cannot live only in memory: a host restart mid-day would wipe
1139
+ * what the automation already earned, and the card would show nothing for
1140
+ * rewards that really were collected.
1141
+ */
1142
+ loadEarnings?: () => AutomationLedger | undefined;
1143
+ saveEarnings?: (ledger: AutomationLedger) => void;
657
1144
  }
658
1145
  /** Logger surface, kept structural so any host logger fits. */
659
1146
  interface SchedulerLogger {
@@ -679,6 +1166,61 @@ interface AutomationJobState {
679
1166
  /** Human-readable summary of the last run. */
680
1167
  message?: string;
681
1168
  }
1169
+ /**
1170
+ * What the automation earned for ONE account today.
1171
+ *
1172
+ * Reset at the local day boundary alongside the per-job "already ran today"
1173
+ * guard, so the card shows today rather than a running total that never
1174
+ * answers "did it do anything for this account recently".
1175
+ */
1176
+ interface AutomationAccountEarnings {
1177
+ /** Credits the automation claimed from the task centre today. */
1178
+ credit: number;
1179
+ /** Energy claimed from the task centre today. */
1180
+ energy: number;
1181
+ /** Tasks claimed today. */
1182
+ claimed: number;
1183
+ /**
1184
+ * Credits the automation collected from check-in today.
1185
+ *
1186
+ * Kept separate from `credit` because they are different achievements and the
1187
+ * card shows them on their own lines: "the automation claimed 3 tasks" and
1188
+ * "the automation checked in" are not the same claim to the user.
1189
+ */
1190
+ checkinCredit: number;
1191
+ /** Credits from streak redemption + lottery today. */
1192
+ bonusCredit: number;
1193
+ /** Credits from the buddy adoption / travel loop today. */
1194
+ travelCredit: number;
1195
+ /** Local date the counters belong to. */
1196
+ date: string;
1197
+ }
1198
+ /** What ONE account gained during a single run. */
1199
+ interface AutomationAccountGain {
1200
+ credit: number;
1201
+ energy: number;
1202
+ claimed: number;
1203
+ checkinCredit: number;
1204
+ bonusCredit: number;
1205
+ travelCredit: number;
1206
+ }
1207
+ /** Totals from running the whole ordered pass at once. */
1208
+ interface AutomationRunSummary {
1209
+ /** How many jobs actually ran (a job with no hour is skipped). */
1210
+ jobsRun: number;
1211
+ /** Accounts that finished without error, summed across jobs. */
1212
+ okCount: number;
1213
+ /** Accounts that failed, summed across jobs. */
1214
+ failed: number;
1215
+ credit: number;
1216
+ energy: number;
1217
+ claimed: number;
1218
+ /**
1219
+ * What each account gained during THIS run, keyed by account id. Only
1220
+ * accounts that gained something appear.
1221
+ */
1222
+ accounts: Readonly<Record<string, AutomationAccountGain>>;
1223
+ }
682
1224
  /** Automation snapshot for the status document and the card. */
683
1225
  interface AutomationStatus {
684
1226
  enabled: boolean;
@@ -688,15 +1230,48 @@ interface AutomationStatus {
688
1230
  taskHours: readonly number[];
689
1231
  reportHours: readonly number[];
690
1232
  streakHours: readonly number[];
1233
+ travelHours: readonly number[];
691
1234
  jobs: {
692
1235
  checkin: AutomationJobState;
693
1236
  report: AutomationJobState;
694
1237
  tasks: AutomationJobState;
695
1238
  streak: AutomationJobState;
1239
+ travel: AutomationJobState;
696
1240
  };
697
1241
  /** Claimable tasks seen on the most recent task pass, across accounts. */
698
1242
  claimableSeen: number;
1243
+ /**
1244
+ * Per-account totals for today, keyed by account id. Only accounts that
1245
+ * actually earned something appear, so the card can render "no earnings"
1246
+ * as an absence rather than a zero it has to explain.
1247
+ */
1248
+ earningsToday: Readonly<Record<string, AutomationAccountEarnings>>;
1249
+ /**
1250
+ * Whether a manual run is in flight right now.
1251
+ *
1252
+ * A run takes tens of seconds (one upstream call per account per job, plus
1253
+ * the scoring wait), which is far too long for the card to hold a request
1254
+ * open. The button starts a run and the panel polls this flag instead.
1255
+ */
1256
+ runInProgress: boolean;
699
1257
  }
1258
+ /** The three plus one job kinds, in a stable order. */
1259
+ type AutomationJobKind = 'checkin' | 'tasks' | 'report' | 'streak' | 'travel';
1260
+ /** The four jobs in the order a tick runs them: report before tasks, always. */
1261
+ export declare const AUTOMATION_JOB_KINDS: readonly AutomationJobKind[];
1262
+ /** Reject anything that is not a job kind, so a route cannot name an unknown job. */
1263
+ export declare function isAutomationJobKind(value: unknown): value is AutomationJobKind;
1264
+ export declare function dayKey(date: Date): string;
1265
+ /**
1266
+ * Whether `now`'s local hour is one of `hours`.
1267
+ *
1268
+ * The reference panel computes a `nextFire` instant and sleeps until it; this
1269
+ * loop instead wakes every minute and asks "is any job due now". Both fire at
1270
+ * the top of a configured hour, but the polling form cannot miss a slot to a
1271
+ * suspended process — a laptop that slept through 10:00 still runs the job the
1272
+ * moment it wakes, on the same day.
1273
+ */
1274
+ export declare function isFireHour(now: Date, hours: readonly number[]): boolean;
700
1275
  /**
701
1276
  * The points automation.
702
1277
  *
@@ -704,21 +1279,34 @@ interface AutomationStatus {
704
1279
  * {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
705
1280
  * twice is harmless.
706
1281
  */
707
- declare class WorkBuddyScheduler {
1282
+ export declare class WorkBuddyScheduler {
708
1283
  private readonly pool;
709
1284
  private readonly client;
710
1285
  private readonly logger;
711
1286
  private readonly now;
712
1287
  private readonly delayMs;
1288
+ /**
1289
+ * How long to wait for event scoring before re-reading the task list.
1290
+ * Tests set 0 so a pass does not spend nine real seconds per account.
1291
+ */
1292
+ private readonly eventScoreWaitMs;
1293
+ /**
1294
+ * Gap between two expert summon chains.
1295
+ * Tests set 0 so a pass does not spend six real seconds per expert.
1296
+ */
1297
+ private readonly expertGapMs;
713
1298
  private enabled;
714
1299
  private checkinHours;
715
1300
  private taskHours;
716
1301
  private reportHours;
717
1302
  private streakHours;
1303
+ private travelHours;
718
1304
  private timer;
719
1305
  private running;
720
1306
  /** Guards against a slow run overlapping the next tick. */
721
1307
  private busy;
1308
+ /** True while a manual run is in flight, so the card can poll it. */
1309
+ private runInFlight;
722
1310
  /**
723
1311
  * Set once {@link stop} is called.
724
1312
  *
@@ -729,8 +1317,36 @@ declare class WorkBuddyScheduler {
729
1317
  private stopped;
730
1318
  private readonly states;
731
1319
  private claimableSeen;
1320
+ /**
1321
+ * Credits/energy/tasks earned per account TODAY, keyed by account id.
1322
+ *
1323
+ * Cleared whenever the day key rolls over, so the card always answers
1324
+ * "what did the automation get for THIS account today".
1325
+ */
1326
+ private earnings;
1327
+ /** Day key the counters above belong to. */
1328
+ private earningsDate;
1329
+ /** Host hooks that persist the ledger across restarts. */
1330
+ private readonly loadEarnings;
1331
+ private saveEarningsFn;
732
1332
  constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
733
1333
  /** Apply a new configuration; safe to call while running. */
1334
+ /**
1335
+ * Install the persistence hook once the host settings service is available.
1336
+ *
1337
+ * Separate from the constructor because the scheduler is built with the pool,
1338
+ * long before the settings section exists; a ledger written before that point
1339
+ * would have nowhere to go.
1340
+ */
1341
+ setEarningsPersistence(save: (ledger: AutomationLedger) => void): void;
1342
+ /**
1343
+ * Fold a previously persisted ledger back in, when it belongs to today.
1344
+ *
1345
+ * Used after the settings document becomes readable, which happens after
1346
+ * construction; a ledger from an earlier day is ignored so the counters never
1347
+ * claim yesterday as today.
1348
+ */
1349
+ applyEarningsLedger(ledger: AutomationLedger): void;
734
1350
  applyConfig(options: AutomationOptions): void;
735
1351
  /** Hours for one job, used by the loop and the status document. */
736
1352
  private hoursOf;
@@ -739,6 +1355,39 @@ declare class WorkBuddyScheduler {
739
1355
  /** Stop the loop. Idempotent, and safe before `start`. */
740
1356
  stop(): void;
741
1357
  /** Snapshot for the status document. */
1358
+ /**
1359
+ * Run one job immediately, regardless of the clock.
1360
+ *
1361
+ * Exists so the automation can be verified from the card without waiting for
1362
+ * its hour. A manual run is recorded exactly like a scheduled one, so the
1363
+ * timer will not repeat it later the same day: every job is idempotent, but a
1364
+ * second pass would still be wasted upstream calls.
1365
+ *
1366
+ * `force` ignores the already-ran-today guard, which is what pressing the
1367
+ * button a second time means.
1368
+ */
1369
+ runNow(kind: AutomationJobKind, force?: boolean): Promise<AutomationJobState>;
1370
+ /**
1371
+ * Run every job once, in the scheduled order.
1372
+ *
1373
+ * Order matters and is not configurable: the activity report has to land
1374
+ * before the task pass reads task progress, or the pass sees counters the
1375
+ * report would have moved. This is what the card's single button calls.
1376
+ */
1377
+ /**
1378
+ * Start a full pass in the background and return immediately.
1379
+ *
1380
+ * A pass takes tens of seconds - one upstream round trip per account per job,
1381
+ * plus the scoring wait - which is far too long to hold the card request open:
1382
+ * the browser or the host web server would time out, and the user would see
1383
+ * a hung button for a run that is actually working.
1384
+ *
1385
+ * Returns whether a run started. A second call while one is in flight is
1386
+ * ignored rather than queued: pressing the button twice means hurry up, and
1387
+ * the run already under way covers it.
1388
+ */
1389
+ startRunAll(): boolean;
1390
+ runAll(): Promise<AutomationRunSummary>;
742
1391
  status(): AutomationStatus;
743
1392
  /**
744
1393
  * One poll: run every due job, serially.
@@ -749,6 +1398,43 @@ declare class WorkBuddyScheduler {
749
1398
  */
750
1399
  private tick;
751
1400
  /** Run one job against every eligible account and record the outcome. */
1401
+ /**
1402
+ * Add one account's take to today's counters, resetting first if the day
1403
+ * rolled over. Called from the task pass, which is the only job that earns.
1404
+ */
1405
+ /**
1406
+ * Today's per-account earnings, as a plain object for the status document.
1407
+ *
1408
+ * Rolls the day first so a status read just after midnight does not report
1409
+ * yesterday's totals under today's date.
1410
+ */
1411
+ private earningsSnapshot;
1412
+ /**
1413
+ * Add one account take to today counters, resetting first if the day rolled
1414
+ * over. Every source is tracked separately so the card can show what earned
1415
+ * what, rather than one opaque total.
1416
+ */
1417
+ private recordEarnings;
1418
+ /** Clear the per-account counters when the local day changes. */
1419
+ private rollEarnings;
1420
+ /**
1421
+ * Write the ledger through the host hook, when one was supplied.
1422
+ *
1423
+ * Best effort on purpose: a failed save must never abort a run that has
1424
+ * already collected rewards, and the in-memory ledger keeps serving the card
1425
+ * for the rest of the session either way.
1426
+ */
1427
+ private persistEarnings;
1428
+ /**
1429
+ * Run one job against every eligible account and record the outcome.
1430
+ *
1431
+ * The task job runs in TWO passes. The first sends the event chains that light
1432
+ * up client-scored tasks; the second collects rewards. They are separate
1433
+ * because scoring lands asynchronously — a chain sent and claimed within the
1434
+ * same breath finds the task still un-scored — and because sending is fast
1435
+ * while claiming wants the whole pool to have been lit up first. Splitting
1436
+ * them costs one shared wait instead of one wait per account.
1437
+ */
752
1438
  private runJob;
753
1439
  /** Compose the one-line summary shown on the card. */
754
1440
  private summarise;
@@ -769,6 +1455,42 @@ declare class WorkBuddyScheduler {
769
1455
  * exists to avoid.
770
1456
  */
771
1457
  private reportOne;
1458
+ private sendEventChains;
1459
+ /**
1460
+ * Send one chain on the channel it was built for.
1461
+ *
1462
+ * The transport is not a detail of the sender: the scorer keys different
1463
+ * tasks to different fingerprint families, so a web-scored event posted as a
1464
+ * desktop event is accepted and then ignored.
1465
+ */
1466
+ private sendChain;
1467
+ /**
1468
+ * Build every chain that scores one task.
1469
+ *
1470
+ * Most tasks need a single chain; `template_5` needs five, because the scorer
1471
+ * counts distinct `template_used` events rather than a boolean. The two tasks
1472
+ * that join a conversation (skill, expert) open a real one first, which is why
1473
+ * this is async.
1474
+ */
1475
+ private chainsFor;
1476
+ /**
1477
+ * The summon-and-use chains for the expert tasks.
1478
+ *
1479
+ * Two steps per expert, and both are load-bearing: the summon events alone are
1480
+ * impressions, and a use event on its own scores nothing because the scorer
1481
+ * looks the conversation up. Only a real chat with `X-Expert-Id` produces an
1482
+ * id it will accept.
1483
+ */
1484
+ private expertChains;
1485
+ /**
1486
+ * The 腾讯轻量云 expert chain.
1487
+ *
1488
+ * Structurally the same as the expert task, with two differences the scorer
1489
+ * checks: `agent_task_created` has to name the expert, and the use event has
1490
+ * to report `mode: 'LOCAL'` with an empty type and zero cost — that is what
1491
+ * the lighthouse criterion looks for.
1492
+ */
1493
+ private lighthouseChains;
772
1494
  /**
773
1495
  * The task-centre pass for one account.
774
1496
  *
@@ -779,15 +1501,28 @@ declare class WorkBuddyScheduler {
779
1501
  */
780
1502
  private runTasks;
781
1503
  /**
782
- * Streak redemption and lottery.
1504
+ * Streak redemption plus the lottery it unlocks.
1505
+ *
1506
+ * Tiers unlock on consecutive active days (7/14/28). Redeeming one pays
1507
+ * credits, energy, a makeup card and — the part nothing else grants — lottery
1508
+ * draws, so the draw runs straight after and only for the chances in hand.
783
1509
  *
784
- * Left as a deliberate no-op placeholder: the tier/lottery endpoints need
785
- * their own round of probing against the live upstream before they can be
786
- * wired safely, and a wrong call here could burn a redemption. The job slot,
787
- * scheduling and status plumbing already exist, so filling it in is a
788
- * self-contained change.
1510
+ * Everything here is idempotent: a tier already claimed is skipped by its
1511
+ * status, and a draw consumes one chance, so a replay cannot double-spend.
789
1512
  */
790
1513
  private redeemStreak;
1514
+ /**
1515
+ * One trip through the buddy travel loop for an account.
1516
+ *
1517
+ * A single pass advances the state machine by at most one step: a trip
1518
+ * that has arrived is collected, and an idle buddy is sent out. A buddy
1519
+ * already travelling is left alone — there is nothing to do until it lands.
1520
+ *
1521
+ * Measured against the live upstream: the departed trip reports
1522
+ * `dailyLimitReached` immediately, so the once-a-day limit needs no local
1523
+ * bookkeeping.
1524
+ */
1525
+ private runTravel;
791
1526
  }
792
1527
  //#endregion
793
1528
  //#region src/status.d.ts
@@ -873,6 +1608,10 @@ export declare const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/c
873
1608
  export declare const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
874
1609
  /** Plugin-owned model-selection save endpoint (writes the settings section). */
875
1610
  export declare const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
1611
+ /** Run one automation job immediately, so the card can verify it on demand. */
1612
+ export declare const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
1613
+ /** Set or clear one account's reserved-credit floor. */
1614
+ export declare const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
876
1615
  /** One pool account's row, token-free. */
877
1616
  interface PoolWebAccount {
878
1617
  id: string;
@@ -901,6 +1640,24 @@ interface PoolWebAccount {
901
1640
  */
902
1641
  disabled: boolean;
903
1642
  rateLimitHits: number;
1643
+ /**
1644
+ * Credits the user asked to keep for this account. The pool stops picking the
1645
+ * account once its balance reaches the reserve, so this many credits survive.
1646
+ * 0 means the account may be spent down as before.
1647
+ */
1648
+ creditReserve: number;
1649
+ /**
1650
+ * Whether the account is held back purely by its reserve right now. Kept
1651
+ * distinct from `cooling`: a reserved account is healthy and simply
1652
+ * protected, which is a different thing to tell the user than rate-limited.
1653
+ */
1654
+ reserved: boolean;
1655
+ /**
1656
+ * What the automation earned for this account today. Absent when it earned
1657
+ * nothing (or the automation never ran for it), so the card can stay quiet
1658
+ * instead of printing a row of zeroes.
1659
+ */
1660
+ automationToday?: PoolWebAutomationEarnings;
904
1661
  /** ISO timestamp of the last successful use (best-effort pool bookkeeping). */
905
1662
  lastUsedAt?: string;
906
1663
  /** Aggregated credit summary for the account, read-only. */
@@ -1018,6 +1775,8 @@ interface PoolWebStatus {
1018
1775
  };
1019
1776
  /** Daily-points automation state, so the card can show what ran and when. */
1020
1777
  automation: PoolWebAutomation;
1778
+ /** Per-account credit floors currently in force, keyed by account id. */
1779
+ creditReserves: Readonly<Record<string, number>>;
1021
1780
  }
1022
1781
  /** One automation job's last run, as shown on the card. */
1023
1782
  interface PoolWebAutomationJob {
@@ -1058,9 +1817,38 @@ interface PoolWebAutomation {
1058
1817
  report: PoolWebAutomationJob;
1059
1818
  tasks: PoolWebAutomationJob;
1060
1819
  streak: PoolWebAutomationJob;
1820
+ travel: PoolWebAutomationJob;
1061
1821
  };
1062
1822
  /** Claimable tasks seen on the most recent task pass, across accounts. */
1063
1823
  claimableSeen: number;
1824
+ /**
1825
+ * Whether a manual run is in flight. The card polls this to know when to
1826
+ * stop showing progress and report the result.
1827
+ */
1828
+ runInProgress: boolean;
1829
+ /**
1830
+ * Per-account credits/energy/tasks the automation earned TODAY, keyed by
1831
+ * account id. An account that earned nothing is simply absent, so the card
1832
+ * can say "nothing yet" instead of showing a bare zero.
1833
+ */
1834
+ earningsToday: Readonly<Record<string, PoolWebAutomationEarnings>>;
1835
+ }
1836
+ /** Today's automation take for one account. */
1837
+ interface PoolWebAutomationEarnings {
1838
+ /** Credits claimed from the task centre today. */
1839
+ credit: number;
1840
+ /** Energy claimed from the task centre today. */
1841
+ energy: number;
1842
+ /** Tasks claimed today. */
1843
+ claimed: number;
1844
+ /** Credits collected from check-in today. */
1845
+ checkinCredit: number;
1846
+ /** Credits from streak redemption and the lottery today. */
1847
+ bonusCredit: number;
1848
+ /** Credits from buddy adoption and the travel loop today. */
1849
+ travelCredit: number;
1850
+ /** Local date the counters belong to (YYYY-MM-DD). */
1851
+ date: string;
1064
1852
  }
1065
1853
  /**
1066
1854
  * The two gateways, matching the provider ids the host registers. `cn` is the
@@ -1071,6 +1859,72 @@ type PoolRegion = 'cn' | 'global';
1071
1859
  /** How the pool spreads requests across its accounts. */
1072
1860
  type PoolDistribution = 'priority' | 'round-robin' | 'balanced';
1073
1861
  //#endregion
1862
+ //#region src/web-status.d.ts
1863
+ /** Constructor dependencies — a narrow slice of the pool runtime. */
1864
+ interface PoolStatusRouteOptions {
1865
+ pool: WorkBuddyAccountPool;
1866
+ /** One catalog per region; the card reads the one for its active tab. */
1867
+ catalogs: Readonly<Record<PoolRegion, WorkBuddyCatalog>>;
1868
+ client: WorkBuddyUpstreamClient;
1869
+ /** Lazily resolve the running loopback shim, when it has bound a port. */
1870
+ shim?: () => {
1871
+ running: boolean;
1872
+ baseUrl?: string;
1873
+ };
1874
+ /**
1875
+ * The daily-points automation, when the host half has one.
1876
+ *
1877
+ * Optional so these routes still mount on a profile that assembled a pool
1878
+ * without a scheduler (the CLI and the tests do exactly that); the card then
1879
+ * reports the automation as off instead of showing a broken panel.
1880
+ */
1881
+ scheduler?: () => AutomationStatus;
1882
+ /**
1883
+ /**
1884
+ * Start a manual pass, for the card's "run now" button.
1885
+ *
1886
+ * Returns whether a run STARTED, not its result: a pass takes tens of
1887
+ * seconds, so it runs in the background and the card polls the scheduler
1888
+ * status for progress and earnings.
1889
+ */
1890
+ runAutomation?: (job: string, force: boolean) => boolean;
1891
+ /**
1892
+ * Persist one account's reserved-credit floor. Same settings document as every
1893
+ * other card write, so it survives a restart and is re-applied after a scan.
1894
+ * Absent without a settings service: the route then answers 503.
1895
+ */
1896
+ setCreditReserve?: (accountId: string, reserve: number) => Promise<void> | void;
1897
+ /**
1898
+ * Persist the user's model selection. Provided by the host half, which owns
1899
+ * the settings section; absent when the plugin runs without a settings
1900
+ * service (the save route then reports 503 rather than pretending to work).
1901
+ */
1902
+ /**
1903
+ * Persist one region's selection. The region travels with the payload: the two
1904
+ * gateways advertise different rosters, so the domestic tab and the
1905
+ * international tab each own their list and must never overwrite each other.
1906
+ */
1907
+ saveSelection?: (region: PoolRegion, selection: PoolWebModelSelection) => Promise<void> | void;
1908
+ /**
1909
+ * Persist one account switch. Same settings document as every other card
1910
+ * write, so it survives a restart and is re-applied after each re-scan.
1911
+ * Absent without a settings service: the route then answers 503.
1912
+ */
1913
+ setAccountDisabled?: (accountId: string, disabled: boolean) => Promise<void> | void;
1914
+ }
1915
+ /**
1916
+ * Assemble the card's status document. Per-account credits and check-in state
1917
+ * are queried live; a failing query degrades to `creditsError` / `checkinError`
1918
+ * rather than failing the whole document. Never throws.
1919
+ */
1920
+ export declare function poolWebStatus(deps: PoolStatusRouteOptions, region?: PoolRegion): Promise<PoolWebStatus>;
1921
+ /**
1922
+ * Mount the read-only routes on a context where `webServer` is available. The
1923
+ * caller uses `ctx.inject(['webServer'], ...)` so Desktop startup order cannot
1924
+ * make this registration disappear.
1925
+ */
1926
+ export declare function registerPoolStatusRoute(ctx: Context$1, deps: PoolStatusRouteOptions): void;
1927
+ //#endregion
1074
1928
  //#region src/index.d.ts
1075
1929
  /** Stable Cordis plugin name. */
1076
1930
  export declare const name = "llm-workbuddy-xdpool";
@@ -1092,11 +1946,16 @@ export interface Config {
1092
1946
  /**
1093
1947
  * How the pool spreads requests across accounts.
1094
1948
  *
1095
- * `priority` (default) drains one account before moving to the next, which
1096
- * is what a pool of your own accounts is for. `round-robin` splits the
1097
- * spend evenly instead. Absent reads as `priority`.
1949
+ * - `priority` (default) drains one account before moving to the next, which
1950
+ * is what a pool of your own accounts is for.
1951
+ * - `round-robin` walks the pool in order, so the spend splits evenly.
1952
+ * - `balanced` draws at random, weighting whichever account has been idle
1953
+ * longest. Spend still spreads, but without a fixed order, so one unhealthy
1954
+ * account cannot pin the pool to itself.
1955
+ *
1956
+ * Absent reads as `priority`.
1098
1957
  */
1099
- distribution?: 'priority' | 'round-robin';
1958
+ distribution?: 'priority' | 'round-robin' | 'balanced';
1100
1959
  /**
1101
1960
  * Account ids switched off on the card. A disabled account is never picked
1102
1961
  * to serve a request, but it stays in the pool and on the card so it can be
@@ -1104,6 +1963,12 @@ export interface Config {
1104
1963
  * survive re-scans (see WorkBuddyAccountPool.disabledIds).
1105
1964
  */
1106
1965
  disabledAccountIds?: string[];
1966
+ /**
1967
+ * Per-account credit floor, keyed by account id. The pool stops picking an
1968
+ * account once its last known balance reaches this value, so the reserved
1969
+ * credits survive. Absent or 0 spends the account down as before.
1970
+ */
1971
+ creditReserves?: Record<string, number>;
1107
1972
  /**
1108
1973
  * Model ids enabled in the picker. Absent means "every model the catalog
1109
1974
  * advertises" — an unconfigured install should never present an empty model
@@ -1136,6 +2001,13 @@ export interface Config {
1136
2001
  * fresh install with background traffic.
1137
2002
  */
1138
2003
  automation?: AutomationConfig;
2004
+ /**
2005
+ * The automation's daily earnings ledger, written by the scheduler itself.
2006
+ *
2007
+ * It lives in settings rather than only in memory so a host restart mid-day
2008
+ * does not wipe what the automation already earned.
2009
+ */
2010
+ automationEarnings?: AutomationLedger;
1139
2011
  }
1140
2012
  /** One region's saved model selection. */
1141
2013
  export interface ModelSelectionConfig {
@@ -1166,6 +2038,8 @@ export interface AutomationConfig {
1166
2038
  taskHours?: number[];
1167
2039
  /** Hours at which streak redemption runs. */
1168
2040
  streakHours?: number[];
2041
+ /** Hours at which the buddy travel loop runs. */
2042
+ travelHours?: number[];
1169
2043
  /** How long an account rests after its credits run out, in milliseconds. */
1170
2044
  exhaustCooldownMs?: number;
1171
2045
  }
@@ -1238,4 +2112,4 @@ export declare function createCore(logger?: {
1238
2112
  */
1239
2113
  export declare function apply(ctx: Context, config?: Config): void;
1240
2114
  //#endregion
1241
- export type { AccountStatus, Context, ModelSelection, PoolWebCheckin, PoolWebCheckinClaim, PoolWebModel, PoolWebModelSelection, PoolWebStatus, UpstreamErrorKind, WorkBuddyAccount, WorkBuddyAdapter, WorkBuddyCredential, WorkBuddyModelInfo, WorkBuddyShim, WorkBuddyStatus };
2115
+ export type { AccountStatus, AutomationLedger, AutomationRunSummary, AutomationStatus, Context, ExpertUseMode, MarketExpert, ModelSelection, PoolStatusRouteOptions, PoolWebCheckin, PoolWebCheckinClaim, PoolWebModel, PoolWebModelSelection, PoolWebStatus, SchedulerLogger, TaskEventChain, TaskEventTransport, UpstreamErrorKind, WorkBuddyAccount, WorkBuddyAdapter, WorkBuddyCredential, WorkBuddyModelInfo, WorkBuddyShim, WorkBuddyStatus };