@workerdeck/server 3.2.0 → 3.4.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
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { WebSocketServer } from "ws";
4
- import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SHELL_REFUSAL as SHELL_REFUSAL$1, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentKind, checkClaudeAuth, clampShellTail, createEngineSession, encodeShellKeys, getEngineAdapter, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText, withoutGatewaySecrets } from "@workerdeck/core";
4
+ import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SHELL_REFUSAL as SHELL_REFUSAL$1, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentKind, checkClaudeAuth, clampShellTail, contextResetDirectoryHandle, createEngineSession, encodeShellKeys, getEngineAdapter, installContextResetDirectory, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText, withoutGatewaySecrets } from "@workerdeck/core";
5
5
  import { JobQueue } from "@workerdeck/queue";
6
6
  import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_ARTIFACT_MAX_BYTES, SHELL_ARTIFACT_TTL_MS, SHELL_ATTACH_REPLAY_BYTES, SHELL_COLS, SHELL_COMMAND_MAX, SHELL_INPUT_MAX, SHELL_LABEL_MAX, SHELL_LINGER_MS, SHELL_MAX_COLS, SHELL_MAX_ROWS, SHELL_MAX_RUNNING_PER_SESSION, SHELL_MAX_RUNNING_TOTAL, SHELL_MIN_COLS, SHELL_MIN_ROWS, SHELL_READ_MAX_LINES, SHELL_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, SHELL_WAIT_DEFAULT_MS, SHELL_WAIT_MAX_MS, costOfByModel, imagePartRef, isJobRun, mergeByModel, mergePricing, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
