@workerdeck/server 2.8.0 → 2.10.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/build/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { IncomingMessage, Server, ServerResponse } from "node:http";
2
- import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
2
+ import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, CostLedgerState, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
3
3
  import { JobQueue, QueueAdapter } from "@workerdeck/queue";
4
- import { CreateSessionRequest, JobEvent, MessageAttachment, ProfileEngine, ProfileInfo, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
4
+ import { ByModel, CreateSessionRequest, JobEvent, MessageAttachment, ProfileEngine, ProfileInfo, ProfileSpend, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
5
5
  //#region src/services/bridge.d.ts
6
6
  type BridgeHubOptions = {
7
7
  timeoutMs?: number;
@@ -29,21 +29,22 @@ declare class SessionNotifier {
29
29
  #private;
30
30
  constructor(options: SessionNotificationOptions);
31
31
  get idle(): boolean;
32
- watch(runner: Runner, afterSeq?: number): void;
32
+ watch(runner: Runner, afterSeq?: number): () => void;
33
33
  }
34
34
  //#endregion
35
35
  //#region src/services/registry.d.ts
36
36
  type SessionRegistryOptions = {
37
- onRegister?: (runner: Runner) => void;
37
+ onRegister?: (runner: Runner) => unknown;
38
38
  };
39
39
  declare class SessionRegistry {
40
40
  #private;
41
41
  constructor(options?: SessionRegistryOptions);
42
- observe(listener: (runner: Runner) => void): () => void;
42
+ observe(listener: (runner: Runner) => unknown): () => void;
43
43
  create(config: SessionRunnerConfig): Runner;
44
44
  prepare(config: SessionRunnerConfig): Runner;
45
45
  adopt(runner: Runner): Runner;
46
46
  register(runner: Runner): Runner;
47
+ retain(id: string, detach: () => void): void;
47
48
  get(id: string): Runner | undefined;
48
49
  list(): SessionInfo[];
49
50
  remove(id: string): boolean;
@@ -60,6 +61,7 @@ type ParkedSessionRecord = {
60
61
  config: SessionRunnerConfig;
61
62
  snapshot: RunnerSnapshot;
62
63
  executions: ParkedExecution[];
64
+ cost?: CostLedgerState;
63
65
  parkedAt: number;
64
66
  };
65
67
  type DormantSessionRecord = {
@@ -69,9 +71,11 @@ type DormantSessionRecord = {
69
71
  profile?: string;
70
72
  config: SessionRunnerConfig;
71
73
  sdkSessionId: string;
74
+ cost?: CostLedgerState;
72
75
  savedAt: number;
73
76
  };
74
77
  type StoredSessionRecord = ParkedSessionRecord | DormantSessionRecord;
78
+ declare function isDormant(record: StoredSessionRecord): record is DormantSessionRecord;
75
79
  interface SessionStore {
76
80
  save(record: StoredSessionRecord): Promise<void>;
77
81
  get(id: string): Promise<StoredSessionRecord | null>;
@@ -119,6 +123,9 @@ declare class SessionParkManager {
119
123
  touch(runner: Runner): void;
120
124
  hydrate(): Promise<void>;
121
125
  watch(runner: Runner, afterSeq?: number): () => void;
126
+ release(sessionId: string): SessionRunnerConfig | undefined;
127
+ adopt(runner: Runner, config?: SessionRunnerConfig): void;
128
+ flush(sessionId?: string): Promise<void>;
122
129
  onDetach(sessionId: string): void;
123
130
  sessionFor(executionId: string): string | undefined;
124
131
  get(id: string): Promise<StoredSessionRecord | null>;
@@ -141,6 +148,33 @@ type ProfileStore = {
141
148
  declare function createMemoryProfileStore(seed?: ProfileInfo[]): ProfileStore;
142
149
  declare function createFileProfileStore(path?: string): ProfileStore;
143
150
  //#endregion
151
+ //#region src/services/spend-ledger.d.ts
152
+ type SpendRecord = {
153
+ days: Record<string, Record<string, ByModel>>;
154
+ };
155
+ type SpendStore = {
156
+ load(): Promise<SpendRecord | undefined>;
157
+ save(record: SpendRecord): Promise<void>;
158
+ };
159
+ type SpendLedgerOptions = {
160
+ store?: SpendStore;
161
+ monthlySubscriptionUsd?: (profile: string) => number | undefined;
162
+ retentionDays?: number;
163
+ saveDebounceMs?: number;
164
+ onError?: (error: unknown) => void;
165
+ };
166
+ declare function dayKey(at: number): string;
167
+ declare class SpendLedger {
168
+ #private;
169
+ constructor(options?: SpendLedgerOptions);
170
+ load(): Promise<void>;
171
+ watch(runner: Runner): () => void;
172
+ spend(profile: string, now?: number): ProfileSpend | undefined;
173
+ flush(): Promise<void>;
174
+ close(): void;
175
+ }
176
+ declare function createFileSpendStore(path: string, onError?: (error: unknown) => void): SpendStore;
177
+ //#endregion
144
178
  //#region src/options.d.ts
145
179
  type SdkSessionLister = (options: {
146
180
  dir?: string;
@@ -198,6 +232,11 @@ type WorkerServerOptions = {
198
232
  persistLive?: boolean;
199
233
  onError?: (error: unknown, context: ParkErrorContext) => void;
200
234
  };
235
+ spend?: {
236
+ store?: SpendStore;
237
+ monthlySubscriptionUsd?: Record<string, number>;
238
+ onError?: (error: unknown) => void;
239
+ };
201
240
  createEngineRunner?: (context: EngineRunnerContext) => Runner | Promise<Runner>;
202
241
  engines?: Partial<Record<ProfileEngine, EngineAdapter>>;
203
242
  };
@@ -223,6 +262,10 @@ type QueueServerOptions = {
223
262
  webhookRetryDelayMs?: number;
224
263
  onEvent?: (event: JobEvent) => void;
225
264
  };
265
+ type CarriedSession = {
266
+ runner: Runner;
267
+ config?: SessionRunnerConfig;
268
+ };
226
269
  type DrainReport = {
227
270
  working: string[];
228
271
  awaitingHuman: string[];
@@ -243,6 +286,8 @@ type WorkerServer = {
243
286
  port: number;
244
287
  }>;
245
288
  drain: (options?: DrainOptions) => Promise<DrainReport>;
289
+ releaseSession: (id: string) => CarriedSession | undefined;
290
+ adoptSession: (carried: CarriedSession) => boolean;
246
291
  close: () => Promise<void>;
247
292
  };
248
293
  //#endregion
@@ -281,6 +326,10 @@ type ProviderRunnerOptions = {
281
326
  };
282
327
  declare function createProviderRunner(ctx: EngineRunnerContext, options: ProviderRunnerOptions): Promise<Runner>;
283
328
  //#endregion
329
+ //#region src/lib/reload-plan.d.ts
330
+ type ReloadPlan = 'carry' | 'persist' | 'drop';
331
+ declare function reloadPlan(runner: Runner): ReloadPlan;
332
+ //#endregion
284
333
  //#region src/services/attachments.d.ts
285
334
  type AttachmentStoreOptions = {
286
335
  maxFileBytes?: number;
@@ -332,7 +381,7 @@ type ProducedFile = {
332
381
  };
333
382
  declare class ProducedFileStore {
334
383
  #private;
335
- watch(runner: Runner): void;
384
+ watch(runner: Runner): () => void;
336
385
  get(sessionId: string, fileId: string): ProducedFile | undefined;
337
386
  list(sessionId: string): ProducedFile[];
338
387
  drop(sessionId: string): void;
@@ -341,9 +390,9 @@ declare class ProducedFileStore {
341
390
  //#region src/services/profile-usage.d.ts
342
391
  declare class ProfileUsageTracker {
343
392
  #private;
344
- watch(runner: Runner): void;
393
+ watch(runner: Runner): () => void;
345
394
  usage(profile: string, now?: number): ProfileUsage | undefined;
346
395
  }
347
396
  //#endregion
348
- export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProducedFile, ProducedFileStore, type ProfileStore, ProfileUsageTracker, type ProviderRunnerOptions, type QueueServerOptions, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, sandboxedProviderProfile, toDurableRecord };
397
+ export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type CarriedSession, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProducedFile, ProducedFileStore, type ProfileStore, ProfileUsageTracker, type ProviderRunnerOptions, type QueueServerOptions, type ReloadPlan, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, SpendLedger, type SpendRecord, type SpendStore, type StoredSessionRecord, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, toDurableRecord };
349
398
  //# sourceMappingURL=index.d.mts.map
package/build/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { createServer } from "node:http";
2
2
  import { WebSocketServer } from "ws";
3
3
  import { BrowserBridgeExecutor, SessionRunner, attachmentKind, checkClaudeAuth, createEngineSession, getEngineAdapter, normalizeMediaType } from "@workerdeck/core";
4
4
  import { JobQueue } from "@workerdeck/queue";
5
- import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SHELL_COMMAND_MAX, imagePartRef, sessionState, supportsPermissionMode } from "@workerdeck/protocol";
5
+ import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SHELL_COMMAND_MAX, costOfByModel, imagePartRef, isJobRun, mergeByModel, sessionState, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { constants as constants$1, homedir } from "node:os";
@@ -211,6 +211,15 @@ function parseSessionRoute(basePath, url) {
211
211
  return null;
212
212
  }
213
213
  //#endregion
214
+ //#region src/lib/reload-plan.ts
215
+ function reloadPlan(runner) {
216
+ const info = runner.info();
217
+ if (info.status === "closed" || info.status === "failed") return "drop";
218
+ if (isJobRun(info)) return "drop";
219
+ if (runner.snapshot !== void 0) return "persist";
220
+ return "carry";
221
+ }
222
+ //#endregion
214
223
  //#region src/routes/executions.ts
215
224
  async function handleExecutionResult(ctx, req, res, pathname, auth) {
216
225
  const { auth: authSvc, basePath, parking, registry } = ctx;
@@ -526,7 +535,7 @@ function subsequenceScore(haystack, needle) {
526
535
  return score - haystack.length / 100;
527
536
  }
528
537
  //#endregion
529
- //#region src/routes/host-files.ts
538
+ //#region src/routes/fs.ts
530
539
  function kindRank(type) {
531
540
  return type === "dir" ? 0 : 1;
532
541
  }
@@ -1808,6 +1817,7 @@ function attachClient(ctx, ws, runner, req, access) {
1808
1817
  imageRefs
1809
1818
  });
1810
1819
  const detachBridge = bridge.attach(runner.id, send);
1820
+ runner.refreshUsage?.().catch(() => {});
1811
1821
  ws.on("message", (data) => {
1812
1822
  let frame;
1813
1823
  try {
@@ -2209,8 +2219,8 @@ var SessionNotifier = class {
2209
2219
  return !this.#options.webhook && !this.#options.onNotification;
2210
2220
  }
2211
2221
  watch(runner, afterSeq = runner.info().lastSeq) {
2212
- if (this.idle) return;
2213
- runner.subscribe((event) => {
2222
+ if (this.idle) return () => {};
2223
+ return runner.subscribe((event) => {
2214
2224
  switch (event.type) {
2215
2225
  case "permission_requested":
2216
2226
  this.#emit(runner, event.seq, event.ts, {
@@ -2301,6 +2311,7 @@ var SessionParkManager = class {
2301
2311
  #configs = /* @__PURE__ */ new Map();
2302
2312
  #remembered = /* @__PURE__ */ new Set();
2303
2313
  #storeOps = /* @__PURE__ */ new Map();
2314
+ #watches = /* @__PURE__ */ new Map();
2304
2315
  #closed = false;
2305
2316
  constructor(options) {
2306
2317
  this.#options = options;
@@ -2320,7 +2331,8 @@ var SessionParkManager = class {
2320
2331
  }
2321
2332
  }
2322
2333
  watch(runner, afterSeq = 0) {
2323
- return runner.subscribe((event) => {
2334
+ this.#watches.get(runner.id)?.();
2335
+ const unsubscribe = runner.subscribe((event) => {
2324
2336
  switch (event.type) {
2325
2337
  case "execution_dispatched":
2326
2338
  if (!event.deferred) return;
@@ -2360,6 +2372,35 @@ var SessionParkManager = class {
2360
2372
  default: return;
2361
2373
  }
2362
2374
  }, afterSeq);
2375
+ const detach = () => {
2376
+ if (this.#watches.get(runner.id) === detach) this.#watches.delete(runner.id);
2377
+ unsubscribe();
2378
+ };
2379
+ this.#watches.set(runner.id, detach);
2380
+ return detach;
2381
+ }
2382
+ release(sessionId) {
2383
+ this.#watches.get(sessionId)?.();
2384
+ this.#watches.delete(sessionId);
2385
+ clearTimeout(this.#detachTimers.get(sessionId));
2386
+ this.#detachTimers.delete(sessionId);
2387
+ const config = this.#configs.get(sessionId);
2388
+ this.#configs.delete(sessionId);
2389
+ this.#remembered.delete(sessionId);
2390
+ return config;
2391
+ }
2392
+ adopt(runner, config) {
2393
+ this.#options.registry.register(runner);
2394
+ if (config) this.remember(runner.id, config);
2395
+ this.watch(runner, runner.info().lastSeq);
2396
+ this.touch(runner);
2397
+ }
2398
+ async flush(sessionId) {
2399
+ if (sessionId === void 0) {
2400
+ await Promise.all(this.#storeOps.values());
2401
+ return;
2402
+ }
2403
+ await this.#storeOps.get(sessionId);
2363
2404
  }
2364
2405
  onDetach(sessionId) {
2365
2406
  if (this.#closed) return;
@@ -2411,6 +2452,7 @@ var SessionParkManager = class {
2411
2452
  };
2412
2453
  }
2413
2454
  async discard(sessionId) {
2455
+ this.#watches.delete(sessionId);
2414
2456
  clearTimeout(this.#detachTimers.get(sessionId));
2415
2457
  this.#detachTimers.delete(sessionId);
2416
2458
  this.#configs.delete(sessionId);
@@ -2451,6 +2493,7 @@ var SessionParkManager = class {
2451
2493
  meta: info.meta
2452
2494
  },
2453
2495
  sdkSessionId,
2496
+ cost: runner.costState?.(),
2454
2497
  savedAt: Date.now()
2455
2498
  };
2456
2499
  try {
@@ -2498,6 +2541,7 @@ var SessionParkManager = class {
2498
2541
  meta: info.meta
2499
2542
  },
2500
2543
  snapshot,
2544
+ cost: runner.costState?.(),
2501
2545
  executions: snapshot.parked,
2502
2546
  parkedAt: Date.now()
2503
2547
  };
@@ -2521,6 +2565,7 @@ var SessionParkManager = class {
2521
2565
  const config = this.#configs.get(id);
2522
2566
  if (!config) return;
2523
2567
  if (this.#options.onParking && !this.#options.onParking(id, executions[0])) return;
2568
+ const cost = runner.costState?.();
2524
2569
  const snapshot = runner.park();
2525
2570
  if (!snapshot) return;
2526
2571
  const info = {
@@ -2536,6 +2581,7 @@ var SessionParkManager = class {
2536
2581
  config,
2537
2582
  snapshot,
2538
2583
  executions: snapshot.parked,
2584
+ cost,
2539
2585
  parkedAt: Date.now()
2540
2586
  };
2541
2587
  for (const execution of snapshot.parked) this.#track(id, execution);
@@ -2581,6 +2627,10 @@ var SessionParkManager = class {
2581
2627
  });
2582
2628
  throw error;
2583
2629
  }
2630
+ runner.carryCost?.(record.cost ?? {
2631
+ byModel: record.info.usageByModel,
2632
+ reportedCostUsd: record.info.totalCostUsd
2633
+ });
2584
2634
  this.#options.registry.register(runner);
2585
2635
  this.remember(id, {
2586
2636
  ...record.config,
@@ -2639,7 +2689,7 @@ var SessionParkManager = class {
2639
2689
  var ProducedFileStore = class {
2640
2690
  #bySession = /* @__PURE__ */ new Map();
2641
2691
  watch(runner) {
2642
- runner.subscribe((event) => {
2692
+ return runner.subscribe((event) => {
2643
2693
  if (event.type !== "file_produced") return;
2644
2694
  const held = this.#bySession.get(runner.id) ?? /* @__PURE__ */ new Map();
2645
2695
  held.set(event.fileId, {
@@ -2723,6 +2773,8 @@ var ProfileService = class {
2723
2773
  }
2724
2774
  const usage = decorate.usage(p.name);
2725
2775
  if (usage) base.usage = usage;
2776
+ const spend = decorate.spend?.(p.name);
2777
+ if (spend) base.spend = spend;
2726
2778
  return base;
2727
2779
  }
2728
2780
  all() {
@@ -2790,8 +2842,8 @@ var ProfileUsageTracker = class {
2790
2842
  #profiles = /* @__PURE__ */ new Map();
2791
2843
  watch(runner) {
2792
2844
  const profile = runner.info().profile;
2793
- if (!profile) return;
2794
- runner.subscribe((event) => {
2845
+ if (!profile) return () => {};
2846
+ return runner.subscribe((event) => {
2795
2847
  if (event.type !== "rate_limit") return;
2796
2848
  const type = event.info.rateLimitType;
2797
2849
  if (!type) return;
@@ -2830,17 +2882,166 @@ function serveWindow(held, now) {
2830
2882
  };
2831
2883
  }
2832
2884
  //#endregion
2885
+ //#region src/services/spend-ledger.ts
2886
+ const DEFAULT_RETENTION_DAYS = 92;
2887
+ const DEFAULT_SAVE_DEBOUNCE_MS = 2e3;
2888
+ function dayKey(at) {
2889
+ const d = new Date(at);
2890
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
2891
+ }
2892
+ function daysBack(now, count) {
2893
+ const out = [];
2894
+ for (let i = 0; i < count; i += 1) out.push(dayKey(now - i * 864e5));
2895
+ return out;
2896
+ }
2897
+ function subtract(current, previous) {
2898
+ const delta = {};
2899
+ for (const [model, usage] of Object.entries(current)) {
2900
+ const was = previous[model];
2901
+ const row = {
2902
+ input: usage.input - (was?.input ?? 0),
2903
+ output: usage.output - (was?.output ?? 0),
2904
+ cacheWrite5m: usage.cacheWrite5m - (was?.cacheWrite5m ?? 0),
2905
+ cacheWrite1h: usage.cacheWrite1h - (was?.cacheWrite1h ?? 0),
2906
+ cacheRead: usage.cacheRead - (was?.cacheRead ?? 0)
2907
+ };
2908
+ if (Object.values(row).some((value) => value < 0)) continue;
2909
+ if (Object.values(row).some((value) => value > 0)) delta[model] = row;
2910
+ }
2911
+ return delta;
2912
+ }
2913
+ var SpendLedger = class {
2914
+ #days = {};
2915
+ #lastBanked = /* @__PURE__ */ new Map();
2916
+ #options;
2917
+ #saveTimer;
2918
+ #loaded;
2919
+ constructor(options = {}) {
2920
+ this.#options = options;
2921
+ }
2922
+ load() {
2923
+ this.#loaded ??= this.#load();
2924
+ return this.#loaded;
2925
+ }
2926
+ async #load() {
2927
+ const store = this.#options.store;
2928
+ if (!store) return;
2929
+ try {
2930
+ const record = await store.load();
2931
+ if (record?.days) this.#days = record.days;
2932
+ } catch (error) {
2933
+ this.#options.onError?.(error);
2934
+ }
2935
+ }
2936
+ watch(runner) {
2937
+ const info = runner.info();
2938
+ const profile = info.profile;
2939
+ if (!profile) return () => {};
2940
+ this.#lastBanked.set(runner.id, info.usageByModel ?? {});
2941
+ const unsubscribe = runner.subscribe((event) => {
2942
+ if (event.type !== "turn_result" || !event.usageByModel) return;
2943
+ this.#bank(profile, event.usageByModel, runner.id, event.ts);
2944
+ });
2945
+ return () => {
2946
+ unsubscribe();
2947
+ this.#lastBanked.delete(runner.id);
2948
+ };
2949
+ }
2950
+ #bank(profile, cumulative, sessionId, at) {
2951
+ const delta = subtract(cumulative, this.#lastBanked.get(sessionId) ?? {});
2952
+ this.#lastBanked.set(sessionId, cumulative);
2953
+ if (Object.keys(delta).length === 0) return;
2954
+ const key = dayKey(at);
2955
+ const day = this.#days[key] ??= {};
2956
+ day[profile] = mergeByModel(day[profile] ?? {}, delta);
2957
+ this.#scheduleSave();
2958
+ }
2959
+ spend(profile, now = Date.now()) {
2960
+ const week = this.#window(profile, daysBack(now, 7));
2961
+ const month = this.#window(profile, daysBack(now, 30));
2962
+ if (Object.keys(month).length === 0) return;
2963
+ const weekCost = costOfByModel(week);
2964
+ const monthlySubscriptionUsd = this.#options.monthlySubscriptionUsd?.(profile);
2965
+ return {
2966
+ weekUsd: weekCost.total,
2967
+ monthUsd: costOfByModel(month).total,
2968
+ weekByModel: week,
2969
+ unpricedShare: weekCost.unpricedShare,
2970
+ monthlySubscriptionUsd,
2971
+ subscription: monthlySubscriptionUsd ? subscriptionComparison(weekCost.total, monthlySubscriptionUsd) : void 0
2972
+ };
2973
+ }
2974
+ #window(profile, keys) {
2975
+ let out = {};
2976
+ for (const key of keys) {
2977
+ const day = this.#days[key]?.[profile];
2978
+ if (day) out = mergeByModel(out, day);
2979
+ }
2980
+ return out;
2981
+ }
2982
+ #scheduleSave() {
2983
+ if (!this.#options.store || this.#saveTimer) return;
2984
+ this.#saveTimer = setTimeout(() => {
2985
+ this.#saveTimer = void 0;
2986
+ this.flush();
2987
+ }, this.#options.saveDebounceMs ?? DEFAULT_SAVE_DEBOUNCE_MS);
2988
+ this.#saveTimer.unref?.();
2989
+ }
2990
+ async flush() {
2991
+ const store = this.#options.store;
2992
+ if (!store) return;
2993
+ this.#prune();
2994
+ try {
2995
+ await store.save({ days: this.#days });
2996
+ } catch (error) {
2997
+ this.#options.onError?.(error);
2998
+ }
2999
+ }
3000
+ #prune() {
3001
+ const keep = new Set(daysBack(Date.now(), this.#options.retentionDays ?? DEFAULT_RETENTION_DAYS));
3002
+ for (const key of Object.keys(this.#days)) if (!keep.has(key)) delete this.#days[key];
3003
+ }
3004
+ close() {
3005
+ clearTimeout(this.#saveTimer);
3006
+ this.#saveTimer = void 0;
3007
+ }
3008
+ };
3009
+ function createFileSpendStore(path, onError) {
3010
+ return {
3011
+ async load() {
3012
+ try {
3013
+ const parsed = JSON.parse(await readFile(path, "utf8"));
3014
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.days !== "object") return;
3015
+ return parsed;
3016
+ } catch {
3017
+ return;
3018
+ }
3019
+ },
3020
+ async save(record) {
3021
+ try {
3022
+ await mkdir(dirname(path), { recursive: true });
3023
+ const temp = `${path}.${process.pid}.tmp`;
3024
+ await writeFile(temp, JSON.stringify(record));
3025
+ await rename(temp, path);
3026
+ } catch (error) {
3027
+ onError?.(error);
3028
+ }
3029
+ }
3030
+ };
3031
+ }
3032
+ //#endregion
2833
3033
  //#region src/services/registry.ts
2834
3034
  var SessionRegistry = class {
2835
3035
  #sessions = /* @__PURE__ */ new Map();
2836
3036
  #options;
2837
3037
  #observers = /* @__PURE__ */ new Set();
3038
+ #detachers = /* @__PURE__ */ new Map();
2838
3039
  constructor(options = {}) {
2839
3040
  this.#options = options;
2840
3041
  }
2841
3042
  observe(listener) {
2842
3043
  this.#observers.add(listener);
2843
- for (const runner of this.#sessions.values()) listener(runner);
3044
+ for (const runner of this.#sessions.values()) this.#retain(runner.id, listener(runner));
2844
3045
  return () => void this.#observers.delete(listener);
2845
3046
  }
2846
3047
  create(config) {
@@ -2858,11 +3059,14 @@ var SessionRegistry = class {
2858
3059
  const existing = this.#sessions.get(runner.id);
2859
3060
  this.#sessions.set(runner.id, runner);
2860
3061
  if (existing !== runner) {
2861
- this.#options.onRegister?.(runner);
2862
- for (const listener of this.#observers) listener(runner);
3062
+ this.#retain(runner.id, this.#options.onRegister?.(runner));
3063
+ for (const listener of this.#observers) this.#retain(runner.id, listener(runner));
2863
3064
  }
2864
3065
  return runner;
2865
3066
  }
3067
+ retain(id, detach) {
3068
+ this.#retain(id, detach);
3069
+ }
2866
3070
  get(id) {
2867
3071
  return this.#sessions.get(id);
2868
3072
  }
@@ -2873,14 +3077,30 @@ var SessionRegistry = class {
2873
3077
  const runner = this.#sessions.get(id);
2874
3078
  if (!runner) return false;
2875
3079
  runner.close("server");
3080
+ this.#detach(id);
2876
3081
  return this.#sessions.delete(id);
2877
3082
  }
2878
3083
  evict(id) {
3084
+ this.#detach(id);
2879
3085
  return this.#sessions.delete(id);
2880
3086
  }
2881
3087
  closeAll() {
2882
3088
  for (const runner of this.#sessions.values()) runner.close("server");
2883
3089
  }
3090
+ #retain(id, detach) {
3091
+ if (typeof detach !== "function") return;
3092
+ const run = detach;
3093
+ const existing = this.#detachers.get(id);
3094
+ if (existing) {
3095
+ existing.push(run);
3096
+ return;
3097
+ }
3098
+ this.#detachers.set(id, [run]);
3099
+ }
3100
+ #detach(id) {
3101
+ for (const detach of this.#detachers.get(id) ?? []) detach();
3102
+ this.#detachers.delete(id);
3103
+ }
2884
3104
  };
2885
3105
  //#endregion
2886
3106
  //#region src/services/session-factory.ts
@@ -3052,7 +3272,7 @@ function createSessionFactory(deps) {
3052
3272
  };
3053
3273
  const watchAuthSource = (runner) => {
3054
3274
  let seen = false;
3055
- runner.subscribe((event) => {
3275
+ return runner.subscribe((event) => {
3056
3276
  if (seen || event.type !== "system_init") return;
3057
3277
  seen = true;
3058
3278
  if (event.apiKeySource !== "oauth") return;
@@ -3084,6 +3304,8 @@ function createSessionFactory(deps) {
3084
3304
  //#endregion
3085
3305
  //#region src/server.ts
3086
3306
  const SOCKET_CLOSE_GRACE_MS = 250;
3307
+ const CLOSE_DEADLINE_MS = 1e3;
3308
+ const USAGE_STALE_MS = 3e5;
3087
3309
  function surveyDrain(registry) {
3088
3310
  const working = [];
3089
3311
  const awaitingHuman = [];
@@ -3110,6 +3332,25 @@ function createWorkerServer(options = {}) {
3110
3332
  const adapterFor = (engine) => options.engines?.[engine ?? "claude"] ?? getEngineAdapter(engine);
3111
3333
  const profileDefaultModels = /* @__PURE__ */ new Map();
3112
3334
  const profileUsage = new ProfileUsageTracker();
3335
+ const spendLedger = new SpendLedger({
3336
+ store: options.spend?.store,
3337
+ monthlySubscriptionUsd: (name) => options.spend?.monthlySubscriptionUsd?.[name] ?? options.spend?.monthlySubscriptionUsd?.["*"],
3338
+ onError: (error) => options.spend?.onError?.(error)
3339
+ });
3340
+ spendLedger.load();
3341
+ const refreshStaleUsage = (name) => {
3342
+ const held = profileUsage.usage(name);
3343
+ const newest = Math.max(0, ...Object.values(held ?? {}).map((window) => window.updatedAt ?? 0));
3344
+ if (Date.now() - newest < USAGE_STALE_MS) return;
3345
+ for (const info of refs.registry?.list() ?? []) {
3346
+ if (info.profile !== name) continue;
3347
+ const runner = refs.registry?.get(info.id);
3348
+ if (runner?.refreshUsage) {
3349
+ runner.refreshUsage().catch(() => {});
3350
+ return;
3351
+ }
3352
+ }
3353
+ };
3113
3354
  const detected = options.profiles ? [] : detectDefaultProfiles();
3114
3355
  const profiles = new ProfileService({
3115
3356
  declared: options.profiles ?? (options.profileStore ? [] : detected),
@@ -3122,7 +3363,11 @@ function createWorkerServer(options = {}) {
3122
3363
  decorate: {
3123
3364
  defaultModel: (name) => profileDefaultModels.get(name),
3124
3365
  availability: (name) => availability.get(name),
3125
- usage: (name) => profileUsage.usage(name)
3366
+ usage: (name) => {
3367
+ refreshStaleUsage(name);
3368
+ return profileUsage.usage(name);
3369
+ },
3370
+ spend: (name) => spendLedger.spend(name)
3126
3371
  }
3127
3372
  });
3128
3373
  for (const p of options.profiles ?? []) {
@@ -3155,16 +3400,21 @@ function createWorkerServer(options = {}) {
3155
3400
  const producedFiles = new ProducedFileStore();
3156
3401
  const shell = options.shell?.enabled === true ? createShellService(options.shell) : null;
3157
3402
  const registry = new SessionRegistry({ onRegister: (runner) => {
3158
- notifier.watch(runner);
3159
- producedFiles.watch(runner);
3160
- profileUsage.watch(runner);
3161
- shell?.watch(runner);
3403
+ const detachers = [
3404
+ notifier.watch(runner),
3405
+ producedFiles.watch(runner),
3406
+ profileUsage.watch(runner),
3407
+ spendLedger.watch(runner),
3408
+ shell?.watch(runner)
3409
+ ];
3162
3410
  const profile = runner.info().profile;
3163
- if (!profile) return;
3164
- runner.subscribe((event) => {
3411
+ if (profile) detachers.push(runner.subscribe((event) => {
3165
3412
  if (event.type !== "capabilities" || !event.defaultModel) return;
3166
3413
  profileDefaultModels.set(profile, event.defaultModel);
3167
- });
3414
+ }));
3415
+ return () => {
3416
+ for (const detach of detachers) detach?.();
3417
+ };
3168
3418
  } });
3169
3419
  const attachmentStore = new AttachmentStore(options.attachments);
3170
3420
  const bridge = new BridgeHub({
@@ -3445,6 +3695,24 @@ function createWorkerServer(options = {}) {
3445
3695
  });
3446
3696
  });
3447
3697
  },
3698
+ releaseSession: (id) => {
3699
+ const runner = registry.get(id);
3700
+ if (!runner) return;
3701
+ const plan = reloadPlan(runner);
3702
+ if (plan !== "carry") throw new Error(`session ${id} cannot be carried by identity (${plan})`);
3703
+ const config = parking.release(id);
3704
+ registry.evict(id);
3705
+ return {
3706
+ runner,
3707
+ config
3708
+ };
3709
+ },
3710
+ adoptSession: ({ runner, config }) => {
3711
+ if (reloadPlan(runner) !== "carry") return false;
3712
+ parking.adopt(runner, config);
3713
+ registry.retain(runner.id, factory.watchAuthSource(runner));
3714
+ return true;
3715
+ },
3448
3716
  drain: async (drainOptions = {}) => {
3449
3717
  const { timeoutMs = 3e4, pollMs = 250, onProgress } = drainOptions;
3450
3718
  draining = true;
@@ -3477,12 +3745,15 @@ function createWorkerServer(options = {}) {
3477
3745
  for (const ws of wss.clients) ws.terminate();
3478
3746
  }, SOCKET_CLOSE_GRACE_MS);
3479
3747
  force.unref();
3480
- wss.close();
3481
- server.close(() => {
3748
+ const deadline = setTimeout(() => resolve(), CLOSE_DEADLINE_MS);
3749
+ deadline.unref();
3750
+ const drained = Promise.all([new Promise((done) => wss.close(() => done())), new Promise((done) => server.close(() => done()))]);
3751
+ server.closeAllConnections();
3752
+ drained.then(() => {
3482
3753
  clearTimeout(force);
3754
+ clearTimeout(deadline);
3483
3755
  resolve();
3484
3756
  });
3485
- server.closeAllConnections();
3486
3757
  });
3487
3758
  return closing;
3488
3759
  }
@@ -3579,6 +3850,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
3579
3850
  };
3580
3851
  }
3581
3852
  //#endregion
3582
- export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, sandboxedProviderProfile, toDurableRecord };
3853
+ export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, toDurableRecord };
3583
3854
 
3584
3855
  //# sourceMappingURL=index.mjs.map