dsh-workbuddy-xdpool 1.1.0 → 1.3.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 {
@@ -664,6 +1151,16 @@ interface SchedulerLogger {
664
1151
  interface AutomationJobState {
665
1152
  /** `YYYY-MM-DD` of the last completed run, or undefined if it never ran. */
666
1153
  lastRunDate?: string;
1154
+ /**
1155
+ * The scheduled SLOT of the last run, as `YYYY-MM-DDTHH`.
1156
+ *
1157
+ * The tick de-duplicates on this rather than on the date: keying on the date
1158
+ * alone caps every job at one run a day, which is wrong for a job with two
1159
+ * time points — blocking the cat loop's second pass would leave the cat out
1160
+ * until tomorrow. Per slot a job runs once in each configured hour, while a
1161
+ * repeat tick inside the same hour is still refused.
1162
+ */
1163
+ lastRunSlot?: string;
667
1164
  /** Epoch ms of the last completed run. */
668
1165
  lastRunAtMs?: number;
669
1166
  /** Accounts that completed without throwing. */
@@ -677,8 +1174,80 @@ interface AutomationJobState {
677
1174
  /** Tasks claimed by the task job on the last run. */
678
1175
  claimed: number;
679
1176
  /** Human-readable summary of the last run. */
1177
+ /**
1178
+ * What this run actually did, in the words of the task board.
1179
+ *
1180
+ * `message` is a count ("3 accounts, 5 tasks claimed"); this is the list a
1181
+ * person can check off — the reward titles the pass collected. A row showing
1182
+ * only a bare number cannot answer "did it do the thing I care about", which
1183
+ * is the question the panel exists to answer.
1184
+ */
1185
+ detail?: readonly string[];
1186
+ /**
1187
+ * A pending milestone worth naming, when there is one.
1188
+ *
1189
+ * Streak tiers are why this exists: every tier reads `locked` until enough
1190
+ * consecutive days accumulate, and "locked" on its own reads as "broken"
1191
+ * rather than "come back in four days".
1192
+ */
1193
+ progress?: string;
680
1194
  message?: string;
681
1195
  }
1196
+ /**
1197
+ * What the automation earned for ONE account today.
1198
+ *
1199
+ * Reset at the local day boundary alongside the per-job "already ran today"
1200
+ * guard, so the card shows today rather than a running total that never
1201
+ * answers "did it do anything for this account recently".
1202
+ */
1203
+ interface AutomationAccountEarnings {
1204
+ /** Credits the automation claimed from the task centre today. */
1205
+ credit: number;
1206
+ /** Energy claimed from the task centre today. */
1207
+ energy: number;
1208
+ /** Tasks claimed today. */
1209
+ claimed: number;
1210
+ /**
1211
+ * Credits the automation collected from check-in today.
1212
+ *
1213
+ * Kept separate from `credit` because they are different achievements and the
1214
+ * card shows them on their own lines: "the automation claimed 3 tasks" and
1215
+ * "the automation checked in" are not the same claim to the user.
1216
+ */
1217
+ checkinCredit: number;
1218
+ /** Credits from streak redemption + lottery today. */
1219
+ bonusCredit: number;
1220
+ /** Credits from the buddy adoption / travel loop today. */
1221
+ travelCredit: number;
1222
+ /** Local date the counters belong to. */
1223
+ date: string;
1224
+ }
1225
+ /** What ONE account gained during a single run. */
1226
+ interface AutomationAccountGain {
1227
+ credit: number;
1228
+ energy: number;
1229
+ claimed: number;
1230
+ checkinCredit: number;
1231
+ bonusCredit: number;
1232
+ travelCredit: number;
1233
+ }
1234
+ /** Totals from running the whole ordered pass at once. */
1235
+ interface AutomationRunSummary {
1236
+ /** How many jobs actually ran (a job with no hour is skipped). */
1237
+ jobsRun: number;
1238
+ /** Accounts that finished without error, summed across jobs. */
1239
+ okCount: number;
1240
+ /** Accounts that failed, summed across jobs. */
1241
+ failed: number;
1242
+ credit: number;
1243
+ energy: number;
1244
+ claimed: number;
1245
+ /**
1246
+ * What each account gained during THIS run, keyed by account id. Only
1247
+ * accounts that gained something appear.
1248
+ */
1249
+ accounts: Readonly<Record<string, AutomationAccountGain>>;
1250
+ }
682
1251
  /** Automation snapshot for the status document and the card. */
683
1252
  interface AutomationStatus {
684
1253
  enabled: boolean;
@@ -688,15 +1257,48 @@ interface AutomationStatus {
688
1257
  taskHours: readonly number[];
689
1258
  reportHours: readonly number[];
690
1259
  streakHours: readonly number[];
1260
+ travelHours: readonly number[];
691
1261
  jobs: {
692
1262
  checkin: AutomationJobState;
693
1263
  report: AutomationJobState;
694
1264
  tasks: AutomationJobState;
695
1265
  streak: AutomationJobState;
1266
+ travel: AutomationJobState;
696
1267
  };
697
1268
  /** Claimable tasks seen on the most recent task pass, across accounts. */
698
1269
  claimableSeen: number;
1270
+ /**
1271
+ * Per-account totals for today, keyed by account id. Only accounts that
1272
+ * actually earned something appear, so the card can render "no earnings"
1273
+ * as an absence rather than a zero it has to explain.
1274
+ */
1275
+ earningsToday: Readonly<Record<string, AutomationAccountEarnings>>;
1276
+ /**
1277
+ * Whether a manual run is in flight right now.
1278
+ *
1279
+ * A run takes tens of seconds (one upstream call per account per job, plus
1280
+ * the scoring wait), which is far too long for the card to hold a request
1281
+ * open. The button starts a run and the panel polls this flag instead.
1282
+ */
1283
+ runInProgress: boolean;
699
1284
  }
1285
+ /** The three plus one job kinds, in a stable order. */
1286
+ type AutomationJobKind = 'checkin' | 'tasks' | 'report' | 'streak' | 'travel';
1287
+ /** The four jobs in the order a tick runs them: report before tasks, always. */
1288
+ export declare const AUTOMATION_JOB_KINDS: readonly AutomationJobKind[];
1289
+ /** Reject anything that is not a job kind, so a route cannot name an unknown job. */
1290
+ export declare function isAutomationJobKind(value: unknown): value is AutomationJobKind;
1291
+ export declare function dayKey(date: Date): string;
1292
+ /**
1293
+ * Whether `now`'s local hour is one of `hours`.
1294
+ *
1295
+ * The reference panel computes a `nextFire` instant and sleeps until it; this
1296
+ * loop instead wakes every minute and asks "is any job due now". Both fire at
1297
+ * the top of a configured hour, but the polling form cannot miss a slot to a
1298
+ * suspended process — a laptop that slept through 10:00 still runs the job the
1299
+ * moment it wakes, on the same day.
1300
+ */
1301
+ export declare function isFireHour(now: Date, hours: readonly number[]): boolean;
700
1302
  /**
701
1303
  * The points automation.
702
1304
  *
@@ -704,21 +1306,34 @@ interface AutomationStatus {
704
1306
  * {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
705
1307
  * twice is harmless.
706
1308
  */
707
- declare class WorkBuddyScheduler {
1309
+ export declare class WorkBuddyScheduler {
708
1310
  private readonly pool;
709
1311
  private readonly client;
710
1312
  private readonly logger;
711
1313
  private readonly now;
712
1314
  private readonly delayMs;
1315
+ /**
1316
+ * How long to wait for event scoring before re-reading the task list.
1317
+ * Tests set 0 so a pass does not spend nine real seconds per account.
1318
+ */
1319
+ private readonly eventScoreWaitMs;
1320
+ /**
1321
+ * Gap between two expert summon chains.
1322
+ * Tests set 0 so a pass does not spend six real seconds per expert.
1323
+ */
1324
+ private readonly expertGapMs;
713
1325
  private enabled;
714
1326
  private checkinHours;
715
1327
  private taskHours;
716
1328
  private reportHours;
717
1329
  private streakHours;
1330
+ private travelHours;
718
1331
  private timer;
719
1332
  private running;
720
1333
  /** Guards against a slow run overlapping the next tick. */
721
1334
  private busy;
1335
+ /** True while a manual run is in flight, so the card can poll it. */
1336
+ private runInFlight;
722
1337
  /**
723
1338
  * Set once {@link stop} is called.
724
1339
  *
@@ -729,8 +1344,36 @@ declare class WorkBuddyScheduler {
729
1344
  private stopped;
730
1345
  private readonly states;
731
1346
  private claimableSeen;
1347
+ /**
1348
+ * Credits/energy/tasks earned per account TODAY, keyed by account id.
1349
+ *
1350
+ * Cleared whenever the day key rolls over, so the card always answers
1351
+ * "what did the automation get for THIS account today".
1352
+ */
1353
+ private earnings;
1354
+ /** Day key the counters above belong to. */
1355
+ private earningsDate;
1356
+ /** Host hooks that persist the ledger across restarts. */
1357
+ private readonly loadEarnings;
1358
+ private saveEarningsFn;
732
1359
  constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
733
1360
  /** Apply a new configuration; safe to call while running. */
1361
+ /**
1362
+ * Install the persistence hook once the host settings service is available.
1363
+ *
1364
+ * Separate from the constructor because the scheduler is built with the pool,
1365
+ * long before the settings section exists; a ledger written before that point
1366
+ * would have nowhere to go.
1367
+ */
1368
+ setEarningsPersistence(save: (ledger: AutomationLedger) => void): void;
1369
+ /**
1370
+ * Fold a previously persisted ledger back in, when it belongs to today.
1371
+ *
1372
+ * Used after the settings document becomes readable, which happens after
1373
+ * construction; a ledger from an earlier day is ignored so the counters never
1374
+ * claim yesterday as today.
1375
+ */
1376
+ applyEarningsLedger(ledger: AutomationLedger): void;
734
1377
  applyConfig(options: AutomationOptions): void;
735
1378
  /** Hours for one job, used by the loop and the status document. */
736
1379
  private hoursOf;
@@ -739,6 +1382,39 @@ declare class WorkBuddyScheduler {
739
1382
  /** Stop the loop. Idempotent, and safe before `start`. */
740
1383
  stop(): void;
741
1384
  /** Snapshot for the status document. */
1385
+ /**
1386
+ * Run one job immediately, regardless of the clock.
1387
+ *
1388
+ * Exists so the automation can be verified from the card without waiting for
1389
+ * its hour. A manual run is recorded exactly like a scheduled one, so the
1390
+ * timer will not repeat it later the same day: every job is idempotent, but a
1391
+ * second pass would still be wasted upstream calls.
1392
+ *
1393
+ * `force` ignores the already-ran-today guard, which is what pressing the
1394
+ * button a second time means.
1395
+ */
1396
+ runNow(kind: AutomationJobKind, force?: boolean): Promise<AutomationJobState>;
1397
+ /**
1398
+ * Run every job once, in the scheduled order.
1399
+ *
1400
+ * Order matters and is not configurable: the activity report has to land
1401
+ * before the task pass reads task progress, or the pass sees counters the
1402
+ * report would have moved. This is what the card's single button calls.
1403
+ */
1404
+ /**
1405
+ * Start a full pass in the background and return immediately.
1406
+ *
1407
+ * A pass takes tens of seconds - one upstream round trip per account per job,
1408
+ * plus the scoring wait - which is far too long to hold the card request open:
1409
+ * the browser or the host web server would time out, and the user would see
1410
+ * a hung button for a run that is actually working.
1411
+ *
1412
+ * Returns whether a run started. A second call while one is in flight is
1413
+ * ignored rather than queued: pressing the button twice means hurry up, and
1414
+ * the run already under way covers it.
1415
+ */
1416
+ startRunAll(): boolean;
1417
+ runAll(): Promise<AutomationRunSummary>;
742
1418
  status(): AutomationStatus;
743
1419
  /**
744
1420
  * One poll: run every due job, serially.
@@ -749,6 +1425,43 @@ declare class WorkBuddyScheduler {
749
1425
  */
750
1426
  private tick;
751
1427
  /** Run one job against every eligible account and record the outcome. */
1428
+ /**
1429
+ * Add one account's take to today's counters, resetting first if the day
1430
+ * rolled over. Called from the task pass, which is the only job that earns.
1431
+ */
1432
+ /**
1433
+ * Today's per-account earnings, as a plain object for the status document.
1434
+ *
1435
+ * Rolls the day first so a status read just after midnight does not report
1436
+ * yesterday's totals under today's date.
1437
+ */
1438
+ private earningsSnapshot;
1439
+ /**
1440
+ * Add one account take to today counters, resetting first if the day rolled
1441
+ * over. Every source is tracked separately so the card can show what earned
1442
+ * what, rather than one opaque total.
1443
+ */
1444
+ private recordEarnings;
1445
+ /** Clear the per-account counters when the local day changes. */
1446
+ private rollEarnings;
1447
+ /**
1448
+ * Write the ledger through the host hook, when one was supplied.
1449
+ *
1450
+ * Best effort on purpose: a failed save must never abort a run that has
1451
+ * already collected rewards, and the in-memory ledger keeps serving the card
1452
+ * for the rest of the session either way.
1453
+ */
1454
+ private persistEarnings;
1455
+ /**
1456
+ * Run one job against every eligible account and record the outcome.
1457
+ *
1458
+ * The task job runs in TWO passes. The first sends the event chains that light
1459
+ * up client-scored tasks; the second collects rewards. They are separate
1460
+ * because scoring lands asynchronously — a chain sent and claimed within the
1461
+ * same breath finds the task still un-scored — and because sending is fast
1462
+ * while claiming wants the whole pool to have been lit up first. Splitting
1463
+ * them costs one shared wait instead of one wait per account.
1464
+ */
752
1465
  private runJob;
753
1466
  /** Compose the one-line summary shown on the card. */
754
1467
  private summarise;
@@ -769,6 +1482,42 @@ declare class WorkBuddyScheduler {
769
1482
  * exists to avoid.
770
1483
  */
771
1484
  private reportOne;
1485
+ private sendEventChains;
1486
+ /**
1487
+ * Send one chain on the channel it was built for.
1488
+ *
1489
+ * The transport is not a detail of the sender: the scorer keys different
1490
+ * tasks to different fingerprint families, so a web-scored event posted as a
1491
+ * desktop event is accepted and then ignored.
1492
+ */
1493
+ private sendChain;
1494
+ /**
1495
+ * Build every chain that scores one task.
1496
+ *
1497
+ * Most tasks need a single chain; `template_5` needs five, because the scorer
1498
+ * counts distinct `template_used` events rather than a boolean. The two tasks
1499
+ * that join a conversation (skill, expert) open a real one first, which is why
1500
+ * this is async.
1501
+ */
1502
+ private chainsFor;
1503
+ /**
1504
+ * The summon-and-use chains for the expert tasks.
1505
+ *
1506
+ * Two steps per expert, and both are load-bearing: the summon events alone are
1507
+ * impressions, and a use event on its own scores nothing because the scorer
1508
+ * looks the conversation up. Only a real chat with `X-Expert-Id` produces an
1509
+ * id it will accept.
1510
+ */
1511
+ private expertChains;
1512
+ /**
1513
+ * The 腾讯轻量云 expert chain.
1514
+ *
1515
+ * Structurally the same as the expert task, with two differences the scorer
1516
+ * checks: `agent_task_created` has to name the expert, and the use event has
1517
+ * to report `mode: 'LOCAL'` with an empty type and zero cost — that is what
1518
+ * the lighthouse criterion looks for.
1519
+ */
1520
+ private lighthouseChains;
772
1521
  /**
773
1522
  * The task-centre pass for one account.
774
1523
  *
@@ -779,15 +1528,28 @@ declare class WorkBuddyScheduler {
779
1528
  */
780
1529
  private runTasks;
781
1530
  /**
782
- * Streak redemption and lottery.
1531
+ * Streak redemption plus the lottery it unlocks.
1532
+ *
1533
+ * Tiers unlock on consecutive active days (7/14/28). Redeeming one pays
1534
+ * credits, energy, a makeup card and — the part nothing else grants — lottery
1535
+ * draws, so the draw runs straight after and only for the chances in hand.
783
1536
  *
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.
1537
+ * Everything here is idempotent: a tier already claimed is skipped by its
1538
+ * status, and a draw consumes one chance, so a replay cannot double-spend.
789
1539
  */
790
1540
  private redeemStreak;
1541
+ /**
1542
+ * One trip through the buddy travel loop for an account.
1543
+ *
1544
+ * A single pass advances the state machine by at most one step: a trip
1545
+ * that has arrived is collected, and an idle buddy is sent out. A buddy
1546
+ * already travelling is left alone — there is nothing to do until it lands.
1547
+ *
1548
+ * Measured against the live upstream: the departed trip reports
1549
+ * `dailyLimitReached` immediately, so the once-a-day limit needs no local
1550
+ * bookkeeping.
1551
+ */
1552
+ private runTravel;
791
1553
  }
792
1554
  //#endregion
793
1555
  //#region src/status.d.ts
@@ -873,6 +1635,10 @@ export declare const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/c
873
1635
  export declare const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
874
1636
  /** Plugin-owned model-selection save endpoint (writes the settings section). */
875
1637
  export declare const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
1638
+ /** Run one automation job immediately, so the card can verify it on demand. */
1639
+ export declare const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
1640
+ /** Set or clear one account's reserved-credit floor. */
1641
+ export declare const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
876
1642
  /** One pool account's row, token-free. */
877
1643
  interface PoolWebAccount {
878
1644
  id: string;
@@ -901,6 +1667,24 @@ interface PoolWebAccount {
901
1667
  */
902
1668
  disabled: boolean;
903
1669
  rateLimitHits: number;
1670
+ /**
1671
+ * Credits the user asked to keep for this account. The pool stops picking the
1672
+ * account once its balance reaches the reserve, so this many credits survive.
1673
+ * 0 means the account may be spent down as before.
1674
+ */
1675
+ creditReserve: number;
1676
+ /**
1677
+ * Whether the account is held back purely by its reserve right now. Kept
1678
+ * distinct from `cooling`: a reserved account is healthy and simply
1679
+ * protected, which is a different thing to tell the user than rate-limited.
1680
+ */
1681
+ reserved: boolean;
1682
+ /**
1683
+ * What the automation earned for this account today. Absent when it earned
1684
+ * nothing (or the automation never ran for it), so the card can stay quiet
1685
+ * instead of printing a row of zeroes.
1686
+ */
1687
+ automationToday?: PoolWebAutomationEarnings;
904
1688
  /** ISO timestamp of the last successful use (best-effort pool bookkeeping). */
905
1689
  lastUsedAt?: string;
906
1690
  /** Aggregated credit summary for the account, read-only. */
@@ -1018,6 +1802,8 @@ interface PoolWebStatus {
1018
1802
  };
1019
1803
  /** Daily-points automation state, so the card can show what ran and when. */
1020
1804
  automation: PoolWebAutomation;
1805
+ /** Per-account credit floors currently in force, keyed by account id. */
1806
+ creditReserves: Readonly<Record<string, number>>;
1021
1807
  }
1022
1808
  /** One automation job's last run, as shown on the card. */
1023
1809
  interface PoolWebAutomationJob {
@@ -1035,6 +1821,15 @@ interface PoolWebAutomationJob {
1035
1821
  claimed: number;
1036
1822
  /** One-line summary of the last run. */
1037
1823
  message?: string;
1824
+ /**
1825
+ * What the last run actually did, in the words of the task board.
1826
+ *
1827
+ * `message` is a count; this is the list a person can check off, which is
1828
+ * what turns a row from "it ran" into "it did the things I care about".
1829
+ */
1830
+ detail?: readonly string[];
1831
+ /** A pending milestone worth naming, e.g. the next streak tier countdown. */
1832
+ progress?: string;
1038
1833
  }
1039
1834
  /**
1040
1835
  * Automation block on the status document.
@@ -1053,14 +1848,44 @@ interface PoolWebAutomation {
1053
1848
  reportHours: readonly number[];
1054
1849
  taskHours: readonly number[];
1055
1850
  streakHours: readonly number[];
1851
+ travelHours: readonly number[];
1056
1852
  jobs: {
1057
1853
  checkin: PoolWebAutomationJob;
1058
1854
  report: PoolWebAutomationJob;
1059
1855
  tasks: PoolWebAutomationJob;
1060
1856
  streak: PoolWebAutomationJob;
1857
+ travel: PoolWebAutomationJob;
1061
1858
  };
1062
1859
  /** Claimable tasks seen on the most recent task pass, across accounts. */
1063
1860
  claimableSeen: number;
1861
+ /**
1862
+ * Whether a manual run is in flight. The card polls this to know when to
1863
+ * stop showing progress and report the result.
1864
+ */
1865
+ runInProgress: boolean;
1866
+ /**
1867
+ * Per-account credits/energy/tasks the automation earned TODAY, keyed by
1868
+ * account id. An account that earned nothing is simply absent, so the card
1869
+ * can say "nothing yet" instead of showing a bare zero.
1870
+ */
1871
+ earningsToday: Readonly<Record<string, PoolWebAutomationEarnings>>;
1872
+ }
1873
+ /** Today's automation take for one account. */
1874
+ interface PoolWebAutomationEarnings {
1875
+ /** Credits claimed from the task centre today. */
1876
+ credit: number;
1877
+ /** Energy claimed from the task centre today. */
1878
+ energy: number;
1879
+ /** Tasks claimed today. */
1880
+ claimed: number;
1881
+ /** Credits collected from check-in today. */
1882
+ checkinCredit: number;
1883
+ /** Credits from streak redemption and the lottery today. */
1884
+ bonusCredit: number;
1885
+ /** Credits from buddy adoption and the travel loop today. */
1886
+ travelCredit: number;
1887
+ /** Local date the counters belong to (YYYY-MM-DD). */
1888
+ date: string;
1064
1889
  }
1065
1890
  /**
1066
1891
  * The two gateways, matching the provider ids the host registers. `cn` is the
@@ -1071,6 +1896,72 @@ type PoolRegion = 'cn' | 'global';
1071
1896
  /** How the pool spreads requests across its accounts. */
1072
1897
  type PoolDistribution = 'priority' | 'round-robin' | 'balanced';
1073
1898
  //#endregion
1899
+ //#region src/web-status.d.ts
1900
+ /** Constructor dependencies — a narrow slice of the pool runtime. */
1901
+ interface PoolStatusRouteOptions {
1902
+ pool: WorkBuddyAccountPool;
1903
+ /** One catalog per region; the card reads the one for its active tab. */
1904
+ catalogs: Readonly<Record<PoolRegion, WorkBuddyCatalog>>;
1905
+ client: WorkBuddyUpstreamClient;
1906
+ /** Lazily resolve the running loopback shim, when it has bound a port. */
1907
+ shim?: () => {
1908
+ running: boolean;
1909
+ baseUrl?: string;
1910
+ };
1911
+ /**
1912
+ * The daily-points automation, when the host half has one.
1913
+ *
1914
+ * Optional so these routes still mount on a profile that assembled a pool
1915
+ * without a scheduler (the CLI and the tests do exactly that); the card then
1916
+ * reports the automation as off instead of showing a broken panel.
1917
+ */
1918
+ scheduler?: () => AutomationStatus;
1919
+ /**
1920
+ /**
1921
+ * Start a manual pass, for the card's "run now" button.
1922
+ *
1923
+ * Returns whether a run STARTED, not its result: a pass takes tens of
1924
+ * seconds, so it runs in the background and the card polls the scheduler
1925
+ * status for progress and earnings.
1926
+ */
1927
+ runAutomation?: (job: string, force: boolean) => boolean;
1928
+ /**
1929
+ * Persist one account's reserved-credit floor. Same settings document as every
1930
+ * other card write, so it survives a restart and is re-applied after a scan.
1931
+ * Absent without a settings service: the route then answers 503.
1932
+ */
1933
+ setCreditReserve?: (accountId: string, reserve: number) => Promise<void> | void;
1934
+ /**
1935
+ * Persist the user's model selection. Provided by the host half, which owns
1936
+ * the settings section; absent when the plugin runs without a settings
1937
+ * service (the save route then reports 503 rather than pretending to work).
1938
+ */
1939
+ /**
1940
+ * Persist one region's selection. The region travels with the payload: the two
1941
+ * gateways advertise different rosters, so the domestic tab and the
1942
+ * international tab each own their list and must never overwrite each other.
1943
+ */
1944
+ saveSelection?: (region: PoolRegion, selection: PoolWebModelSelection) => Promise<void> | void;
1945
+ /**
1946
+ * Persist one account switch. Same settings document as every other card
1947
+ * write, so it survives a restart and is re-applied after each re-scan.
1948
+ * Absent without a settings service: the route then answers 503.
1949
+ */
1950
+ setAccountDisabled?: (accountId: string, disabled: boolean) => Promise<void> | void;
1951
+ }
1952
+ /**
1953
+ * Assemble the card's status document. Per-account credits and check-in state
1954
+ * are queried live; a failing query degrades to `creditsError` / `checkinError`
1955
+ * rather than failing the whole document. Never throws.
1956
+ */
1957
+ export declare function poolWebStatus(deps: PoolStatusRouteOptions, region?: PoolRegion): Promise<PoolWebStatus>;
1958
+ /**
1959
+ * Mount the read-only routes on a context where `webServer` is available. The
1960
+ * caller uses `ctx.inject(['webServer'], ...)` so Desktop startup order cannot
1961
+ * make this registration disappear.
1962
+ */
1963
+ export declare function registerPoolStatusRoute(ctx: Context$1, deps: PoolStatusRouteOptions): void;
1964
+ //#endregion
1074
1965
  //#region src/index.d.ts
1075
1966
  /** Stable Cordis plugin name. */
1076
1967
  export declare const name = "llm-workbuddy-xdpool";
@@ -1092,11 +1983,16 @@ export interface Config {
1092
1983
  /**
1093
1984
  * How the pool spreads requests across accounts.
1094
1985
  *
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`.
1986
+ * - `priority` (default) drains one account before moving to the next, which
1987
+ * is what a pool of your own accounts is for.
1988
+ * - `round-robin` walks the pool in order, so the spend splits evenly.
1989
+ * - `balanced` draws at random, weighting whichever account has been idle
1990
+ * longest. Spend still spreads, but without a fixed order, so one unhealthy
1991
+ * account cannot pin the pool to itself.
1992
+ *
1993
+ * Absent reads as `priority`.
1098
1994
  */
1099
- distribution?: 'priority' | 'round-robin';
1995
+ distribution?: 'priority' | 'round-robin' | 'balanced';
1100
1996
  /**
1101
1997
  * Account ids switched off on the card. A disabled account is never picked
1102
1998
  * to serve a request, but it stays in the pool and on the card so it can be
@@ -1104,6 +2000,12 @@ export interface Config {
1104
2000
  * survive re-scans (see WorkBuddyAccountPool.disabledIds).
1105
2001
  */
1106
2002
  disabledAccountIds?: string[];
2003
+ /**
2004
+ * Per-account credit floor, keyed by account id. The pool stops picking an
2005
+ * account once its last known balance reaches this value, so the reserved
2006
+ * credits survive. Absent or 0 spends the account down as before.
2007
+ */
2008
+ creditReserves?: Record<string, number>;
1107
2009
  /**
1108
2010
  * Model ids enabled in the picker. Absent means "every model the catalog
1109
2011
  * advertises" — an unconfigured install should never present an empty model
@@ -1136,6 +2038,13 @@ export interface Config {
1136
2038
  * fresh install with background traffic.
1137
2039
  */
1138
2040
  automation?: AutomationConfig;
2041
+ /**
2042
+ * The automation's daily earnings ledger, written by the scheduler itself.
2043
+ *
2044
+ * It lives in settings rather than only in memory so a host restart mid-day
2045
+ * does not wipe what the automation already earned.
2046
+ */
2047
+ automationEarnings?: AutomationLedger;
1139
2048
  }
1140
2049
  /** One region's saved model selection. */
1141
2050
  export interface ModelSelectionConfig {
@@ -1166,6 +2075,8 @@ export interface AutomationConfig {
1166
2075
  taskHours?: number[];
1167
2076
  /** Hours at which streak redemption runs. */
1168
2077
  streakHours?: number[];
2078
+ /** Hours at which the buddy travel loop runs. */
2079
+ travelHours?: number[];
1169
2080
  /** How long an account rests after its credits run out, in milliseconds. */
1170
2081
  exhaustCooldownMs?: number;
1171
2082
  }
@@ -1238,4 +2149,4 @@ export declare function createCore(logger?: {
1238
2149
  */
1239
2150
  export declare function apply(ctx: Context, config?: Config): void;
1240
2151
  //#endregion
1241
- export type { AccountStatus, Context, ModelSelection, PoolWebCheckin, PoolWebCheckinClaim, PoolWebModel, PoolWebModelSelection, PoolWebStatus, UpstreamErrorKind, WorkBuddyAccount, WorkBuddyAdapter, WorkBuddyCredential, WorkBuddyModelInfo, WorkBuddyShim, WorkBuddyStatus };
2152
+ 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 };