dsh-workbuddy-xdpool 1.0.0 → 1.1.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
@@ -102,6 +102,32 @@ export declare function classifyUpstreamError(status: number, body: string): Ups
102
102
  * form, so the pool can resume exactly when the window reopens.
103
103
  */
104
104
  export declare function parseRateLimitReset(body: string): number | undefined;
105
+ /** One growth-centre task, flattened from the upstream's loosely-shaped entry. */
106
+ interface WorkBuddyTask {
107
+ /** Upstream task code; the claim path is built from it. */
108
+ taskCode: string;
109
+ title: string;
110
+ /** Reward in credits, when the task declares one. */
111
+ credit: number;
112
+ /** Reward in energy, when the task declares one. */
113
+ energy: number;
114
+ /** Whether the task carries any reward at all. */
115
+ hasReward: boolean;
116
+ /** Progress target; 0 is a valid value (a task with no counter). */
117
+ target: number;
118
+ /** Current progress; 0 is a valid value. */
119
+ current: number;
120
+ /** Upstream enrolment state: not_accepted / accepted / claimed. */
121
+ acceptStatus: string;
122
+ /** Upstream task state, e.g. `complete`. */
123
+ status: string;
124
+ /** Progress reached its target and the reward is still outstanding. */
125
+ claimable: boolean;
126
+ /** Reward already collected. */
127
+ claimed: boolean;
128
+ /** Upstream marked the task locked (not yet reachable). */
129
+ locked: boolean;
130
+ }
105
131
  export declare class WorkBuddyUpstreamClient {
106
132
  private readonly fetchImpl;
107
133
  private readonly clientVersion;
@@ -139,6 +165,40 @@ export declare class WorkBuddyUpstreamClient {
139
165
  fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
140
166
  /** Claim today's check-in reward. The browser route guards this mutation. */
141
167
  claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
168
+ /**
169
+ * Report one chat-activity event to the growth system.
170
+ *
171
+ * The body is an ARRAY holding a single `chat_request_send` event, and every
172
+ * field is filled in: a three-field minimal event is accepted with 200 and
173
+ * 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.
176
+ *
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`
179
+ * family, which is why this runs before the task-centre pass.
180
+ */
181
+ reportActivity(credential: WorkBuddyCredential, conversationId?: string): Promise<void>;
182
+ /**
183
+ * Read back the growth streak in days.
184
+ *
185
+ * This is the read-only oracle for {@link reportActivity}: a report that
186
+ * returned 200 yet left the streak untouched was silently dropped (a missing
187
+ * `userId` is the usual cause), so callers verify instead of trusting the
188
+ * status code.
189
+ *
190
+ * Two shape traps, both measured against the live upstream:
191
+ *
192
+ * - The path carries NO `/v2` prefix, unlike its sibling task endpoints under
193
+ * `/v2/activity/growth/*`. Asking for the `/v2` form does not 404; it
194
+ * answers with a body that carries no `streak` object at all.
195
+ * - The counter is nested as `data.streak.days`, not `data.days`. Reading the
196
+ * flat field yields a constant 0, which would make every successful report
197
+ * look like a silent drop.
198
+ *
199
+ * Returns 0 only when the field is genuinely absent.
200
+ */
201
+ growthStreakDays(credential: WorkBuddyCredential): Promise<number>;
142
202
  /** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
143
203
  credits(credential: WorkBuddyCredential): Promise<{
144
204
  ok: true;
@@ -147,6 +207,40 @@ export declare class WorkBuddyUpstreamClient {
147
207
  ok: false;
148
208
  message: string;
149
209
  }>;
210
+ /**
211
+ * Fetch the growth task list for one account.
212
+ *
213
+ * The upstream answers `data.tasks[]`, and `claimable` is derived locally —
214
+ * the upstream does not mark it. Only a task whose progress reached its
215
+ * target and that is not already claimed counts as eligible.
216
+ */
217
+ listTasks(credential: WorkBuddyCredential): Promise<readonly WorkBuddyTask[]>;
218
+ /**
219
+ * Accept (enrol in) tasks by code.
220
+ *
221
+ * Accepting is the "sign up" half: it produces no progress by itself, and the
222
+ * upstream answers success for an already-accepted task, so replaying this is
223
+ * safe. Progress is lit by real activity (a chat, an activity report).
224
+ */
225
+ acceptTasks(credential: WorkBuddyCredential, taskCodes: readonly string[]): Promise<void>;
226
+ /**
227
+ * Claim one task's reward.
228
+ *
229
+ * Two details differ from list/accept and are load-bearing:
230
+ *
231
+ * - The task code rides the PATH, not the body.
232
+ * - It is served by the web origin, not the chat host, and only when the
233
+ * request carries the growth-centre Origin/Referer plus
234
+ * `x-client-platform: web`. The chat host's `/reward/claim` path does not
235
+ * exist and answers 400 "task not completed".
236
+ *
237
+ * A repeat claim answers `already_claimed` with zero credit, which is treated
238
+ * as success so the caller can stay idempotent.
239
+ */
240
+ claimTaskReward(credential: WorkBuddyCredential, taskCode: string): Promise<{
241
+ credit: number;
242
+ energy: number;
243
+ }>;
150
244
  }
151
245
  //#endregion
152
246
  //#region src/accounts.d.ts
@@ -240,6 +334,8 @@ interface AccountPoolOptions {
240
334
  authDirs?: readonly string[];
241
335
  /** How long a rate-limited account stays out of rotation. */
242
336
  cooldownMs?: number;
337
+ /** How long an account rests after its credits run out (default 30 minutes). */
338
+ exhaustCooldownMs?: number;
243
339
  /** Upstream client used to refresh near-expiry tokens. */
244
340
  client?: TokenRefresher;
245
341
  /** Refresh this long before actual expiry; default five minutes. */
@@ -260,6 +356,12 @@ export declare class WorkBuddyAccountPool {
260
356
  private readonly logger;
261
357
  private authDirs;
262
358
  private cooldownMs;
359
+ /**
360
+ * How long an account stays out of rotation after the upstream reports its
361
+ * credits are spent. Credit packs reset on their own schedule rather than on a
362
+ * rate-limit window, so this is much longer than `cooldownMs`.
363
+ */
364
+ private exhaustCooldownMs;
263
365
  private readonly client;
264
366
  private readonly refreshMarginMs;
265
367
  private accounts;
@@ -296,6 +398,7 @@ export declare class WorkBuddyAccountPool {
296
398
  applyConfig(options: {
297
399
  authDirs?: readonly string[];
298
400
  cooldownMs?: number;
401
+ exhaustCooldownMs?: number;
299
402
  distribution?: AccountDistribution;
300
403
  disabledAccountIds?: readonly string[];
301
404
  }): void;
@@ -358,6 +461,27 @@ export declare class WorkBuddyAccountPool {
358
461
  isDisabled(accountId: string): boolean;
359
462
  /** Every account id the user switched off, in discovery order. */
360
463
  disabledIdsInOrder(): string[];
464
+ /**
465
+ * Record that an account actually served a request.
466
+ *
467
+ * Called by the shim once the upstream answers 200 — only then is the account
468
+ * the one the user is really being served by. `balanced` mode reads the same map
469
+ * for its idle weighting, so a request that failed over to another account must
470
+ * not count as used for the account that was merely tried.
471
+ */
472
+ noteServed(accountId: string): void;
473
+ /**
474
+ * The account that served the most recent request, if any.
475
+ *
476
+ * Distinct from "who would serve the next one": this is a record of what
477
+ * actually happened, which is what the card needs to answer "which account am
478
+ * I using right now?". Under `balanced` there is no deterministic next account
479
+ * at all, so a recorded fact is the only honest answer.
480
+ *
481
+ * Returns undefined before the first request of the process, and after every
482
+ * known account has been re-scanned away (a login swapped out under us).
483
+ */
484
+ lastServedId(): string | undefined;
361
485
  /** Best-effort refresh of one account after a session-dead upstream answer. */
362
486
  refreshAccount(accountId: string): Promise<void>;
363
487
  /**
@@ -367,6 +491,15 @@ export declare class WorkBuddyAccountPool {
367
491
  * endpoint never takes down a working session.
368
492
  */
369
493
  private ensureFresh;
494
+ /**
495
+ * Cool a whole account after the upstream reports its credits are spent.
496
+ *
497
+ * Credit exhaustion is an ACCOUNT condition, unlike a model rate limit: every
498
+ * model on that account is unusable until the quota resets, so this cools the
499
+ * account as a whole (no `modelId`) for the configured exhaustion window. The
500
+ * shim then rotates to a different account instead of failing the request.
501
+ */
502
+ penalizeExhausted(accountId: string): void;
370
503
  /**
371
504
  * Mark an account (or one of its models) rate-limited.
372
505
  *
@@ -502,6 +635,161 @@ interface WorkBuddyAdapter {
502
635
  */
503
636
  export declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
504
637
  //#endregion
638
+ //#region src/scheduler.d.ts
639
+ /** Which jobs the automation runs, and when. */
640
+ interface AutomationOptions {
641
+ /** Master switch; false stops every job. */
642
+ enabled?: boolean;
643
+ /** Hour list for the daily check-in job. */
644
+ checkinHours?: readonly number[];
645
+ /** Hour list for the task-centre job (accept + claim). */
646
+ taskHours?: readonly number[];
647
+ /** Hour list for the activity-report job. */
648
+ reportHours?: readonly number[];
649
+ /** Hour list for the streak-redemption job. */
650
+ streakHours?: readonly number[];
651
+ /** Per-account serial delay, in milliseconds. */
652
+ accountDelayMs?: number;
653
+ /** Override the clock, for tests. */
654
+ now?: () => Date;
655
+ /** Logger; defaults to a no-op so tests stay quiet. */
656
+ logger?: SchedulerLogger;
657
+ }
658
+ /** Logger surface, kept structural so any host logger fits. */
659
+ interface SchedulerLogger {
660
+ info?(...args: unknown[]): void;
661
+ warn?(...args: unknown[]): void;
662
+ }
663
+ /** One job's last-run record, surfaced on the status document. */
664
+ interface AutomationJobState {
665
+ /** `YYYY-MM-DD` of the last completed run, or undefined if it never ran. */
666
+ lastRunDate?: string;
667
+ /** Epoch ms of the last completed run. */
668
+ lastRunAtMs?: number;
669
+ /** Accounts that completed without throwing. */
670
+ ok: number;
671
+ /** Accounts that threw (each one skipped, the run continued). */
672
+ failed: number;
673
+ /** Credits claimed by the task job on the last run. */
674
+ credit: number;
675
+ /** Energy claimed by the task job on the last run. */
676
+ energy: number;
677
+ /** Tasks claimed by the task job on the last run. */
678
+ claimed: number;
679
+ /** Human-readable summary of the last run. */
680
+ message?: string;
681
+ }
682
+ /** Automation snapshot for the status document and the card. */
683
+ interface AutomationStatus {
684
+ enabled: boolean;
685
+ /** Whether the loop is running. */
686
+ running: boolean;
687
+ checkinHours: readonly number[];
688
+ taskHours: readonly number[];
689
+ reportHours: readonly number[];
690
+ streakHours: readonly number[];
691
+ jobs: {
692
+ checkin: AutomationJobState;
693
+ report: AutomationJobState;
694
+ tasks: AutomationJobState;
695
+ streak: AutomationJobState;
696
+ };
697
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
698
+ claimableSeen: number;
699
+ }
700
+ /**
701
+ * The points automation.
702
+ *
703
+ * Owns a single timer loop. Construction is inert — nothing runs until
704
+ * {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
705
+ * twice is harmless.
706
+ */
707
+ declare class WorkBuddyScheduler {
708
+ private readonly pool;
709
+ private readonly client;
710
+ private readonly logger;
711
+ private readonly now;
712
+ private readonly delayMs;
713
+ private enabled;
714
+ private checkinHours;
715
+ private taskHours;
716
+ private reportHours;
717
+ private streakHours;
718
+ private timer;
719
+ private running;
720
+ /** Guards against a slow run overlapping the next tick. */
721
+ private busy;
722
+ /**
723
+ * Set once {@link stop} is called.
724
+ *
725
+ * Deliberately false before `start`: the loop is not running yet, but a
726
+ * manual `tick` must still work. `stop` is what makes a run abandon the
727
+ * accounts it has not reached yet.
728
+ */
729
+ private stopped;
730
+ private readonly states;
731
+ private claimableSeen;
732
+ constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
733
+ /** Apply a new configuration; safe to call while running. */
734
+ applyConfig(options: AutomationOptions): void;
735
+ /** Hours for one job, used by the loop and the status document. */
736
+ private hoursOf;
737
+ /** Start the loop. Idempotent. */
738
+ start(): void;
739
+ /** Stop the loop. Idempotent, and safe before `start`. */
740
+ stop(): void;
741
+ /** Snapshot for the status document. */
742
+ status(): AutomationStatus;
743
+ /**
744
+ * One poll: run every due job, serially.
745
+ *
746
+ * Serial by design — the jobs share the same accounts and the upstream
747
+ * rate-limits per account, so overlapping passes would only trip that limit.
748
+ * A job that throws is recorded and the loop continues.
749
+ */
750
+ private tick;
751
+ /** Run one job against every eligible account and record the outcome. */
752
+ private runJob;
753
+ /** Compose the one-line summary shown on the card. */
754
+ private summarise;
755
+ /**
756
+ * Accounts to run against, in pool order.
757
+ *
758
+ * Disabled accounts are excluded here rather than filtered by the caller so a
759
+ * card switch takes effect on the next pass without any event plumbing.
760
+ */
761
+ private accountsInOrder;
762
+ /**
763
+ * Send one activity report, then verify it landed.
764
+ *
765
+ * The upstream answers 200 even when it drops the event, so the streak is
766
+ * read back as the oracle: `days > 0` means it counted. A failed read-back is
767
+ * logged and treated as a suspicious result, never as a retry — the report is
768
+ * idempotent per day, and hammering it is exactly what the one-a-day quota
769
+ * exists to avoid.
770
+ */
771
+ private reportOne;
772
+ /**
773
+ * The task-centre pass for one account.
774
+ *
775
+ * Order matters: enrich first (enrol in everything open), then claim. Both
776
+ * halves are idempotent — accepting an already-accepted task succeeds, and a
777
+ * repeat claim answers `already_claimed` — so a pass that dies halfway is
778
+ * safe to replay on the next tick.
779
+ */
780
+ private runTasks;
781
+ /**
782
+ * Streak redemption and lottery.
783
+ *
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.
789
+ */
790
+ private redeemStreak;
791
+ }
792
+ //#endregion
505
793
  //#region src/status.d.ts
506
794
  /** One account's status row. */
507
795
  interface AccountStatus {
@@ -728,6 +1016,51 @@ interface PoolWebStatus {
728
1016
  running: boolean;
729
1017
  baseUrl?: string;
730
1018
  };
1019
+ /** Daily-points automation state, so the card can show what ran and when. */
1020
+ automation: PoolWebAutomation;
1021
+ }
1022
+ /** One automation job's last run, as shown on the card. */
1023
+ interface PoolWebAutomationJob {
1024
+ /** `YYYY-MM-DD` of the last run in this process, if it has run. */
1025
+ lastRunDate?: string;
1026
+ /** Accounts that finished without error on the last run. */
1027
+ ok: number;
1028
+ /** Accounts that failed on the last run (each one skipped, the run continued). */
1029
+ failed: number;
1030
+ /** Credits claimed by the task job on the last run. */
1031
+ credit: number;
1032
+ /** Energy claimed by the task job on the last run. */
1033
+ energy: number;
1034
+ /** Tasks claimed by the task job on the last run. */
1035
+ claimed: number;
1036
+ /** One-line summary of the last run. */
1037
+ message?: string;
1038
+ }
1039
+ /**
1040
+ * Automation block on the status document.
1041
+ *
1042
+ * Carries the schedule and each job's last outcome so the card can answer
1043
+ * "is it on, when does it run, and what did it last do" without reaching into
1044
+ * the scheduler itself.
1045
+ */
1046
+ interface PoolWebAutomation {
1047
+ /** Master switch, mirrored from the saved config. */
1048
+ enabled: boolean;
1049
+ /** Whether the loop is currently running. */
1050
+ running: boolean;
1051
+ /** Configured hours per job, so the card can show the schedule. */
1052
+ checkinHours: readonly number[];
1053
+ reportHours: readonly number[];
1054
+ taskHours: readonly number[];
1055
+ streakHours: readonly number[];
1056
+ jobs: {
1057
+ checkin: PoolWebAutomationJob;
1058
+ report: PoolWebAutomationJob;
1059
+ tasks: PoolWebAutomationJob;
1060
+ streak: PoolWebAutomationJob;
1061
+ };
1062
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
1063
+ claimableSeen: number;
731
1064
  }
732
1065
  /**
733
1066
  * The two gateways, matching the provider ids the host registers. `cn` is the
@@ -797,6 +1130,12 @@ export interface Config {
797
1130
  */
798
1131
  modelSelectionCn?: ModelSelectionConfig;
799
1132
  modelSelectionGlobal?: ModelSelectionConfig;
1133
+ /**
1134
+ * Daily-points automation. Absent means off: the scheduler makes upstream
1135
+ * calls on the user behalf, so it stays opt-in rather than surprising a
1136
+ * fresh install with background traffic.
1137
+ */
1138
+ automation?: AutomationConfig;
800
1139
  }
801
1140
  /** One region's saved model selection. */
802
1141
  export interface ModelSelectionConfig {
@@ -804,8 +1143,42 @@ export interface ModelSelectionConfig {
804
1143
  imageModelIds?: string[];
805
1144
  contextBudgets?: Record<string, number>;
806
1145
  }
1146
+ /**
1147
+ * Daily-points automation.
1148
+ *
1149
+ * Absent means off: the scheduler makes upstream calls on the user behalf, so
1150
+ * it stays opt-in rather than surprising a fresh install with background
1151
+ * traffic. Each job carries its own hour list so the passes can be spread out
1152
+ * (or pushed off-peak) without disabling any of them.
1153
+ *
1154
+ * Ordering note: the report job must run before the task job. A report is what
1155
+ * lights the growth streak and unlocks the `first_buddy` family, so a task pass
1156
+ * that ran first would read counters before they could have moved.
1157
+ */
1158
+ export interface AutomationConfig {
1159
+ /** Master switch for every automation job. Absent reads as false. */
1160
+ enabled?: boolean;
1161
+ /** Hours (local, 0-23) at which the daily check-in runs. */
1162
+ checkinHours?: number[];
1163
+ /** Hours at which the activity report runs. Keep ahead of `taskHours`. */
1164
+ reportHours?: number[];
1165
+ /** Hours at which tasks are enrolled in and claimed. */
1166
+ taskHours?: number[];
1167
+ /** Hours at which streak redemption runs. */
1168
+ streakHours?: number[];
1169
+ /** How long an account rests after its credits run out, in milliseconds. */
1170
+ exhaustCooldownMs?: number;
1171
+ }
807
1172
  /** Upper bound the card offers as the "default" context window, in tokens. */
808
1173
  export declare const DEFAULT_CONTEXT_BUDGET = 200000;
1174
+ /**
1175
+ * Fold a saved automation block into scheduler options.
1176
+ *
1177
+ * Absent means off, stated once here so every caller agrees: the card writes
1178
+ * `enabled` as a real boolean, and a config that never touched the section must
1179
+ * not accidentally arm background upstream traffic.
1180
+ */
1181
+ export declare function automationOptions(automation: AutomationConfig | undefined): AutomationOptions;
809
1182
  /** Settings key holding one region's saved selection. */
810
1183
  export declare const modelSelectionKeyFor: (region: 'cn' | 'global') => string;
811
1184
  /**
@@ -830,6 +1203,8 @@ export interface WorkBuddyPoolApi {
830
1203
  rescan(): Promise<number>;
831
1204
  status(includeCredits?: boolean): Promise<Awaited<ReturnType<typeof buildStatus>>>;
832
1205
  resetCooldowns(): void;
1206
+ /** Daily-points automation; assembled with the core, inert until started. */
1207
+ scheduler: WorkBuddyScheduler;
833
1208
  }
834
1209
  /** The live API, or undefined when the plugin has not applied yet. */
835
1210
  export declare function currentApi(): WorkBuddyPoolApi | undefined;
@@ -846,6 +1221,7 @@ export declare function setApi(next: WorkBuddyPoolApi | undefined): void;
846
1221
  */
847
1222
  export declare function createCore(logger?: {
848
1223
  warn(...args: unknown[]): void;
1224
+ info?(...args: unknown[]): void;
849
1225
  }): {
850
1226
  pool: WorkBuddyAccountPool;
851
1227
  catalogs: {
@@ -853,6 +1229,7 @@ export declare function createCore(logger?: {
853
1229
  readonly global: WorkBuddyCatalog;
854
1230
  };
855
1231
  client: WorkBuddyUpstreamClient;
1232
+ scheduler: WorkBuddyScheduler;
856
1233
  };
857
1234
  /**
858
1235
  * Start the loopback endpoint, register the `workbuddy-xdpool` provider, and