@workerdeck/server 2.9.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;
@@ -61,6 +61,7 @@ type ParkedSessionRecord = {
61
61
  config: SessionRunnerConfig;
62
62
  snapshot: RunnerSnapshot;
63
63
  executions: ParkedExecution[];
64
+ cost?: CostLedgerState;
64
65
  parkedAt: number;
65
66
  };
66
67
  type DormantSessionRecord = {
@@ -70,6 +71,7 @@ type DormantSessionRecord = {
70
71
  profile?: string;
71
72
  config: SessionRunnerConfig;
72
73
  sdkSessionId: string;
74
+ cost?: CostLedgerState;
73
75
  savedAt: number;
74
76
  };
75
77
  type StoredSessionRecord = ParkedSessionRecord | DormantSessionRecord;
@@ -146,6 +148,33 @@ type ProfileStore = {
146
148
  declare function createMemoryProfileStore(seed?: ProfileInfo[]): ProfileStore;
147
149
  declare function createFileProfileStore(path?: string): ProfileStore;
148
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
149
178
  //#region src/options.d.ts
150
179
  type SdkSessionLister = (options: {
151
180
  dir?: string;
@@ -203,6 +232,11 @@ type WorkerServerOptions = {
203
232
  persistLive?: boolean;
204
233
  onError?: (error: unknown, context: ParkErrorContext) => void;
205
234
  };
235
+ spend?: {
236
+ store?: SpendStore;
237
+ monthlySubscriptionUsd?: Record<string, number>;
238
+ onError?: (error: unknown) => void;
239
+ };
206
240
  createEngineRunner?: (context: EngineRunnerContext) => Runner | Promise<Runner>;
207
241
  engines?: Partial<Record<ProfileEngine, EngineAdapter>>;
208
242
  };
@@ -360,5 +394,5 @@ declare class ProfileUsageTracker {
360
394
  usage(profile: string, now?: number): ProfileUsage | undefined;
361
395
  }
362
396
  //#endregion
363
- 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, type StoredSessionRecord, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, isDormant, reloadPlan, 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 };
364
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, isJobRun, 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";
@@ -535,7 +535,7 @@ function subsequenceScore(haystack, needle) {
535
535
  return score - haystack.length / 100;
536
536
  }
537
537
  //#endregion
538
- //#region src/routes/host-files.ts
538
+ //#region src/routes/fs.ts
539
539
  function kindRank(type) {
540
540
  return type === "dir" ? 0 : 1;
541
541
  }
@@ -2493,6 +2493,7 @@ var SessionParkManager = class {
2493
2493
  meta: info.meta
2494
2494
  },
2495
2495
  sdkSessionId,
2496
+ cost: runner.costState?.(),
2496
2497
  savedAt: Date.now()
2497
2498
  };
2498
2499
  try {
@@ -2540,6 +2541,7 @@ var SessionParkManager = class {
2540
2541
  meta: info.meta
2541
2542
  },
2542
2543
  snapshot,
2544
+ cost: runner.costState?.(),
2543
2545
  executions: snapshot.parked,
2544
2546
  parkedAt: Date.now()
2545
2547
  };
@@ -2563,6 +2565,7 @@ var SessionParkManager = class {
2563
2565
  const config = this.#configs.get(id);
2564
2566
  if (!config) return;
2565
2567
  if (this.#options.onParking && !this.#options.onParking(id, executions[0])) return;
2568
+ const cost = runner.costState?.();
2566
2569
  const snapshot = runner.park();
2567
2570
  if (!snapshot) return;
2568
2571
  const info = {
@@ -2578,6 +2581,7 @@ var SessionParkManager = class {
2578
2581
  config,
2579
2582
  snapshot,
2580
2583
  executions: snapshot.parked,
2584
+ cost,
2581
2585
  parkedAt: Date.now()
2582
2586
  };
2583
2587
  for (const execution of snapshot.parked) this.#track(id, execution);
@@ -2623,6 +2627,10 @@ var SessionParkManager = class {
2623
2627
  });
2624
2628
  throw error;
2625
2629
  }
2630
+ runner.carryCost?.(record.cost ?? {
2631
+ byModel: record.info.usageByModel,
2632
+ reportedCostUsd: record.info.totalCostUsd
2633
+ });
2626
2634
  this.#options.registry.register(runner);
2627
2635
  this.remember(id, {
2628
2636
  ...record.config,
@@ -2765,6 +2773,8 @@ var ProfileService = class {
2765
2773
  }
2766
2774
  const usage = decorate.usage(p.name);
2767
2775
  if (usage) base.usage = usage;
2776
+ const spend = decorate.spend?.(p.name);
2777
+ if (spend) base.spend = spend;
2768
2778
  return base;
2769
2779
  }
2770
2780
  all() {
@@ -2872,6 +2882,154 @@ function serveWindow(held, now) {
2872
2882
  };
2873
2883
  }
2874
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
2875
3033
  //#region src/services/registry.ts
2876
3034
  var SessionRegistry = class {
2877
3035
  #sessions = /* @__PURE__ */ new Map();
@@ -3174,6 +3332,12 @@ function createWorkerServer(options = {}) {
3174
3332
  const adapterFor = (engine) => options.engines?.[engine ?? "claude"] ?? getEngineAdapter(engine);
3175
3333
  const profileDefaultModels = /* @__PURE__ */ new Map();
3176
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();
3177
3341
  const refreshStaleUsage = (name) => {
3178
3342
  const held = profileUsage.usage(name);
3179
3343
  const newest = Math.max(0, ...Object.values(held ?? {}).map((window) => window.updatedAt ?? 0));
@@ -3202,7 +3366,8 @@ function createWorkerServer(options = {}) {
3202
3366
  usage: (name) => {
3203
3367
  refreshStaleUsage(name);
3204
3368
  return profileUsage.usage(name);
3205
- }
3369
+ },
3370
+ spend: (name) => spendLedger.spend(name)
3206
3371
  }
3207
3372
  });
3208
3373
  for (const p of options.profiles ?? []) {
@@ -3239,6 +3404,7 @@ function createWorkerServer(options = {}) {
3239
3404
  notifier.watch(runner),
3240
3405
  producedFiles.watch(runner),
3241
3406
  profileUsage.watch(runner),
3407
+ spendLedger.watch(runner),
3242
3408
  shell?.watch(runner)
3243
3409
  ];
3244
3410
  const profile = runner.info().profile;
@@ -3684,6 +3850,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
3684
3850
  };
3685
3851
  }
3686
3852
  //#endregion
3687
- export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, isDormant, reloadPlan, sandboxedProviderProfile, toDurableRecord };
3853
+ export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, toDurableRecord };
3688
3854
 
3689
3855
  //# sourceMappingURL=index.mjs.map