@workerdeck/server 2.8.0 → 2.9.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
@@ -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;
@@ -72,6 +73,7 @@ type DormantSessionRecord = {
72
73
  savedAt: number;
73
74
  };
74
75
  type StoredSessionRecord = ParkedSessionRecord | DormantSessionRecord;
76
+ declare function isDormant(record: StoredSessionRecord): record is DormantSessionRecord;
75
77
  interface SessionStore {
76
78
  save(record: StoredSessionRecord): Promise<void>;
77
79
  get(id: string): Promise<StoredSessionRecord | null>;
@@ -119,6 +121,9 @@ declare class SessionParkManager {
119
121
  touch(runner: Runner): void;
120
122
  hydrate(): Promise<void>;
121
123
  watch(runner: Runner, afterSeq?: number): () => void;
124
+ release(sessionId: string): SessionRunnerConfig | undefined;
125
+ adopt(runner: Runner, config?: SessionRunnerConfig): void;
126
+ flush(sessionId?: string): Promise<void>;
122
127
  onDetach(sessionId: string): void;
123
128
  sessionFor(executionId: string): string | undefined;
124
129
  get(id: string): Promise<StoredSessionRecord | null>;
@@ -223,6 +228,10 @@ type QueueServerOptions = {
223
228
  webhookRetryDelayMs?: number;
224
229
  onEvent?: (event: JobEvent) => void;
225
230
  };
231
+ type CarriedSession = {
232
+ runner: Runner;
233
+ config?: SessionRunnerConfig;
234
+ };
226
235
  type DrainReport = {
227
236
  working: string[];
228
237
  awaitingHuman: string[];
@@ -243,6 +252,8 @@ type WorkerServer = {
243
252
  port: number;
244
253
  }>;
245
254
  drain: (options?: DrainOptions) => Promise<DrainReport>;
255
+ releaseSession: (id: string) => CarriedSession | undefined;
256
+ adoptSession: (carried: CarriedSession) => boolean;
246
257
  close: () => Promise<void>;
247
258
  };
248
259
  //#endregion
@@ -281,6 +292,10 @@ type ProviderRunnerOptions = {
281
292
  };
282
293
  declare function createProviderRunner(ctx: EngineRunnerContext, options: ProviderRunnerOptions): Promise<Runner>;
283
294
  //#endregion
295
+ //#region src/lib/reload-plan.d.ts
296
+ type ReloadPlan = 'carry' | 'persist' | 'drop';
297
+ declare function reloadPlan(runner: Runner): ReloadPlan;
298
+ //#endregion
284
299
  //#region src/services/attachments.d.ts
285
300
  type AttachmentStoreOptions = {
286
301
  maxFileBytes?: number;
@@ -332,7 +347,7 @@ type ProducedFile = {
332
347
  };
333
348
  declare class ProducedFileStore {
334
349
  #private;
335
- watch(runner: Runner): void;
350
+ watch(runner: Runner): () => void;
336
351
  get(sessionId: string, fileId: string): ProducedFile | undefined;
337
352
  list(sessionId: string): ProducedFile[];
338
353
  drop(sessionId: string): void;
@@ -341,9 +356,9 @@ declare class ProducedFileStore {
341
356
  //#region src/services/profile-usage.d.ts
342
357
  declare class ProfileUsageTracker {
343
358
  #private;
344
- watch(runner: Runner): void;
359
+ watch(runner: Runner): () => void;
345
360
  usage(profile: string, now?: number): ProfileUsage | undefined;
346
361
  }
347
362
  //#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 };
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 };
349
364
  //# 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, imagePartRef, isJobRun, sessionState, 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;
@@ -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);
@@ -2639,7 +2681,7 @@ var SessionParkManager = class {
2639
2681
  var ProducedFileStore = class {
2640
2682
  #bySession = /* @__PURE__ */ new Map();
2641
2683
  watch(runner) {
2642
- runner.subscribe((event) => {
2684
+ return runner.subscribe((event) => {
2643
2685
  if (event.type !== "file_produced") return;
2644
2686
  const held = this.#bySession.get(runner.id) ?? /* @__PURE__ */ new Map();
2645
2687
  held.set(event.fileId, {
@@ -2790,8 +2832,8 @@ var ProfileUsageTracker = class {
2790
2832
  #profiles = /* @__PURE__ */ new Map();
2791
2833
  watch(runner) {
2792
2834
  const profile = runner.info().profile;
2793
- if (!profile) return;
2794
- runner.subscribe((event) => {
2835
+ if (!profile) return () => {};
2836
+ return runner.subscribe((event) => {
2795
2837
  if (event.type !== "rate_limit") return;
2796
2838
  const type = event.info.rateLimitType;
2797
2839
  if (!type) return;
@@ -2835,12 +2877,13 @@ var SessionRegistry = class {
2835
2877
  #sessions = /* @__PURE__ */ new Map();
2836
2878
  #options;
2837
2879
  #observers = /* @__PURE__ */ new Set();
2880
+ #detachers = /* @__PURE__ */ new Map();
2838
2881
  constructor(options = {}) {
2839
2882
  this.#options = options;
2840
2883
  }
2841
2884
  observe(listener) {
2842
2885
  this.#observers.add(listener);
2843
- for (const runner of this.#sessions.values()) listener(runner);
2886
+ for (const runner of this.#sessions.values()) this.#retain(runner.id, listener(runner));
2844
2887
  return () => void this.#observers.delete(listener);
2845
2888
  }
2846
2889
  create(config) {
@@ -2858,11 +2901,14 @@ var SessionRegistry = class {
2858
2901
  const existing = this.#sessions.get(runner.id);
2859
2902
  this.#sessions.set(runner.id, runner);
2860
2903
  if (existing !== runner) {
2861
- this.#options.onRegister?.(runner);
2862
- for (const listener of this.#observers) listener(runner);
2904
+ this.#retain(runner.id, this.#options.onRegister?.(runner));
2905
+ for (const listener of this.#observers) this.#retain(runner.id, listener(runner));
2863
2906
  }
2864
2907
  return runner;
2865
2908
  }
2909
+ retain(id, detach) {
2910
+ this.#retain(id, detach);
2911
+ }
2866
2912
  get(id) {
2867
2913
  return this.#sessions.get(id);
2868
2914
  }
@@ -2873,14 +2919,30 @@ var SessionRegistry = class {
2873
2919
  const runner = this.#sessions.get(id);
2874
2920
  if (!runner) return false;
2875
2921
  runner.close("server");
2922
+ this.#detach(id);
2876
2923
  return this.#sessions.delete(id);
2877
2924
  }
2878
2925
  evict(id) {
2926
+ this.#detach(id);
2879
2927
  return this.#sessions.delete(id);
2880
2928
  }
2881
2929
  closeAll() {
2882
2930
  for (const runner of this.#sessions.values()) runner.close("server");
2883
2931
  }
2932
+ #retain(id, detach) {
2933
+ if (typeof detach !== "function") return;
2934
+ const run = detach;
2935
+ const existing = this.#detachers.get(id);
2936
+ if (existing) {
2937
+ existing.push(run);
2938
+ return;
2939
+ }
2940
+ this.#detachers.set(id, [run]);
2941
+ }
2942
+ #detach(id) {
2943
+ for (const detach of this.#detachers.get(id) ?? []) detach();
2944
+ this.#detachers.delete(id);
2945
+ }
2884
2946
  };
2885
2947
  //#endregion
2886
2948
  //#region src/services/session-factory.ts
@@ -3052,7 +3114,7 @@ function createSessionFactory(deps) {
3052
3114
  };
3053
3115
  const watchAuthSource = (runner) => {
3054
3116
  let seen = false;
3055
- runner.subscribe((event) => {
3117
+ return runner.subscribe((event) => {
3056
3118
  if (seen || event.type !== "system_init") return;
3057
3119
  seen = true;
3058
3120
  if (event.apiKeySource !== "oauth") return;
@@ -3084,6 +3146,8 @@ function createSessionFactory(deps) {
3084
3146
  //#endregion
3085
3147
  //#region src/server.ts
3086
3148
  const SOCKET_CLOSE_GRACE_MS = 250;
3149
+ const CLOSE_DEADLINE_MS = 1e3;
3150
+ const USAGE_STALE_MS = 3e5;
3087
3151
  function surveyDrain(registry) {
3088
3152
  const working = [];
3089
3153
  const awaitingHuman = [];
@@ -3110,6 +3174,19 @@ function createWorkerServer(options = {}) {
3110
3174
  const adapterFor = (engine) => options.engines?.[engine ?? "claude"] ?? getEngineAdapter(engine);
3111
3175
  const profileDefaultModels = /* @__PURE__ */ new Map();
3112
3176
  const profileUsage = new ProfileUsageTracker();
3177
+ const refreshStaleUsage = (name) => {
3178
+ const held = profileUsage.usage(name);
3179
+ const newest = Math.max(0, ...Object.values(held ?? {}).map((window) => window.updatedAt ?? 0));
3180
+ if (Date.now() - newest < USAGE_STALE_MS) return;
3181
+ for (const info of refs.registry?.list() ?? []) {
3182
+ if (info.profile !== name) continue;
3183
+ const runner = refs.registry?.get(info.id);
3184
+ if (runner?.refreshUsage) {
3185
+ runner.refreshUsage().catch(() => {});
3186
+ return;
3187
+ }
3188
+ }
3189
+ };
3113
3190
  const detected = options.profiles ? [] : detectDefaultProfiles();
3114
3191
  const profiles = new ProfileService({
3115
3192
  declared: options.profiles ?? (options.profileStore ? [] : detected),
@@ -3122,7 +3199,10 @@ function createWorkerServer(options = {}) {
3122
3199
  decorate: {
3123
3200
  defaultModel: (name) => profileDefaultModels.get(name),
3124
3201
  availability: (name) => availability.get(name),
3125
- usage: (name) => profileUsage.usage(name)
3202
+ usage: (name) => {
3203
+ refreshStaleUsage(name);
3204
+ return profileUsage.usage(name);
3205
+ }
3126
3206
  }
3127
3207
  });
3128
3208
  for (const p of options.profiles ?? []) {
@@ -3155,16 +3235,20 @@ function createWorkerServer(options = {}) {
3155
3235
  const producedFiles = new ProducedFileStore();
3156
3236
  const shell = options.shell?.enabled === true ? createShellService(options.shell) : null;
3157
3237
  const registry = new SessionRegistry({ onRegister: (runner) => {
3158
- notifier.watch(runner);
3159
- producedFiles.watch(runner);
3160
- profileUsage.watch(runner);
3161
- shell?.watch(runner);
3238
+ const detachers = [
3239
+ notifier.watch(runner),
3240
+ producedFiles.watch(runner),
3241
+ profileUsage.watch(runner),
3242
+ shell?.watch(runner)
3243
+ ];
3162
3244
  const profile = runner.info().profile;
3163
- if (!profile) return;
3164
- runner.subscribe((event) => {
3245
+ if (profile) detachers.push(runner.subscribe((event) => {
3165
3246
  if (event.type !== "capabilities" || !event.defaultModel) return;
3166
3247
  profileDefaultModels.set(profile, event.defaultModel);
3167
- });
3248
+ }));
3249
+ return () => {
3250
+ for (const detach of detachers) detach?.();
3251
+ };
3168
3252
  } });
3169
3253
  const attachmentStore = new AttachmentStore(options.attachments);
3170
3254
  const bridge = new BridgeHub({
@@ -3445,6 +3529,24 @@ function createWorkerServer(options = {}) {
3445
3529
  });
3446
3530
  });
3447
3531
  },
3532
+ releaseSession: (id) => {
3533
+ const runner = registry.get(id);
3534
+ if (!runner) return;
3535
+ const plan = reloadPlan(runner);
3536
+ if (plan !== "carry") throw new Error(`session ${id} cannot be carried by identity (${plan})`);
3537
+ const config = parking.release(id);
3538
+ registry.evict(id);
3539
+ return {
3540
+ runner,
3541
+ config
3542
+ };
3543
+ },
3544
+ adoptSession: ({ runner, config }) => {
3545
+ if (reloadPlan(runner) !== "carry") return false;
3546
+ parking.adopt(runner, config);
3547
+ registry.retain(runner.id, factory.watchAuthSource(runner));
3548
+ return true;
3549
+ },
3448
3550
  drain: async (drainOptions = {}) => {
3449
3551
  const { timeoutMs = 3e4, pollMs = 250, onProgress } = drainOptions;
3450
3552
  draining = true;
@@ -3477,12 +3579,15 @@ function createWorkerServer(options = {}) {
3477
3579
  for (const ws of wss.clients) ws.terminate();
3478
3580
  }, SOCKET_CLOSE_GRACE_MS);
3479
3581
  force.unref();
3480
- wss.close();
3481
- server.close(() => {
3582
+ const deadline = setTimeout(() => resolve(), CLOSE_DEADLINE_MS);
3583
+ deadline.unref();
3584
+ const drained = Promise.all([new Promise((done) => wss.close(() => done())), new Promise((done) => server.close(() => done()))]);
3585
+ server.closeAllConnections();
3586
+ drained.then(() => {
3482
3587
  clearTimeout(force);
3588
+ clearTimeout(deadline);
3483
3589
  resolve();
3484
3590
  });
3485
- server.closeAllConnections();
3486
3591
  });
3487
3592
  return closing;
3488
3593
  }
@@ -3579,6 +3684,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
3579
3684
  };
3580
3685
  }
3581
3686
  //#endregion
3582
- export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, sandboxedProviderProfile, toDurableRecord };
3687
+ export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, isDormant, reloadPlan, sandboxedProviderProfile, toDurableRecord };
3583
3688
 
3584
3689
  //# sourceMappingURL=index.mjs.map