@workerdeck/server 2.7.2 → 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.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;
@@ -712,7 +721,7 @@ async function handleHostFiles(ctx, req, res, pathname) {
712
721
  }
713
722
  const existing = current.ok ? current.data : null;
714
723
  if (existing && !body.expectedHash) {
715
- json(res, 409, { error: "file exists — pass expectedHash to overwrite it" });
724
+ json(res, 409, { error: "file exists - pass expectedHash to overwrite it" });
716
725
  return;
717
726
  }
718
727
  if (existing && hashBytes(existing) !== body.expectedHash) {
@@ -1068,7 +1077,7 @@ function createFileSessionStore(options = {}) {
1068
1077
  path,
1069
1078
  op: "save"
1070
1079
  });
1071
- throw new Error(`parked session '${record.id}' is not JSON-serializable — a host-injected value reached its config or snapshot: ${String(error)}`, { cause: error });
1080
+ throw new Error(`parked session '${record.id}' is not JSON-serializable - a host-injected value reached its config or snapshot: ${String(error)}`, { cause: error });
1072
1081
  }
1073
1082
  try {
1074
1083
  await mkdir(dir, {
@@ -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);
@@ -2574,7 +2616,7 @@ var SessionParkManager = class {
2574
2616
  }
2575
2617
  if (runner.id !== id) {
2576
2618
  runner.close("error");
2577
- const error = /* @__PURE__ */ new Error(`rebuilt session has id '${runner.id}', expected '${id}' — the engine factory must forward EngineRunnerContext.restore (or, without a snapshot, the session id) to the runner config`);
2619
+ const error = /* @__PURE__ */ new Error(`rebuilt session has id '${runner.id}', expected '${id}' - the engine factory must forward EngineRunnerContext.restore (or, without a snapshot, the session id) to the runner config`);
2578
2620
  this.#options.onError?.(error, {
2579
2621
  sessionId: id,
2580
2622
  phase: "resume"
@@ -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, {
@@ -2682,7 +2724,7 @@ var ProfileService = class {
2682
2724
  if (!hasEngineRunnerFactory) return `profile '${p.name}' uses engine 'provider' but no \`createEngineRunner\` was provided to build one`;
2683
2725
  } else if (p.engine === "codex") {
2684
2726
  if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
2685
- if (p.session?.instructions) return `profile '${p.name}' declares session.instructions, which the codex engine cannot deliver — put instructions in the target repo’s AGENTS.md instead`;
2727
+ if (p.session?.instructions) return `profile '${p.name}' declares session.instructions, which the codex engine cannot deliver - put instructions in the target repo’s AGENTS.md instead`;
2686
2728
  } else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
2687
2729
  if (disableBypassPermissions && p.defaults?.permissionMode === "bypassPermissions") return `profile '${p.name}' defaults to bypassPermissions but disableBypassPermissions is set`;
2688
2730
  const fallbackMode = p.defaults?.permissionMode;
@@ -2745,7 +2787,7 @@ var ProfileService = class {
2745
2787
  declaredGuard(profile) {
2746
2788
  return this.#declaredByName.has(profile.name) ? {
2747
2789
  status: 403,
2748
- error: `profile '${profile.name}' is declared in server options and cannot be changed over the API — edit the \`profiles\` option instead`
2790
+ error: `profile '${profile.name}' is declared in server options and cannot be changed over the API - edit the \`profiles\` option instead`
2749
2791
  } : null;
2750
2792
  }
2751
2793
  configDirGuard(profile) {
@@ -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
@@ -2895,13 +2957,13 @@ function createSessionFactory(deps) {
2895
2957
  };
2896
2958
  const checkPermissionMode = (mode, profile) => {
2897
2959
  if (mode === void 0 || supportsPermissionMode(profile?.engine, mode)) return null;
2898
- return `permission mode '${mode}' is not supported by profile '${profile.name}' (engine '${engineOf(profile)}') — supported: ` + adapterFor(profile?.engine).capabilities.permissionModes.join(", ");
2960
+ return `permission mode '${mode}' is not supported by profile '${profile.name}' (engine '${engineOf(profile)}') - supported: ` + adapterFor(profile?.engine).capabilities.permissionModes.join(", ");
2899
2961
  };
2900
2962
  const checkEngineGrants = (req, profile) => {
2901
2963
  const engine = engineOf(profile);
2902
2964
  const caps = adapterFor(profile?.engine).capabilities;
2903
2965
  const name = profile?.name ?? "default";
2904
- if (!caps.sessionMcpServers && req.mcpServers && Object.keys(req.mcpServers).length > 0) return `profile '${name}' runs the ${engine} engine, whose MCP servers are declared outside the session request — a request cannot add its own`;
2966
+ if (!caps.sessionMcpServers && req.mcpServers && Object.keys(req.mcpServers).length > 0) return `profile '${name}' runs the ${engine} engine, whose MCP servers are declared outside the session request - a request cannot add its own`;
2905
2967
  if (!caps.budgets && (req.maxTurns !== void 0 || req.maxBudgetUsd !== void 0)) return `the ${engine} engine does not honor maxTurns/maxBudgetUsd`;
2906
2968
  if (!caps.settingSources && req.settingSources !== void 0) return `the ${engine} engine does not load settingSources`;
2907
2969
  if (!caps.resume && req.resume !== void 0) return `the ${engine} engine cannot resume a session`;
@@ -2912,7 +2974,7 @@ function createSessionFactory(deps) {
2912
2974
  if (!req.capabilities || !granted) return null;
2913
2975
  const ungranted = req.capabilities.filter((c) => !granted.includes(c));
2914
2976
  if (ungranted.length === 0) return null;
2915
- return `profile '${profile.name}' does not grant: ${ungranted.join(", ")} (granted: ${granted.join(", ") || "none"}) — a request may narrow capabilities, not widen them`;
2977
+ return `profile '${profile.name}' does not grant: ${ungranted.join(", ")} (granted: ${granted.join(", ") || "none"}) - a request may narrow capabilities, not widen them`;
2916
2978
  };
2917
2979
  const stripInertFields = (req, profile) => {
2918
2980
  if (!adapterFor(profile?.engine).capabilities.interactiveApprovals) delete req.questionBehavior;
@@ -3006,7 +3068,7 @@ function createSessionFactory(deps) {
3006
3068
  id
3007
3069
  });
3008
3070
  const reported = runner.info().scope;
3009
- if (!sameScope(reported, config.scope)) throw new Error(`runner for session ${runner.id} reports scope ${JSON.stringify(reported)}, expected ${JSON.stringify(config.scope)} — echo config.scope from info()`);
3071
+ if (!sameScope(reported, config.scope)) throw new Error(`runner for session ${runner.id} reports scope ${JSON.stringify(reported)}, expected ${JSON.stringify(config.scope)} - echo config.scope from info()`);
3010
3072
  return runner;
3011
3073
  };
3012
3074
  const createRunner = async (config) => {
@@ -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;
@@ -3062,7 +3124,7 @@ function createSessionFactory(deps) {
3062
3124
  if (subscriptionNoticeShown.has(profileName)) return;
3063
3125
  subscriptionNoticeShown.add(profileName);
3064
3126
  const scope = profileName ? `Sessions under profile '${profileName}'` : "Sessions";
3065
- console.warn(`[workerdeck] ${scope} are using claude.ai subscription credentials (apiKeySource 'oauth'), not an API key. That is only appropriate for personal, single-user use of your own account. Unattended/scheduled or multi-user use requires an API key under Anthropic's terms — set ANTHROPIC_API_KEY in the server environment, or set requireApiKey: true to fail closed.`);
3127
+ console.warn(`[workerdeck] ${scope} are using claude.ai subscription credentials (apiKeySource 'oauth'), not an API key. That is only appropriate for personal, single-user use of your own account. Unattended/scheduled or multi-user use requires an API key under Anthropic's terms - set ANTHROPIC_API_KEY in the server environment, or set requireApiKey: true to fail closed.`);
3066
3128
  }
3067
3129
  });
3068
3130
  };
@@ -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