7
7
  import { chmodSync, closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
@@ -10,6 +10,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
10
10
  import { chmod, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
11
11
  import { spawnSync } from "node:child_process";
12
12
  import xtermHeadless from "@xterm/headless";
13
+ import { connectRelay, parseRelayPeerId } from "@workerdeck/relay-client";
13
14
  //#region src/lifecycle.ts
14
15
  const SOCKET_CLOSE_GRACE_MS = 250;
15
16
  const CLOSE_DEADLINE_MS = 1e3;
@@ -78,6 +79,7 @@ function closeServer(deps) {
78
79
  shells?.killAll("server_stopped");
79
80
  const flushed = shells?.flush().catch((error) => deps.diagnose(error, "shell-flush")) ?? Promise.resolve();
80
81
  parking.close();
82
+ deps.engineSleep.close();
81
83
  registry.closeAll();
82
84
  for (const ws of wss.clients) ws.close(1001, "server shutting down");
83
85
  deps.closeQueueSockets();
@@ -221,6 +223,23 @@ function cwdAllowed(cwd, roots) {
221
223
  return resolved === r || resolved.startsWith(r + sep);
222
224
  });
223
225
  }
226
+ function listDirs(path) {
227
+ try {
228
+ return readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
229
+ } catch {
230
+ return [];
231
+ }
232
+ }
233
+ function listMd(path) {
234
+ try {
235
+ return readdirSync(path).filter((file) => file.endsWith(".md")).map((file) => file.slice(0, -3)).sort();
236
+ } catch {
237
+ return [];
238
+ }
239
+ }
240
+ function count(rules) {
241
+ return Array.isArray(rules) ? rules.length : 0;
242
+ }
224
243
  function readProfileConfig(profile) {
225
244
  const dir = profile.configDir;
226
245
  if (!dir) return {
@@ -229,22 +248,8 @@ function readProfileConfig(profile) {
229
248
  agents: [],
230
249
  commands: []
231
250
  };
232
- const listDirs = (path) => {
233
- try {
234
- return readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
235
- } catch {
236
- return [];
237
- }
238
- };
239
- const listMd = (path) => {
240
- try {
241
- return readdirSync(path).filter((file) => file.endsWith(".md")).map((file) => file.slice(0, -3)).sort();
242
- } catch {
243
- return [];
244
- }
245
- };
246
251
  const snapshot = {
247
- hasUserMemory: existsSync(join(dir, "CLAUDE.md")),
252
+ hasUserMemory: ["CLAUDE.md", "AGENTS.md"].some((name) => existsSync(join(dir, name))),
248
253
  skills: listDirs(join(dir, "skills")),
249
254
  agents: listMd(join(dir, "agents")),
250
255
  commands: listMd(join(dir, "commands"))
@@ -252,7 +257,6 @@ function readProfileConfig(profile) {
252
257
  try {
253
258
  const raw = JSON.parse(readFileSync(join(dir, "settings.json"), "utf8"));
254
259
  const permissions = raw.permissions ?? {};
255
- const count = (rules) => Array.isArray(rules) ? rules.length : 0;
256
260
  snapshot.settings = {
257
261
  model: typeof raw.model === "string" ? raw.model : void 0,
258
262
  defaultPermissionMode: typeof permissions.defaultMode === "string" ? permissions.defaultMode : void 0,
@@ -387,6 +391,15 @@ function itemRoute(rawId, parts) {
387
391
  id: decode(rawId),
388
392
  stopTaskId: decode(third)
389
393
  };
394
+ if (length === 3 && section === "tasks" && third === "background") return {
395
+ kind: "background-task",
396
+ id: decode(rawId)
397
+ };
398
+ if (length === 4 && section === "tasks" && fourth === "background") return {
399
+ kind: "background-task",
400
+ id: decode(rawId),
401
+ backgroundTaskId: decode(third)
402
+ };
390
403
  if (length === 3 && section === "project" && third === "icon") return {
391
404
  kind: "project-icon",
392
405
  id: decode(rawId)
@@ -399,6 +412,14 @@ function itemRoute(rawId, parts) {
399
412
  resultSeq: seq
400
413
  } : null;
401
414
  }
415
+ if (length === 2 && section === "sleep") return {
416
+ kind: "sleep",
417
+ id: decode(rawId)
418
+ };
419
+ if (length === 2 && section === "peers") return {
420
+ kind: "peers",
421
+ id: decode(rawId)
422
+ };
402
423
  if (length <= 3 && section === "mcp") return {
403
424
  kind: "mcp",
404
425
  id: decode(rawId),
@@ -463,6 +484,303 @@ function permissionDecision(input) {
463
484
  };
464
485
  }
465
486
  //#endregion
487
+ //#region src/lib/scope.ts
488
+ const MAX_SCOPE_KEYS = 16;
489
+ const MAX_SCOPE_LEN = 200;
490
+ function readScope(value) {
491
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return;
492
+ const entries = Object.entries(value);
493
+ if (entries.some(([, v]) => typeof v !== "string")) return;
494
+ return Object.fromEntries(entries);
495
+ }
496
+ function checkScope(value) {
497
+ if (value === void 0) return null;
498
+ const scope = readScope(value);
499
+ if (!scope) return "scope must be an object of string values";
500
+ const entries = Object.entries(scope);
501
+ if (entries.length > MAX_SCOPE_KEYS) return `scope may carry at most ${MAX_SCOPE_KEYS} keys`;
502
+ for (const [key, val] of entries) {
503
+ if (key.length === 0) return "scope keys must not be empty";
504
+ if (key.length > MAX_SCOPE_LEN || val.length > MAX_SCOPE_LEN) return `scope keys and values must be at most ${MAX_SCOPE_LEN} characters`;
505
+ }
506
+ return null;
507
+ }
508
+ function sameScope(a, b) {
509
+ const left = Object.entries(a ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
510
+ const right = Object.entries(b ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
511
+ return left.length === right.length && left.every(([key, value], i) => right[i][0] === key && right[i][1] === value);
512
+ }
513
+ function scopeMatches(principal, session) {
514
+ if (!principal) return true;
515
+ return Object.entries(principal).every(([key, value]) => session?.[key] === value);
516
+ }
517
+ //#endregion
518
+ //#region src/services/peers.ts
519
+ const DEFAULT_PER_MINUTE = 10;
520
+ const DEFAULT_MAX_HOPS = 12;
521
+ const WINDOW_MS = 6e4;
522
+ function visible(from, to) {
523
+ return to.id !== from.id && scopeMatches(from.scope, to.scope);
524
+ }
525
+ function relayEntry(info, live) {
526
+ const items = info.checklist;
527
+ return {
528
+ id: info.id,
529
+ engine: info.engine,
530
+ status: info.status,
531
+ title: info.title,
532
+ project: info.project ? {
533
+ name: info.project.name,
534
+ root: info.project.root
535
+ } : void 0,
536
+ cwd: info.cwd,
537
+ profile: info.profile,
538
+ model: info.model,
539
+ permissionMode: info.permissionMode,
540
+ contextUsage: info.contextUsage,
541
+ checklist: items?.length ? {
542
+ done: items.filter((item) => item.status === "completed").length,
543
+ total: items.length
544
+ } : void 0,
545
+ numTurns: info.numTurns,
546
+ createdAt: info.createdAt,
547
+ lastActivityAt: info.lastActivityAt,
548
+ pendingPermissionCount: info.pendingPermissionCount,
549
+ live
550
+ };
551
+ }
552
+ function createPeerService(deps) {
553
+ const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
554
+ const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
555
+ const maxHops = deps.options?.maxHops ?? DEFAULT_MAX_HOPS;
556
+ const sent = /* @__PURE__ */ new Map();
557
+ const inbound = /* @__PURE__ */ new Map();
558
+ const registry = () => {
559
+ const found = deps.refs.registry;
560
+ if (!found) throw new Error("peer messaging is not ready");
561
+ return found;
562
+ };
563
+ const allSessions = async () => {
564
+ const live = registry().list();
565
+ const dormant = await deps.refs.parking?.listInfo() ?? [];
566
+ return [...live, ...dormant].map((info) => deps.projects.withProject(info));
567
+ };
568
+ const infoOf = async (id) => {
569
+ const live = registry().get(id)?.info();
570
+ if (live) return deps.projects.withProject(live);
571
+ const record = await deps.refs.parking?.get(id);
572
+ return record ? deps.projects.withProject(record.info) : void 0;
573
+ };
574
+ const sender = async (from) => {
575
+ const info = await infoOf(from);
576
+ if (!info) throw new Error(`unknown sender session: ${from}`);
577
+ return info;
578
+ };
579
+ const recentOf = (sessionId, recent) => {
580
+ const limit = Math.min(Math.max(recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
581
+ const runner = registry().get(sessionId);
582
+ const events = [];
583
+ if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
584
+ truncateResults: true,
585
+ imageRefs: true
586
+ })();
587
+ return {
588
+ runner,
589
+ lines: recentLines(events, limit)
590
+ };
591
+ };
592
+ const underRateLimit = (from, to) => {
593
+ const key = `${from}->${to}`;
594
+ const now = Date.now();
595
+ const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
596
+ if (recent.length >= perMinute) {
597
+ sent.set(key, recent);
598
+ return false;
599
+ }
600
+ recent.push(now);
601
+ sent.set(key, recent);
602
+ return true;
603
+ };
604
+ const list = async (from) => {
605
+ const me = await sender(from);
606
+ return (await allSessions()).filter((info) => visible(me, info)).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map(peerSummary);
607
+ };
608
+ const peek = async (from, sessionId, options) => {
609
+ const me = await sender(from);
610
+ const info = await infoOf(sessionId);
611
+ if (!info || !visible(me, info)) return;
612
+ const { runner, lines } = recentOf(sessionId, options?.recent);
613
+ return {
614
+ ...peerSummary(info),
615
+ live: runner !== void 0,
616
+ checklist: info.checklist,
617
+ pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
618
+ recent: lines
619
+ };
620
+ };
621
+ const send = async (from, sessionId, text, options) => {
622
+ const me = await sender(from);
623
+ if (sessionId === from) return {
624
+ delivered: false,
625
+ reason: "that is this session"
626
+ };
627
+ if (text.length > maxChars) return {
628
+ delivered: false,
629
+ reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
630
+ };
631
+ const target = await infoOf(sessionId);
632
+ if (!target || !visible(me, target)) return {
633
+ delivered: false,
634
+ reason: `no such session: ${sessionId}`
635
+ };
636
+ const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
637
+ return deliver(target, text, {
638
+ kind: "peer",
639
+ sessionId: from,
640
+ name: me.title,
641
+ engine: me.engine,
642
+ hops
643
+ }, from);
644
+ };
645
+ const deliver = async (target, text, origin, rateKey) => {
646
+ const sessionId = target.id;
647
+ if (target.status === "closed" || target.status === "failed") return {
648
+ delivered: false,
649
+ reason: `session ${sessionId} is ${target.status}`
650
+ };
651
+ const hops = origin.hops ?? [];
652
+ if (hops.length > maxHops) return {
653
+ delivered: false,
654
+ reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
655
+ };
656
+ if (!underRateLimit(rateKey, sessionId)) return {
657
+ delivered: false,
658
+ reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
659
+ };
660
+ const runner = await deps.refs.parking?.ensureLive(sessionId) ?? registry().get(sessionId);
661
+ if (!runner) return {
662
+ delivered: false,
663
+ reason: `session ${sessionId} could not be woken`
664
+ };
665
+ const before = runner.info().status;
666
+ try {
667
+ runner.sendMessage(text, void 0, { origin });
668
+ } catch (error) {
669
+ return {
670
+ delivered: false,
671
+ reason: error instanceof Error ? error.message : String(error)
672
+ };
673
+ }
674
+ inbound.set(sessionId, hops);
675
+ return {
676
+ delivered: true,
677
+ sessionId,
678
+ name: target.title,
679
+ queued: before === "running" || before === "awaiting_approval"
680
+ };
681
+ };
682
+ const exposedInfo = async (sessionId, exposed) => {
683
+ const info = await infoOf(sessionId);
684
+ return info && info.status !== "closed" && scopeMatches(exposed, info.scope) ? info : void 0;
685
+ };
686
+ const relayEntries = async (exposed) => {
687
+ const live = new Set(registry().list().map((info) => info.id));
688
+ return (await allSessions()).filter((info) => info.status !== "closed" && scopeMatches(exposed, info.scope)).map((info) => relayEntry(info, live.has(info.id)));
689
+ };
690
+ const relayPeek = async (sessionId, recent, exposed) => {
691
+ const info = await exposedInfo(sessionId, exposed);
692
+ if (!info) return;
693
+ const { runner, lines } = recentOf(sessionId, recent);
694
+ return {
695
+ ...relayEntry(info, runner !== void 0),
696
+ checklistItems: info.checklist,
697
+ pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
698
+ recent: lines
699
+ };
700
+ };
701
+ const relaySend = async (origin, sessionId, text, exposed) => {
702
+ if (text.length === 0 || text.length > maxChars) return {
703
+ delivered: false,
704
+ reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
705
+ };
706
+ const target = await exposedInfo(sessionId, exposed);
707
+ if (!target) return {
708
+ delivered: false,
709
+ reason: `no such session: ${sessionId}`
710
+ };
711
+ const from = `${origin.gateway}:${origin.sessionId}`;
712
+ return deliver(target, text, {
713
+ kind: "peer",
714
+ sessionId: from,
715
+ hostId: origin.gateway,
716
+ name: origin.name,
717
+ engine: origin.engine,
718
+ hops: origin.hops
719
+ }, from);
720
+ };
721
+ const watch = (runner) => runner.subscribe((event) => {
722
+ if (event.type === "user_message" && !event.origin && !event.synthetic && event.parentToolUseId == null && !event.replay) inbound.delete(runner.id);
723
+ }, runner.info().lastSeq);
724
+ return {
725
+ list,
726
+ peek,
727
+ send,
728
+ watch,
729
+ relaySender: sender,
730
+ relayChain: (from) => [...inbound.get(from) ?? [], from],
731
+ relayEntries,
732
+ relayPeek,
733
+ relaySend
734
+ };
735
+ }
736
+ function resolvePeerMentions(rows, text) {
737
+ const tokens = scanPeerMentions(text);
738
+ if (tokens.length === 0) return [];
739
+ const byKey = /* @__PURE__ */ new Map();
740
+ const add = (key, row) => {
741
+ const held = byKey.get(key);
742
+ if (held) held.push(row);
743
+ else byKey.set(key, [row]);
744
+ };
745
+ for (const row of rows) {
746
+ add(peerMentionKey(peerMentionSlug(row.title, row.id)), row);
747
+ add(peerMentionKey(row.id), row);
748
+ }
749
+ const resolved = [];
750
+ const seen = /* @__PURE__ */ new Set();
751
+ for (const token of tokens) {
752
+ const key = peerMentionKey(token.body);
753
+ const exact = byKey.get(key);
754
+ const matches = exact ?? (key.length >= 4 ? rows.filter((row) => row.id.startsWith(key)) : []);
755
+ const target = matches[0];
756
+ if (!target || matches.length > 1 && !exact) continue;
757
+ if (seen.has(target.id)) continue;
758
+ seen.add(target.id);
759
+ const others = matches.slice(1, 4).map((row) => row.id);
760
+ resolved.push({
761
+ typed: token.body,
762
+ id: target.id,
763
+ name: target.title,
764
+ engine: target.engine,
765
+ status: target.status,
766
+ cwd: target.cwd,
767
+ ...others.length ? { ambiguousWith: others } : {}
768
+ });
769
+ if (resolved.length >= PEER_MENTION_MAX) break;
770
+ }
771
+ return resolved;
772
+ }
773
+ async function mentionsFor(directory, from, text) {
774
+ return scanPeerMentions(text).length === 0 ? [] : resolvePeerMentions(await directory.list(from), text);
775
+ }
776
+ //#endregion
777
+ //#region src/routes/sleep.ts
778
+ async function sleepRunner(runner) {
779
+ if (!runner.sleep) throw new HttpError(501, `the ${engineOf(runner.info())} engine has no process to put to sleep`);
780
+ const result = await runner.sleep();
781
+ if (!result.ok) throw new HttpError(409, `cannot sleep: ${result.reason}`);
782
+ }
783
+ //#endregion
466
784
  //#region src/services/shell-directory.ts
467
785
  function createShellDirectory(registry, deps = {}) {
468
786
  const take = async (from, shellId, options) => {
@@ -1852,13 +2170,14 @@ function attachClient(ctx, ws, runner, req, access) {
1852
2170
  unsubscribe();
1853
2171
  detachBridge();
1854
2172
  parking.onDetach(runner.id);
2173
+ ctx.engineSleep.onDetach(runner.id);
1855
2174
  });
1856
2175
  }
1857
2176
  async function handleCommand(ctx, frame, runner, access, client) {
1858
2177
  const { attachmentStore, bridge } = ctx;
1859
2178
  switch (frame.type) {
1860
2179
  case "user_message": {
1861
- const mentions = ctx.peers && !isSlashCommand(frame.text) ? await ctx.peers.mentions(runner.id, frame.text).catch(() => []) : [];
2180
+ const mentions = ctx.peers && !isSlashCommand(frame.text) ? await mentionsFor(ctx.peers, runner.id, frame.text).catch(() => []) : [];
1862
2181
  const options = mentions.length > 0 ? { mentions } : void 0;
1863
2182
  if (!frame.attachmentIds?.length) {
1864
2183
  runner.sendMessage(frame.text, void 0, options);
@@ -1875,6 +2194,13 @@ async function handleCommand(ctx, frame, runner, access, client) {
1875
2194
  case "interrupt":
1876
2195
  await runner.interrupt();
1877
2196
  return;
2197
+ case "sleep":
2198
+ await sleepRunner(runner);
2199
+ return;
2200
+ case "background_task":
2201
+ if (!runner.backgroundTask) throw new Error(`the ${engineOf(runner.info())} engine cannot move a task to the background`);
2202
+ await runner.backgroundTask(frame.toolUseId);
2203
+ return;
1878
2204
  case "clear_context":
1879
2205
  if (!runner.clearContext) throw new Error(`the ${engineOf(runner.info())} engine cannot clear a conversation`);
1880
2206
  await runner.clearContext();
@@ -1891,6 +2217,11 @@ async function handleCommand(ctx, frame, runner, access, client) {
1891
2217
  case "set_model":
1892
2218
  await runner.setModel(frame.model);
1893
2219
  return;
2220
+ case "set_effort":
2221
+ if (!runner.setEffort) throw new Error(`the ${engineOf(runner.info())} engine cannot change its reasoning effort`);
2222
+ if (frame.effort !== void 0 && typeof frame.effort !== "string") throw new Error("effort must be a string");
2223
+ await runner.setEffort(frame.effort);
2224
+ return;
1894
2225
  case "tool_call_result":
1895
2226
  bridge.resolve(runner.id, frame.executionId, {
1896
2227
  output: frame.output,
@@ -2502,10 +2833,13 @@ const HOST_ONLY_KEY_TABLE = {
2502
2833
  historyFn: "transient",
2503
2834
  sessionInfoFn: "transient",
2504
2835
  peers: "transient",
2836
+ contextReset: "transient",
2505
2837
  shells: "transient",
2506
2838
  shellAgentWrite: "transient",
2507
2839
  createdByOperator: "durable",
2508
2840
  fallbackTitle: "transient",
2841
+ startAsleep: "transient",
2842
+ effortDefaults: "transient",
2509
2843
  connectFn: "transient",
2510
2844
  codexHome: "durable",
2511
2845
  codexPathOverride: "durable",
@@ -2520,6 +2854,7 @@ const HOST_ONLY_KEY_TABLE = {
2520
2854
  toolTitles: "transient",
2521
2855
  shouldApprove: "transient",
2522
2856
  resolveModel: "transient",
2857
+ contextWindow: "transient",
2523
2858
  reportMcpServers: "transient",
2524
2859
  onClose: "transient",
2525
2860
  restore: "transient"
@@ -3259,12 +3594,15 @@ const ITEM_HANDLERS = {
3259
3594
  produced: ({ ctx, req, res, route }) => handleProducedFiles(ctx, req, res, route.id, route.producedFileId),
3260
3595
  shells: ({ ctx, req, res, route, runner, auth }) => handleShells(ctx, req, res, route, runner ?? null, ctx.auth.isOperator(auth)),
3261
3596
  "stop-task": handleStopTask,
3597
+ "background-task": handleBackgroundTask,
3262
3598
  "project-icon": ({ ctx, req, res, info }) => handleProjectIcon(ctx.projects, req, res, info.cwd),
3263
3599
  "tool-result": ({ req, res, route, runner, parked }) => {
3264
3600
  const snapshot = parked && !isDormant(parked) ? parked.snapshot.events : void 0;
3265
3601
  handleToolResult(req, res, runner?.eventAt?.bind(runner) ?? (snapshot && ((seq) => snapshot.find((event) => event.seq === seq))), route.resultSeq);
3266
3602
  },
3267
- permission: handlePermission
3603
+ permission: handlePermission,
3604
+ peers: handlePeers,
3605
+ sleep: handleSleep
3268
3606
  };
3269
3607
  async function handleSessions(ctx, req, res, route, auth) {
3270
3608
  if (route.kind === "collection") {
@@ -3328,6 +3666,11 @@ async function handleSession({ ctx, req, res, route, runner, parked, info }) {
3328
3666
  status: "closed"
3329
3667
  }) });
3330
3668
  }
3669
+ async function handlePeers({ ctx, req, res, route }) {
3670
+ requireMethod(req, "GET");
3671
+ if (!ctx.peers) fail(404, "peer messaging is off on this gateway");
3672
+ json(res, 200, { peers: await ctx.peers.list(route.id) });
3673
+ }
3331
3674
  function handleFiles({ req, res, route, runner, parked }) {
3332
3675
  requireMethod(req, "GET");
3333
3676
  const snapshotFiles = parked && !isDormant(parked) ? parked.snapshot.vfs : void 0;
@@ -3355,6 +3698,18 @@ async function handleStopTask({ req, res, route, runner }) {
3355
3698
  if (!await live.stopTask(route.stopTaskId)) fail(404, "no running task to stop");
3356
3699
  json(res, 200, { ok: true });
3357
3700
  }
3701
+ async function handleBackgroundTask({ req, res, route, runner }) {
3702
+ requireMethod(req, "POST");
3703
+ const live = requireLive(runner, "it has no running tasks");
3704
+ if (!live.backgroundTask) fail(501, `the ${engineOf(live.info())} engine cannot move a task to the background`);
3705
+ if (!await live.backgroundTask(route.backgroundTaskId)) fail(404, "no foreground task to move to the background");
3706
+ json(res, 200, { ok: true });
3707
+ }
3708
+ async function handleSleep({ ctx, req, res, runner, info }) {
3709
+ requireMethod(req, "POST");
3710
+ if (runner) await sleepRunner(runner);
3711
+ json(res, 200, { session: ctx.projects.withProject(runner?.info() ?? info) });
3712
+ }
3358
3713
  async function handlePermission({ ctx, req, res, route, runner }) {
3359
3714
  requireMethod(req, "POST");
3360
3715
  const body = await readJsonBody(req, ctx.maxBodyBytes);
@@ -3567,37 +3922,6 @@ function safeName(name) {
3567
3922
  return cleaned.length > 120 ? cleaned.slice(0, 120) : cleaned;
3568
3923
  }
3569
3924
  //#endregion
3570
- //#region src/lib/scope.ts
3571
- const MAX_SCOPE_KEYS = 16;
3572
- const MAX_SCOPE_LEN = 200;
3573
- function readScope(value) {
3574
- if (typeof value !== "object" || value === null || Array.isArray(value)) return;
3575
- const entries = Object.entries(value);
3576
- if (entries.some(([, v]) => typeof v !== "string")) return;
3577
- return Object.fromEntries(entries);
3578
- }
3579
- function checkScope(value) {
3580
- if (value === void 0) return null;
3581
- const scope = readScope(value);
3582
- if (!scope) return "scope must be an object of string values";
3583
- const entries = Object.entries(scope);
3584
- if (entries.length > MAX_SCOPE_KEYS) return `scope may carry at most ${MAX_SCOPE_KEYS} keys`;
3585
- for (const [key, val] of entries) {
3586
- if (key.length === 0) return "scope keys must not be empty";
3587
- if (key.length > MAX_SCOPE_LEN || val.length > MAX_SCOPE_LEN) return `scope keys and values must be at most ${MAX_SCOPE_LEN} characters`;
3588
- }
3589
- return null;
3590
- }
3591
- function sameScope(a, b) {
3592
- const left = Object.entries(a ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
3593
- const right = Object.entries(b ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
3594
- return left.length === right.length && left.every(([key, value], i) => right[i][0] === key && right[i][1] === value);
3595
- }
3596
- function scopeMatches(principal, session) {
3597
- if (!principal) return true;
3598
- return Object.entries(principal).every(([key, value]) => session?.[key] === value);
3599
- }
3600
- //#endregion
3601
3925
  //#region src/services/auth.ts
3602
3926
  function createAuthService(deps) {
3603
3927
  const { options, registry } = deps;
@@ -3707,6 +4031,188 @@ var AvailabilityTracker = class {
3707
4031
  }
3708
4032
  };
3709
4033
  //#endregion
4034
+ //#region src/services/engine-sleep.ts
4035
+ var EngineSleepTimers = class {
4036
+ #options;
4037
+ #timers = /* @__PURE__ */ new Map();
4038
+ #runners = /* @__PURE__ */ new Map();
4039
+ #closed = false;
4040
+ constructor(options) {
4041
+ this.#options = options;
4042
+ }
4043
+ get enabled() {
4044
+ return this.#options.afterMs > 0 && !this.#closed;
4045
+ }
4046
+ watch(runner) {
4047
+ if (!this.enabled || !runner.sleep) return;
4048
+ this.#runners.set(runner.id, runner);
4049
+ const unsubscribe = runner.subscribe((event) => {
4050
+ if (event.type === "status_changed") this.#rearm(runner);
4051
+ }, runner.info().lastSeq);
4052
+ this.#rearm(runner);
4053
+ return () => {
4054
+ unsubscribe();
4055
+ this.#disarm(runner.id);
4056
+ if (this.#runners.get(runner.id) === runner) this.#runners.delete(runner.id);
4057
+ };
4058
+ }
4059
+ onDetach(sessionId) {
4060
+ const runner = this.#runners.get(sessionId);
4061
+ if (runner) this.#rearm(runner);
4062
+ }
4063
+ close() {
4064
+ this.#closed = true;
4065
+ for (const timer of this.#timers.values()) clearTimeout(timer);
4066
+ this.#timers.clear();
4067
+ this.#runners.clear();
4068
+ }
4069
+ #rearm(runner) {
4070
+ this.#disarm(runner.id);
4071
+ if (!this.enabled || !this.#sleepable(runner)) return;
4072
+ const timer = setTimeout(() => {
4073
+ this.#timers.delete(runner.id);
4074
+ if (!this.enabled || !this.#sleepable(runner)) return;
4075
+ runner.sleep?.().catch((error) => this.#options.onError?.(error, runner.id));
4076
+ }, this.#options.afterMs);
4077
+ timer.unref?.();
4078
+ this.#timers.set(runner.id, timer);
4079
+ }
4080
+ #sleepable(runner) {
4081
+ const info = runner.info();
4082
+ return info.status === "idle" && info.engineAsleep !== true && this.#options.attachedCount(runner.id) === 0;
4083
+ }
4084
+ #disarm(sessionId) {
4085
+ clearTimeout(this.#timers.get(sessionId));
4086
+ this.#timers.delete(sessionId);
4087
+ }
4088
+ };
4089
+ const HOUR_MS = 36e5;
4090
+ const RESET_TIMEOUT_MS = 6e4;
4091
+ const CONTEXT_RESET_SCHEDULED = "Reset scheduled. It runs when this turn ends: your conversation is cleared and your prompt becomes the first message of a fresh one. End your turn now and start no new work. If the turn is interrupted or ends in an error, the reset is cancelled.";
4092
+ var ContextResetService = class {
4093
+ #options;
4094
+ #runners = /* @__PURE__ */ new Map();
4095
+ #pending = /* @__PURE__ */ new Map();
4096
+ #running = /* @__PURE__ */ new Set();
4097
+ #history = /* @__PURE__ */ new Map();
4098
+ #turnFailed = /* @__PURE__ */ new Set();
4099
+ #closed = false;
4100
+ constructor(options = {}) {
4101
+ this.#options = options;
4102
+ }
4103
+ get defaultEnabled() {
4104
+ return this.#options.default === true;
4105
+ }
4106
+ watch(runner) {
4107
+ if (this.#closed) return;
4108
+ this.#runners.set(runner.id, runner);
4109
+ const unsubscribe = runner.subscribe((event) => this.#observe(runner, event), runner.info().lastSeq);
4110
+ return () => {
4111
+ unsubscribe();
4112
+ if (this.#runners.get(runner.id) === runner) {
4113
+ this.#runners.delete(runner.id);
4114
+ this.#pending.delete(runner.id);
4115
+ }
4116
+ };
4117
+ }
4118
+ async request(sessionId, request) {
4119
+ const runner = this.#runners.get(sessionId);
4120
+ if (this.#closed || !runner || runner.info().agentContextReset !== true) throw new Error("context reset is not enabled for this session");
4121
+ if (!runner.clearContext) throw new Error("this engine cannot clear its conversation");
4122
+ if (this.#pending.has(sessionId) || this.#running.has(sessionId)) throw new Error("a reset is already scheduled for this session");
4123
+ const refused = this.#rateRefusal(sessionId);
4124
+ if (refused) throw new Error(refused);
4125
+ this.#pending.set(sessionId, {
4126
+ prompt: request.prompt,
4127
+ reason: request.reason
4128
+ });
4129
+ this.#turnFailed.delete(sessionId);
4130
+ return CONTEXT_RESET_SCHEDULED;
4131
+ }
4132
+ close() {
4133
+ this.#closed = true;
4134
+ this.#pending.clear();
4135
+ this.#runners.clear();
4136
+ }
4137
+ #rateRefusal(sessionId) {
4138
+ const now = this.#now();
4139
+ const minInterval = this.#options.minIntervalMs ?? 6e5;
4140
+ const maxPerHour = this.#options.maxPerHour ?? 3;
4141
+ const recent = (this.#history.get(sessionId) ?? []).filter((at) => now - at < HOUR_MS);
4142
+ const last = recent.at(-1);
4143
+ if (last !== void 0 && now - last < minInterval) return `refused: the last reset was ${minutes(now - last)} ago and resets need ${minutes(minInterval)} between them; keep working`;
4144
+ if (maxPerHour <= 0 || recent.length >= maxPerHour) return `refused: this session already reset ${recent.length} time(s) in the last hour (limit ${maxPerHour}); keep working`;
4145
+ return null;
4146
+ }
4147
+ #observe(runner, event) {
4148
+ if (!this.#pending.has(runner.id)) return;
4149
+ if (event.type === "turn_result" && event.isError) {
4150
+ this.#turnFailed.add(runner.id);
4151
+ return;
4152
+ }
4153
+ if (event.type === "session_closed") {
4154
+ this.#pending.delete(runner.id);
4155
+ return;
4156
+ }
4157
+ if (event.type !== "status_changed" || event.status !== "idle") return;
4158
+ const request = this.#pending.get(runner.id);
4159
+ this.#pending.delete(runner.id);
4160
+ if (this.#turnFailed.delete(runner.id)) return;
4161
+ this.#run(runner, request);
4162
+ }
4163
+ async #run(runner, request) {
4164
+ this.#running.add(runner.id);
4165
+ try {
4166
+ const reset = nextReset(runner, this.#options.resetTimeoutMs ?? RESET_TIMEOUT_MS);
4167
+ try {
4168
+ await runner.clearContext({ agentReason: request.reason });
4169
+ await reset.done;
4170
+ } finally {
4171
+ reset.cancel();
4172
+ }
4173
+ const history = (this.#history.get(runner.id) ?? []).filter((at) => this.#now() - at < HOUR_MS);
4174
+ history.push(this.#now());
4175
+ this.#history.set(runner.id, history);
4176
+ runner.sendMessage(request.prompt);
4177
+ } catch (error) {
4178
+ this.#options.onError?.(error, runner.id);
4179
+ } finally {
4180
+ this.#running.delete(runner.id);
4181
+ }
4182
+ }
4183
+ #now() {
4184
+ return this.#options.now?.() ?? Date.now();
4185
+ }
4186
+ };
4187
+ function nextReset(runner, timeoutMs) {
4188
+ let timer;
4189
+ let unsubscribe;
4190
+ const cancel = () => {
4191
+ clearTimeout(timer);
4192
+ unsubscribe?.();
4193
+ };
4194
+ return {
4195
+ done: new Promise((resolve, reject) => {
4196
+ timer = setTimeout(() => {
4197
+ cancel();
4198
+ reject(/* @__PURE__ */ new Error("the engine did not confirm the reset"));
4199
+ }, timeoutMs);
4200
+ timer.unref?.();
4201
+ unsubscribe = runner.subscribe((event) => {
4202
+ if (event.type === "conversation_reset") {
4203
+ cancel();
4204
+ resolve();
4205
+ }
4206
+ }, runner.info().lastSeq);
4207
+ }),
4208
+ cancel
4209
+ };
4210
+ }
4211
+ function minutes(ms) {
4212
+ const value = Math.max(1, Math.round(ms / 6e4));
4213
+ return `${value} minute${value === 1 ? "" : "s"}`;
4214
+ }
4215
+ //#endregion
3710
4216
  //#region src/services/bridge.ts
3711
4217
  var BridgeHub = class {
3712
4218
  #sessions = /* @__PURE__ */ new Map();
@@ -4022,7 +4528,10 @@ var SessionParkManager = class {
4022
4528
  }
4023
4529
  async listInfo() {
4024
4530
  await Promise.all(this.#storeOps.values());
4025
- return (await this.#options.store.list()).filter((record) => this.#options.registry.get(record.id) === void 0).map((record) => record.info);
4531
+ return (await this.#options.store.list()).filter((record) => this.#options.registry.get(record.id) === void 0).map((record) => isDormant(record) ? {
4532
+ ...record.info,
4533
+ engineAsleep: true
4534
+ } : record.info);
4026
4535
  }
4027
4536
  async ensureLive(id) {
4028
4537
  const live = this.#options.registry.get(id);
@@ -4272,7 +4781,7 @@ var SessionParkManager = class {
4272
4781
  }
4273
4782
  };
4274
4783
  function idleRecordBase(runner, config) {
4275
- const info = runner.info();
4784
+ const { engineAsleep: _engineAsleep, ...info } = runner.info();
4276
4785
  return {
4277
4786
  id: runner.id,
4278
4787
  info: {
@@ -4337,6 +4846,8 @@ var ProfileService = class {
4337
4846
  if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
4338
4847
  } else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
4339
4848
  if (disableBypassPermissions && p.defaults?.permissionMode === "bypassPermissions") return `profile '${p.name}' defaults to bypassPermissions but disableBypassPermissions is set`;
4849
+ const efforts = p.defaults?.efforts;
4850
+ if (efforts !== void 0 && !isEffortMap(efforts)) return `profile '${p.name}' defaults.efforts must map model names to effort levels`;
4340
4851
  const fallbackMode = p.defaults?.permissionMode;
4341
4852
  if (fallbackMode && !supportsPermissionMode(p.engine, fallbackMode)) return `profile '${p.name}' defaults to permission mode '${fallbackMode}', which engine '${engineOf(p)}' does not support (supported: ${adapterFor(engineOf(p)).capabilities.permissionModes.join(", ")})`;
4342
4853
  return null;
@@ -4438,6 +4949,9 @@ var ProfileService = class {
4438
4949
  };
4439
4950
  }
4440
4951
  };
4952
+ function isEffortMap(value) {
4953
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.entries(value).every(([model, effort]) => model !== "" && typeof effort === "string" && effort !== "");
4954
+ }
4441
4955
  //#endregion
4442
4956
  //#region src/services/profile-usage.ts
4443
4957
  var ProfileUsageTracker = class {
@@ -4625,179 +5139,160 @@ function createFileSpendStore(path, onError) {
4625
5139
  };
4626
5140
  }
4627
5141
  //#endregion
4628
- //#region src/services/peers.ts
4629
- const DEFAULT_PER_MINUTE = 10;
4630
- const DEFAULT_MAX_HOPS = 12;
4631
- const WINDOW_MS = 6e4;
4632
- function visible(from, to) {
4633
- return to.id !== from.id && scopeMatches(from.scope, to.scope);
4634
- }
4635
- function createPeerService(deps) {
4636
- const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
4637
- const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
4638
- const maxHops = deps.options?.maxHops ?? DEFAULT_MAX_HOPS;
4639
- const sent = /* @__PURE__ */ new Map();
4640
- const inbound = /* @__PURE__ */ new Map();
4641
- const registry = () => {
4642
- const found = deps.refs.registry;
4643
- if (!found) throw new Error("peer messaging is not ready");
4644
- return found;
4645
- };
4646
- const allSessions = async () => {
4647
- const live = registry().list();
4648
- const dormant = await deps.refs.parking?.listInfo() ?? [];
4649
- return [...live, ...dormant].map((info) => deps.projects.withProject(info));
4650
- };
4651
- const infoOf = async (id) => {
4652
- const live = registry().get(id)?.info();
4653
- if (live) return deps.projects.withProject(live);
4654
- const record = await deps.refs.parking?.get(id);
4655
- return record ? deps.projects.withProject(record.info) : void 0;
4656
- };
4657
- const sender = async (from) => {
4658
- const info = await infoOf(from);
4659
- if (!info) throw new Error(`unknown sender session: ${from}`);
4660
- return info;
4661
- };
4662
- const underRateLimit = (from, to) => {
4663
- const key = `${from}->${to}`;
4664
- const now = Date.now();
4665
- const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
4666
- if (recent.length >= perMinute) {
4667
- sent.set(key, recent);
4668
- return false;
4669
- }
4670
- recent.push(now);
4671
- sent.set(key, recent);
4672
- return true;
4673
- };
4674
- const list = async (from) => {
4675
- const me = await sender(from);
4676
- return (await allSessions()).filter((info) => visible(me, info)).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map(peerSummary);
5142
+ //#region src/services/peer-relay.ts
5143
+ const CARRIED = Symbol.for("workerdeck.relay.carried");
5144
+ const slots = globalThis;
5145
+ const UNAVAILABLE = "Remote gateways are unavailable right now; only sessions on this gateway are listed.";
5146
+ function expandHome(path) {
5147
+ return path === "~" || path.startsWith("~/") ? `${homedir()}${path.slice(1)}` : path;
5148
+ }
5149
+ function identityOf(options) {
5150
+ return JSON.stringify([
5151
+ options.url,
5152
+ options.gateway,
5153
+ options.keyFile ?? "",
5154
+ options.key ? "inline" : "",
5155
+ options.caFile ?? "",
5156
+ options.expose?.allow ?? null
5157
+ ]);
5158
+ }
5159
+ function remoteSummary(row) {
5160
+ return {
5161
+ id: `${row.gateway}:${row.id}`,
5162
+ gateway: row.gateway,
5163
+ engine: row.engine,
5164
+ status: row.status,
5165
+ title: row.title,
5166
+ project: row.project?.name,
5167
+ projectRoot: row.project?.root,
5168
+ cwd: row.cwd,
5169
+ profile: row.profile,
5170
+ model: row.model,
5171
+ contextUsage: row.contextUsage,
5172
+ lastActivityAt: row.lastActivityAt,
5173
+ pendingPermissionCount: row.pendingPermissionCount,
5174
+ allow: row.allow
4677
5175
  };
4678
- const peek = async (from, sessionId, options) => {
4679
- const me = await sender(from);
4680
- const info = await infoOf(sessionId);
4681
- if (!info || !visible(me, info)) return;
4682
- const limit = Math.min(Math.max(options?.recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
4683
- const runner = registry().get(sessionId);
4684
- const events = [];
4685
- if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
4686
- truncateResults: true,
4687
- imageRefs: true
4688
- })();
4689
- return {
4690
- ...peerSummary(info),
4691
- live: runner !== void 0,
4692
- checklist: info.checklist,
4693
- pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
4694
- recent: recentLines(events, limit)
4695
- };
5176
+ }
5177
+ function remotePeek(gateway, peek) {
5178
+ const { allow: _allow, ...summary } = remoteSummary({
5179
+ ...peek,
5180
+ gateway,
5181
+ allow: []
5182
+ });
5183
+ return {
5184
+ ...summary,
5185
+ live: peek.live,
5186
+ checklist: peek.checklistItems,
5187
+ pendingApprovals: peek.pendingApprovals,
5188
+ recent: peek.recent
4696
5189
  };
4697
- const send = async (from, sessionId, text, options) => {
4698
- const me = await sender(from);
4699
- if (sessionId === from) return {
4700
- delivered: false,
4701
- reason: "that is this session"
4702
- };
4703
- if (text.length > maxChars) return {
4704
- delivered: false,
4705
- reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
4706
- };
4707
- const target = await infoOf(sessionId);
4708
- if (!target || !visible(me, target)) return {
4709
- delivered: false,
4710
- reason: `no such session: ${sessionId}`
4711
- };
4712
- if (target.status === "closed" || target.status === "failed") return {
4713
- delivered: false,
4714
- reason: `session ${sessionId} is ${target.status}`
4715
- };
4716
- const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
4717
- if (hops.length > maxHops) return {
4718
- delivered: false,
4719
- reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
4720
- };
4721
- if (!underRateLimit(from, sessionId)) return {
4722
- delivered: false,
4723
- reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
4724
- };
4725
- const runner = await deps.refs.parking?.ensureLive(sessionId) ?? registry().get(sessionId);
4726
- if (!runner) return {
4727
- delivered: false,
4728
- reason: `session ${sessionId} could not be woken`
4729
- };
4730
- const before = runner.info().status;
4731
- const origin = {
4732
- kind: "peer",
4733
- sessionId: from,
4734
- name: me.title,
4735
- engine: me.engine,
4736
- hops
4737
- };
4738
- try {
4739
- runner.sendMessage(text, void 0, { origin });
4740
- } catch (error) {
4741
- return {
4742
- delivered: false,
4743
- reason: error instanceof Error ? error.message : String(error)
4744
- };
4745
- }
4746
- inbound.set(sessionId, hops);
4747
- return {
4748
- delivered: true,
4749
- sessionId,
4750
- name: target.title,
4751
- queued: before === "running" || before === "awaiting_approval"
4752
- };
5190
+ }
5191
+ async function readRelayKey(options) {
5192
+ if (options.key) return options.key;
5193
+ if (!options.keyFile) throw new Error("relay: set relay.keyFile (or WORKERDECK_RELAY_KEY) to the key `workerdeck relay enroll` printed");
5194
+ const key = (await readFile(expandHome(options.keyFile), "utf8")).trim();
5195
+ if (!key) throw new Error(`relay: ${options.keyFile} is empty`);
5196
+ return key;
5197
+ }
5198
+ function createRelayLink(options, peers, log) {
5199
+ const identity = identityOf(options);
5200
+ const exposed = options.expose?.scope;
5201
+ let connection;
5202
+ let closed = false;
5203
+ let released = false;
5204
+ const host = {
5205
+ snapshot: () => peers.relayEntries(exposed),
5206
+ peek: (_origin, sessionId, recent) => peers.relayPeek(sessionId, recent, exposed),
5207
+ send: (origin, sessionId, text) => peers.relaySend(origin, sessionId, text, exposed)
4753
5208
  };
4754
- const mentions = async (from, text) => {
4755
- const tokens = scanPeerMentions(text);
4756
- if (tokens.length === 0) return [];
4757
- const rows = await list(from);
4758
- const byKey = /* @__PURE__ */ new Map();
4759
- const add = (key, row) => {
4760
- const held = byKey.get(key);
4761
- if (held) held.push(row);
4762
- else byKey.set(key, [row]);
4763
- };
4764
- for (const row of rows) {
4765
- add(peerMentionKey(peerMentionSlug(row.title, row.id)), row);
4766
- add(peerMentionKey(row.id), row);
4767
- }
4768
- const resolved = [];
4769
- const seen = /* @__PURE__ */ new Set();
4770
- for (const token of tokens) {
4771
- const key = peerMentionKey(token.body);
4772
- const exact = byKey.get(key);
4773
- const matches = exact ?? (key.length >= 4 ? rows.filter((row) => row.id.startsWith(key)) : []);
4774
- const target = matches[0];
4775
- if (!target || matches.length > 1 && !exact) continue;
4776
- if (seen.has(target.id)) continue;
4777
- seen.add(target.id);
4778
- const others = matches.slice(1, 4).map((row) => row.id);
4779
- resolved.push({
4780
- typed: token.body,
4781
- id: target.id,
4782
- name: target.title,
4783
- engine: target.engine,
4784
- status: target.status,
4785
- cwd: target.cwd,
4786
- ...others.length ? { ambiguousWith: others } : {}
4787
- });
4788
- if (resolved.length >= PEER_MENTION_MAX) break;
4789
- }
4790
- return resolved;
5209
+ const carried = slots[CARRIED];
5210
+ if (carried) {
5211
+ delete slots[CARRIED];
5212
+ if (carried.identity === identity && carried.connection.state() !== "stopped") {
5213
+ carried.connection.setHost(host);
5214
+ connection = carried.connection;
5215
+ } else carried.connection.close();
5216
+ }
5217
+ if (!connection) (async () => {
5218
+ const key = await readRelayKey(options);
5219
+ const ca = options.caFile ? await readFile(expandHome(options.caFile)) : void 0;
5220
+ if (!closed) connection = connectRelay({
5221
+ url: options.url,
5222
+ gateway: options.gateway,
5223
+ key,
5224
+ ca,
5225
+ allow: options.expose?.allow,
5226
+ log
5227
+ }, host);
5228
+ })().catch((error) => log(error instanceof Error ? error.message : String(error)));
5229
+ const remoteTarget = parseRelayPeerId;
5230
+ const reachesRemote = async (from) => {
5231
+ const me = await peers.relaySender(from);
5232
+ return me.scope === void 0 || Object.keys(me.scope).length === 0;
4791
5233
  };
4792
- const watch = (runner) => runner.subscribe((event) => {
4793
- if (event.type === "user_message" && !event.origin && !event.synthetic && event.parentToolUseId == null && !event.replay) inbound.delete(runner.id);
4794
- }, runner.info().lastSeq);
4795
5234
  return {
4796
- list,
4797
- peek,
4798
- send,
4799
- mentions,
4800
- watch
5235
+ directory: {
5236
+ list: async (from) => {
5237
+ const local = await peers.list(from);
5238
+ if (!connection || connection.state() !== "online" || !await reachesRemote(from)) return local;
5239
+ try {
5240
+ return [...local, ...(await connection.list(from)).map(remoteSummary)];
5241
+ } catch {
5242
+ return local;
5243
+ }
5244
+ },
5245
+ notice: async (from) => {
5246
+ if (!await reachesRemote(from)) return;
5247
+ return connection?.state() === "online" ? void 0 : UNAVAILABLE;
5248
+ },
5249
+ peek: async (from, sessionId, peekOptions) => {
5250
+ const target = remoteTarget(sessionId);
5251
+ if (!target) return peers.peek(from, sessionId, peekOptions);
5252
+ if (!connection || !await reachesRemote(from)) return;
5253
+ const peek = await connection.peek(from, target, peekOptions?.recent);
5254
+ return peek ? remotePeek(target.gateway, peek) : void 0;
5255
+ },
5256
+ send: async (from, sessionId, text, sendOptions) => {
5257
+ const target = remoteTarget(sessionId);
5258
+ if (!target) return peers.send(from, sessionId, text, sendOptions);
5259
+ if (!await reachesRemote(from)) return {
5260
+ delivered: false,
5261
+ reason: `no such session: ${sessionId}`
5262
+ };
5263
+ if (!connection || connection.state() !== "online") return {
5264
+ delivered: false,
5265
+ reason: "remote gateways are unavailable right now; try again later"
5266
+ };
5267
+ try {
5268
+ const result = await connection.send(from, target, text, sendOptions?.hops ?? peers.relayChain(from));
5269
+ return result.delivered ? {
5270
+ ...result,
5271
+ sessionId
5272
+ } : result;
5273
+ } catch (error) {
5274
+ return {
5275
+ delivered: false,
5276
+ reason: error instanceof Error ? error.message : String(error)
5277
+ };
5278
+ }
5279
+ }
5280
+ },
5281
+ nudge: () => connection?.nudge(),
5282
+ release: () => {
5283
+ if (connection && !closed) {
5284
+ released = true;
5285
+ slots[CARRIED] = {
5286
+ identity,
5287
+ connection
5288
+ };
5289
+ }
5290
+ },
5291
+ close: () => {
5292
+ closed = true;
5293
+ if (!released) connection?.close();
5294
+ connection = void 0;
5295
+ }
4801
5296
  };
4802
5297
  }
4803
5298
  //#endregion
@@ -5022,8 +5517,16 @@ function createSessionFactory(deps) {
5022
5517
  ...deps.peers ? { peers: deps.peers } : {},
5023
5518
  ...deps.pricing ? { pricing: deps.pricing } : {}
5024
5519
  };
5520
+ delete config.contextReset;
5025
5521
  delete config.shells;
5026
5522
  delete config.shellAgentWrite;
5523
+ delete config.effortDefaults;
5524
+ const efforts = {
5525
+ ...deps.effortDefaults,
5526
+ ...profile?.defaults?.efforts
5527
+ };
5528
+ if (Object.keys(efforts).length > 0) config.effortDefaults = efforts;
5529
+ if (deps.contextReset && (built.agentContextReset ?? profile?.defaults?.agentContextReset ?? deps.contextReset.defaultEnabled)) config.contextReset = deps.contextReset.directory;
5027
5530
  if (deps.shells && capabilities.hostCwd === true) {
5028
5531
  config.shells = deps.shells;
5029
5532
  const write = deps.shellAgentWrite ?? "read-only";
@@ -5173,6 +5676,17 @@ function createWorkerServer(options = {}) {
5173
5676
  onError: options.notifications?.onError ?? ((error, context) => diagnose(error, `notification-${context.op}`))
5174
5677
  });
5175
5678
  const producedFiles = new ProducedFileStore();
5679
+ const engineSleep = new EngineSleepTimers({
5680
+ afterMs: options.engineSleepAfterMs ?? 0,
5681
+ attachedCount: (sessionId) => bridge.attachedCount(sessionId),
5682
+ onError: (error) => diagnose(error, "engine-sleep")
5683
+ });
5684
+ const contextResets = options.agentContextReset === false ? void 0 : new ContextResetService({
5685
+ ...options.agentContextReset,
5686
+ onError: (error) => diagnose(error, "context-reset")
5687
+ });
5688
+ let ownContextResets = contextResets;
5689
+ installContextResetDirectory(contextResets);
5176
5690
  const registry = new SessionRegistry({ onRegister: (runner) => {
5177
5691
  const detachers = [
5178
5692
  notifier.watch(runner),
@@ -5181,7 +5695,9 @@ function createWorkerServer(options = {}) {
5181
5695
  spendLedger.watch(runner),
5182
5696
  shells?.watch(runner),
5183
5697
  peers?.watch(runner),
5184
- factory.watchAuthSource(runner)
5698
+ factory.watchAuthSource(runner),
5699
+ engineSleep.watch(runner),
5700
+ contextResets?.watch(runner)
5185
5701
  ];
5186
5702
  const profile = runner.info().profile;
5187
5703
  if (profile) detachers.push(runner.subscribe((event) => {
@@ -5222,10 +5738,12 @@ function createWorkerServer(options = {}) {
5222
5738
  options: options.peers
5223
5739
  });
5224
5740
  const shellDirectory = shells ? createShellDirectory(shells, { runnerFor: (id) => registry.get(id) }) : void 0;
5225
- let ownPeers = peers;
5741
+ const relay = peers && options.relay ? createRelayLink(options.relay, peers, options.relay.log ?? ((message) => diagnose(new Error(message), "relay"))) : void 0;
5742
+ let ownPeers = relay?.directory ?? peers;
5226
5743
  let ownShells = shellDirectory;
5227
- if (peers) installPeerDirectory(peers);
5744
+ if (ownPeers) installPeerDirectory(ownPeers);
5228
5745
  installShellDirectory(shellDirectory);
5746
+ if (options.effortDefaults !== void 0 && !isEffortMap(options.effortDefaults)) throw new Error("createWorkerServer: `effortDefaults` must map model names to effort levels");
5229
5747
  const factory = createSessionFactory({
5230
5748
  adapterFor,
5231
5749
  profiles,
@@ -5234,8 +5752,13 @@ function createWorkerServer(options = {}) {
5234
5752
  allowedCwdRoots: options.allowedCwdRoots,
5235
5753
  disableBypassPermissions: options.disableBypassPermissions,
5236
5754
  approvalTimeoutMs: options.approvalTimeoutMs,
5755
+ effortDefaults: options.effortDefaults,
5237
5756
  requireApiKey: options.requireApiKey,
5238
5757
  peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
5758
+ contextReset: contextResets ? {
5759
+ directory: contextResetDirectoryHandle(() => ownContextResets),
5760
+ defaultEnabled: contextResets.defaultEnabled
5761
+ } : void 0,
5239
5762
  shells: shellDirectory ? shellDirectoryHandle(() => ownShells) : void 0,
5240
5763
  shellAgentWrite: options.shell?.agentWrite,
5241
5764
  pricing: pricing.pricing,
@@ -5291,7 +5814,8 @@ function createWorkerServer(options = {}) {
5291
5814
  factory,
5292
5815
  registry,
5293
5816
  parking,
5294
- peers,
5817
+ engineSleep,
5818
+ peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
5295
5819
  bridge,
5296
5820
  projects,
5297
5821
  queue,
@@ -5338,11 +5862,15 @@ function createWorkerServer(options = {}) {
5338
5862
  parking,
5339
5863
  shells,
5340
5864
  queue,
5865
+ engineSleep,
5341
5866
  closeQueueSockets: queueSockets.clear,
5342
5867
  diagnose,
5343
5868
  releaseDirectories: () => {
5869
+ relay?.close();
5344
5870
  ownPeers = void 0;
5345
5871
  ownShells = void 0;
5872
+ contextResets?.close();
5873
+ ownContextResets = void 0;
5346
5874
  }
5347
5875
  });
5348
5876
  return {
@@ -5383,6 +5911,7 @@ function createWorkerServer(options = {}) {
5383
5911
  parking.adopt(runner, config);
5384
5912
  return true;
5385
5913
  },
5914
+ releaseRelay: () => relay?.release(),
5386
5915
  drain: lifecycle.drain,
5387
5916
  close: lifecycle.close
5388
5917
  };
@@ -5441,7 +5970,8 @@ function rebuildRunner(factory, record) {
5441
5970
  resume: record.sdkSessionId
5442
5971
  }),
5443
5972
  epoch: (record.info.epoch ?? 0) + 1,
5444
- fallbackTitle: record.info.title
5973
+ fallbackTitle: record.info.title,
5974
+ startAsleep: true
5445
5975
  }, void 0, record.id);
5446
5976
  }
5447
5977
  //#endregion