@workerdeck/server 3.3.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";
@@ -79,6 +79,7 @@ function closeServer(deps) {
79
79
  shells?.killAll("server_stopped");
80
80
  const flushed = shells?.flush().catch((error) => deps.diagnose(error, "shell-flush")) ?? Promise.resolve();
81
81
  parking.close();
82
+ deps.engineSleep.close();
82
83
  registry.closeAll();
83
84
  for (const ws of wss.clients) ws.close(1001, "server shutting down");
84
85
  deps.closeQueueSockets();
@@ -222,6 +223,23 @@ function cwdAllowed(cwd, roots) {
222
223
  return resolved === r || resolved.startsWith(r + sep);
223
224
  });
224
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
+ }
225
243
  function readProfileConfig(profile) {
226
244
  const dir = profile.configDir;
227
245
  if (!dir) return {
@@ -230,22 +248,8 @@ function readProfileConfig(profile) {
230
248
  agents: [],
231
249
  commands: []
232
250
  };
233
- const listDirs = (path) => {
234
- try {
235
- return readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
236
- } catch {
237
- return [];
238
- }
239
- };
240
- const listMd = (path) => {
241
- try {
242
- return readdirSync(path).filter((file) => file.endsWith(".md")).map((file) => file.slice(0, -3)).sort();
243
- } catch {
244
- return [];
245
- }
246
- };
247
251
  const snapshot = {
248
- hasUserMemory: existsSync(join(dir, "CLAUDE.md")),
252
+ hasUserMemory: ["CLAUDE.md", "AGENTS.md"].some((name) => existsSync(join(dir, name))),
249
253
  skills: listDirs(join(dir, "skills")),
250
254
  agents: listMd(join(dir, "agents")),
251
255
  commands: listMd(join(dir, "commands"))
@@ -253,7 +257,6 @@ function readProfileConfig(profile) {
253
257
  try {
254
258
  const raw = JSON.parse(readFileSync(join(dir, "settings.json"), "utf8"));
255
259
  const permissions = raw.permissions ?? {};
256
- const count = (rules) => Array.isArray(rules) ? rules.length : 0;
257
260
  snapshot.settings = {
258
261
  model: typeof raw.model === "string" ? raw.model : void 0,
259
262
  defaultPermissionMode: typeof permissions.defaultMode === "string" ? permissions.defaultMode : void 0,
@@ -388,6 +391,15 @@ function itemRoute(rawId, parts) {
388
391
  id: decode(rawId),
389
392
  stopTaskId: decode(third)
390
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
+ };
391
403
  if (length === 3 && section === "project" && third === "icon") return {
392
404
  kind: "project-icon",
393
405
  id: decode(rawId)
@@ -400,6 +412,14 @@ function itemRoute(rawId, parts) {
400
412
  resultSeq: seq
401
413
  } : null;
402
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
+ };
403
423
  if (length <= 3 && section === "mcp") return {
404
424
  kind: "mcp",
405
425
  id: decode(rawId),
@@ -464,6 +484,303 @@ function permissionDecision(input) {
464
484
  };
465
485
  }
466
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
467
784
  //#region src/services/shell-directory.ts
468
785
  function createShellDirectory(registry, deps = {}) {
469
786
  const take = async (from, shellId, options) => {
@@ -1853,13 +2170,14 @@ function attachClient(ctx, ws, runner, req, access) {
1853
2170
  unsubscribe();
1854
2171
  detachBridge();
1855
2172
  parking.onDetach(runner.id);
2173
+ ctx.engineSleep.onDetach(runner.id);
1856
2174
  });
1857
2175
  }
1858
2176
  async function handleCommand(ctx, frame, runner, access, client) {
1859
2177
  const { attachmentStore, bridge } = ctx;
1860
2178
  switch (frame.type) {
1861
2179
  case "user_message": {
1862
- 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(() => []) : [];
1863
2181
  const options = mentions.length > 0 ? { mentions } : void 0;
1864
2182
  if (!frame.attachmentIds?.length) {
1865
2183
  runner.sendMessage(frame.text, void 0, options);
@@ -1876,6 +2194,13 @@ async function handleCommand(ctx, frame, runner, access, client) {
1876
2194
  case "interrupt":
1877
2195
  await runner.interrupt();
1878
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;
1879
2204
  case "clear_context":
1880
2205
  if (!runner.clearContext) throw new Error(`the ${engineOf(runner.info())} engine cannot clear a conversation`);
1881
2206
  await runner.clearContext();
@@ -1892,6 +2217,11 @@ async function handleCommand(ctx, frame, runner, access, client) {
1892
2217
  case "set_model":
1893
2218
  await runner.setModel(frame.model);
1894
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;
1895
2225
  case "tool_call_result":
1896
2226
  bridge.resolve(runner.id, frame.executionId, {
1897
2227
  output: frame.output,
@@ -2503,10 +2833,13 @@ const HOST_ONLY_KEY_TABLE = {
2503
2833
  historyFn: "transient",
2504
2834
  sessionInfoFn: "transient",
2505
2835
  peers: "transient",
2836
+ contextReset: "transient",
2506
2837
  shells: "transient",
2507
2838
  shellAgentWrite: "transient",
2508
2839
  createdByOperator: "durable",
2509
2840
  fallbackTitle: "transient",
2841
+ startAsleep: "transient",
2842
+ effortDefaults: "transient",
2510
2843
  connectFn: "transient",
2511
2844
  codexHome: "durable",
2512
2845
  codexPathOverride: "durable",
@@ -2521,6 +2854,7 @@ const HOST_ONLY_KEY_TABLE = {
2521
2854
  toolTitles: "transient",
2522
2855
  shouldApprove: "transient",
2523
2856
  resolveModel: "transient",
2857
+ contextWindow: "transient",
2524
2858
  reportMcpServers: "transient",
2525
2859
  onClose: "transient",
2526
2860
  restore: "transient"
@@ -3260,12 +3594,15 @@ const ITEM_HANDLERS = {
3260
3594
  produced: ({ ctx, req, res, route }) => handleProducedFiles(ctx, req, res, route.id, route.producedFileId),
3261
3595
  shells: ({ ctx, req, res, route, runner, auth }) => handleShells(ctx, req, res, route, runner ?? null, ctx.auth.isOperator(auth)),
3262
3596
  "stop-task": handleStopTask,
3597
+ "background-task": handleBackgroundTask,
3263
3598
  "project-icon": ({ ctx, req, res, info }) => handleProjectIcon(ctx.projects, req, res, info.cwd),
3264
3599
  "tool-result": ({ req, res, route, runner, parked }) => {
3265
3600
  const snapshot = parked && !isDormant(parked) ? parked.snapshot.events : void 0;
3266
3601
  handleToolResult(req, res, runner?.eventAt?.bind(runner) ?? (snapshot && ((seq) => snapshot.find((event) => event.seq === seq))), route.resultSeq);
3267
3602
  },
3268
- permission: handlePermission
3603
+ permission: handlePermission,
3604
+ peers: handlePeers,
3605
+ sleep: handleSleep
3269
3606
  };
3270
3607
  async function handleSessions(ctx, req, res, route, auth) {
3271
3608
  if (route.kind === "collection") {
@@ -3329,6 +3666,11 @@ async function handleSession({ ctx, req, res, route, runner, parked, info }) {
3329
3666
  status: "closed"
3330
3667
  }) });
3331
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
+ }
3332
3674
  function handleFiles({ req, res, route, runner, parked }) {
3333
3675
  requireMethod(req, "GET");
3334
3676
  const snapshotFiles = parked && !isDormant(parked) ? parked.snapshot.vfs : void 0;
@@ -3356,6 +3698,18 @@ async function handleStopTask({ req, res, route, runner }) {
3356
3698
  if (!await live.stopTask(route.stopTaskId)) fail(404, "no running task to stop");
3357
3699
  json(res, 200, { ok: true });
3358
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
+ }
3359
3713
  async function handlePermission({ ctx, req, res, route, runner }) {
3360
3714
  requireMethod(req, "POST");
3361
3715
  const body = await readJsonBody(req, ctx.maxBodyBytes);
@@ -3568,37 +3922,6 @@ function safeName(name) {
3568
3922
  return cleaned.length > 120 ? cleaned.slice(0, 120) : cleaned;
3569
3923
  }
3570
3924
  //#endregion
3571
- //#region src/lib/scope.ts
3572
- const MAX_SCOPE_KEYS = 16;
3573
- const MAX_SCOPE_LEN = 200;
3574
- function readScope(value) {
3575
- if (typeof value !== "object" || value === null || Array.isArray(value)) return;
3576
- const entries = Object.entries(value);
3577
- if (entries.some(([, v]) => typeof v !== "string")) return;
3578
- return Object.fromEntries(entries);
3579
- }
3580
- function checkScope(value) {
3581
- if (value === void 0) return null;
3582
- const scope = readScope(value);
3583
- if (!scope) return "scope must be an object of string values";
3584
- const entries = Object.entries(scope);
3585
- if (entries.length > MAX_SCOPE_KEYS) return `scope may carry at most ${MAX_SCOPE_KEYS} keys`;
3586
- for (const [key, val] of entries) {
3587
- if (key.length === 0) return "scope keys must not be empty";
3588
- if (key.length > MAX_SCOPE_LEN || val.length > MAX_SCOPE_LEN) return `scope keys and values must be at most ${MAX_SCOPE_LEN} characters`;
3589
- }
3590
- return null;
3591
- }
3592
- function sameScope(a, b) {
3593
- const left = Object.entries(a ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
3594
- const right = Object.entries(b ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
3595
- return left.length === right.length && left.every(([key, value], i) => right[i][0] === key && right[i][1] === value);
3596
- }
3597
- function scopeMatches(principal, session) {
3598
- if (!principal) return true;
3599
- return Object.entries(principal).every(([key, value]) => session?.[key] === value);
3600
- }
3601
- //#endregion
3602
3925
  //#region src/services/auth.ts
3603
3926
  function createAuthService(deps) {
3604
3927
  const { options, registry } = deps;
@@ -3708,6 +4031,188 @@ var AvailabilityTracker = class {
3708
4031
  }
3709
4032
  };
3710
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
3711
4216
  //#region src/services/bridge.ts
3712
4217
  var BridgeHub = class {
3713
4218
  #sessions = /* @__PURE__ */ new Map();
@@ -4023,7 +4528,10 @@ var SessionParkManager = class {
4023
4528
  }
4024
4529
  async listInfo() {
4025
4530
  await Promise.all(this.#storeOps.values());
4026
- 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);
4027
4535
  }
4028
4536
  async ensureLive(id) {
4029
4537
  const live = this.#options.registry.get(id);
@@ -4273,7 +4781,7 @@ var SessionParkManager = class {
4273
4781
  }
4274
4782
  };
4275
4783
  function idleRecordBase(runner, config) {
4276
- const info = runner.info();
4784
+ const { engineAsleep: _engineAsleep, ...info } = runner.info();
4277
4785
  return {
4278
4786
  id: runner.id,
4279
4787
  info: {
@@ -4338,6 +4846,8 @@ var ProfileService = class {
4338
4846
  if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
4339
4847
  } else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
4340
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`;
4341
4851
  const fallbackMode = p.defaults?.permissionMode;
4342
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(", ")})`;
4343
4853
  return null;
@@ -4439,6 +4949,9 @@ var ProfileService = class {
4439
4949
  };
4440
4950
  }
4441
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
+ }
4442
4955
  //#endregion
4443
4956
  //#region src/services/profile-usage.ts
4444
4957
  var ProfileUsageTracker = class {
@@ -4783,264 +5296,6 @@ function createRelayLink(options, peers, log) {
4783
5296
  };
4784
5297
  }
4785
5298
  //#endregion
4786
- //#region src/services/peers.ts
4787
- const DEFAULT_PER_MINUTE = 10;
4788
- const DEFAULT_MAX_HOPS = 12;
4789
- const WINDOW_MS = 6e4;
4790
- function visible(from, to) {
4791
- return to.id !== from.id && scopeMatches(from.scope, to.scope);
4792
- }
4793
- function relayEntry(info, live) {
4794
- const items = info.checklist;
4795
- return {
4796
- id: info.id,
4797
- engine: info.engine,
4798
- status: info.status,
4799
- title: info.title,
4800
- project: info.project ? {
4801
- name: info.project.name,
4802
- root: info.project.root
4803
- } : void 0,
4804
- cwd: info.cwd,
4805
- profile: info.profile,
4806
- model: info.model,
4807
- permissionMode: info.permissionMode,
4808
- contextUsage: info.contextUsage,
4809
- checklist: items?.length ? {
4810
- done: items.filter((item) => item.status === "completed").length,
4811
- total: items.length
4812
- } : void 0,
4813
- numTurns: info.numTurns,
4814
- createdAt: info.createdAt,
4815
- lastActivityAt: info.lastActivityAt,
4816
- pendingPermissionCount: info.pendingPermissionCount,
4817
- live
4818
- };
4819
- }
4820
- function createPeerService(deps) {
4821
- const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
4822
- const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
4823
- const maxHops = deps.options?.maxHops ?? DEFAULT_MAX_HOPS;
4824
- const sent = /* @__PURE__ */ new Map();
4825
- const inbound = /* @__PURE__ */ new Map();
4826
- const registry = () => {
4827
- const found = deps.refs.registry;
4828
- if (!found) throw new Error("peer messaging is not ready");
4829
- return found;
4830
- };
4831
- const allSessions = async () => {
4832
- const live = registry().list();
4833
- const dormant = await deps.refs.parking?.listInfo() ?? [];
4834
- return [...live, ...dormant].map((info) => deps.projects.withProject(info));
4835
- };
4836
- const infoOf = async (id) => {
4837
- const live = registry().get(id)?.info();
4838
- if (live) return deps.projects.withProject(live);
4839
- const record = await deps.refs.parking?.get(id);
4840
- return record ? deps.projects.withProject(record.info) : void 0;
4841
- };
4842
- const sender = async (from) => {
4843
- const info = await infoOf(from);
4844
- if (!info) throw new Error(`unknown sender session: ${from}`);
4845
- return info;
4846
- };
4847
- const recentOf = (sessionId, recent) => {
4848
- const limit = Math.min(Math.max(recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
4849
- const runner = registry().get(sessionId);
4850
- const events = [];
4851
- if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
4852
- truncateResults: true,
4853
- imageRefs: true
4854
- })();
4855
- return {
4856
- runner,
4857
- lines: recentLines(events, limit)
4858
- };
4859
- };
4860
- const underRateLimit = (from, to) => {
4861
- const key = `${from}->${to}`;
4862
- const now = Date.now();
4863
- const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
4864
- if (recent.length >= perMinute) {
4865
- sent.set(key, recent);
4866
- return false;
4867
- }
4868
- recent.push(now);
4869
- sent.set(key, recent);
4870
- return true;
4871
- };
4872
- const list = async (from) => {
4873
- const me = await sender(from);
4874
- return (await allSessions()).filter((info) => visible(me, info)).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map(peerSummary);
4875
- };
4876
- const peek = async (from, sessionId, options) => {
4877
- const me = await sender(from);
4878
- const info = await infoOf(sessionId);
4879
- if (!info || !visible(me, info)) return;
4880
- const { runner, lines } = recentOf(sessionId, options?.recent);
4881
- return {
4882
- ...peerSummary(info),
4883
- live: runner !== void 0,
4884
- checklist: info.checklist,
4885
- pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
4886
- recent: lines
4887
- };
4888
- };
4889
- const send = async (from, sessionId, text, options) => {
4890
- const me = await sender(from);
4891
- if (sessionId === from) return {
4892
- delivered: false,
4893
- reason: "that is this session"
4894
- };
4895
- if (text.length > maxChars) return {
4896
- delivered: false,
4897
- reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
4898
- };
4899
- const target = await infoOf(sessionId);
4900
- if (!target || !visible(me, target)) return {
4901
- delivered: false,
4902
- reason: `no such session: ${sessionId}`
4903
- };
4904
- const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
4905
- return deliver(target, text, {
4906
- kind: "peer",
4907
- sessionId: from,
4908
- name: me.title,
4909
- engine: me.engine,
4910
- hops
4911
- }, from);
4912
- };
4913
- const deliver = async (target, text, origin, rateKey) => {
4914
- const sessionId = target.id;
4915
- if (target.status === "closed" || target.status === "failed") return {
4916
- delivered: false,
4917
- reason: `session ${sessionId} is ${target.status}`
4918
- };
4919
- const hops = origin.hops ?? [];
4920
- if (hops.length > maxHops) return {
4921
- delivered: false,
4922
- reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
4923
- };
4924
- if (!underRateLimit(rateKey, sessionId)) return {
4925
- delivered: false,
4926
- reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
4927
- };
4928
- const runner = await deps.refs.parking?.ensureLive(sessionId) ?? registry().get(sessionId);
4929
- if (!runner) return {
4930
- delivered: false,
4931
- reason: `session ${sessionId} could not be woken`
4932
- };
4933
- const before = runner.info().status;
4934
- try {
4935
- runner.sendMessage(text, void 0, { origin });
4936
- } catch (error) {
4937
- return {
4938
- delivered: false,
4939
- reason: error instanceof Error ? error.message : String(error)
4940
- };
4941
- }
4942
- inbound.set(sessionId, hops);
4943
- return {
4944
- delivered: true,
4945
- sessionId,
4946
- name: target.title,
4947
- queued: before === "running" || before === "awaiting_approval"
4948
- };
4949
- };
4950
- const exposedInfo = async (sessionId, exposed) => {
4951
- const info = await infoOf(sessionId);
4952
- return info && info.status !== "closed" && scopeMatches(exposed, info.scope) ? info : void 0;
4953
- };
4954
- const relayEntries = async (exposed) => {
4955
- const live = new Set(registry().list().map((info) => info.id));
4956
- return (await allSessions()).filter((info) => info.status !== "closed" && scopeMatches(exposed, info.scope)).map((info) => relayEntry(info, live.has(info.id)));
4957
- };
4958
- const relayPeek = async (sessionId, recent, exposed) => {
4959
- const info = await exposedInfo(sessionId, exposed);
4960
- if (!info) return;
4961
- const { runner, lines } = recentOf(sessionId, recent);
4962
- return {
4963
- ...relayEntry(info, runner !== void 0),
4964
- checklistItems: info.checklist,
4965
- pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
4966
- recent: lines
4967
- };
4968
- };
4969
- const relaySend = async (origin, sessionId, text, exposed) => {
4970
- if (text.length === 0 || text.length > maxChars) return {
4971
- delivered: false,
4972
- reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
4973
- };
4974
- const target = await exposedInfo(sessionId, exposed);
4975
- if (!target) return {
4976
- delivered: false,
4977
- reason: `no such session: ${sessionId}`
4978
- };
4979
- const from = `${origin.gateway}:${origin.sessionId}`;
4980
- return deliver(target, text, {
4981
- kind: "peer",
4982
- sessionId: from,
4983
- hostId: origin.gateway,
4984
- name: origin.name,
4985
- engine: origin.engine,
4986
- hops: origin.hops
4987
- }, from);
4988
- };
4989
- const mentions = async (from, text) => {
4990
- const tokens = scanPeerMentions(text);
4991
- if (tokens.length === 0) return [];
4992
- const rows = await list(from);
4993
- const byKey = /* @__PURE__ */ new Map();
4994
- const add = (key, row) => {
4995
- const held = byKey.get(key);
4996
- if (held) held.push(row);
4997
- else byKey.set(key, [row]);
4998
- };
4999
- for (const row of rows) {
5000
- add(peerMentionKey(peerMentionSlug(row.title, row.id)), row);
5001
- add(peerMentionKey(row.id), row);
5002
- }
5003
- const resolved = [];
5004
- const seen = /* @__PURE__ */ new Set();
5005
- for (const token of tokens) {
5006
- const key = peerMentionKey(token.body);
5007
- const exact = byKey.get(key);
5008
- const matches = exact ?? (key.length >= 4 ? rows.filter((row) => row.id.startsWith(key)) : []);
5009
- const target = matches[0];
5010
- if (!target || matches.length > 1 && !exact) continue;
5011
- if (seen.has(target.id)) continue;
5012
- seen.add(target.id);
5013
- const others = matches.slice(1, 4).map((row) => row.id);
5014
- resolved.push({
5015
- typed: token.body,
5016
- id: target.id,
5017
- name: target.title,
5018
- engine: target.engine,
5019
- status: target.status,
5020
- cwd: target.cwd,
5021
- ...others.length ? { ambiguousWith: others } : {}
5022
- });
5023
- if (resolved.length >= PEER_MENTION_MAX) break;
5024
- }
5025
- return resolved;
5026
- };
5027
- const watch = (runner) => runner.subscribe((event) => {
5028
- if (event.type === "user_message" && !event.origin && !event.synthetic && event.parentToolUseId == null && !event.replay) inbound.delete(runner.id);
5029
- }, runner.info().lastSeq);
5030
- return {
5031
- list,
5032
- peek,
5033
- send,
5034
- mentions,
5035
- watch,
5036
- relaySender: sender,
5037
- relayChain: (from) => [...inbound.get(from) ?? [], from],
5038
- relayEntries,
5039
- relayPeek,
5040
- relaySend
5041
- };
5042
- }
5043
- //#endregion
5044
5299
  //#region src/services/registry.ts
5045
5300
  var SessionRegistry = class {
5046
5301
  #sessions = /* @__PURE__ */ new Map();
@@ -5262,8 +5517,16 @@ function createSessionFactory(deps) {
5262
5517
  ...deps.peers ? { peers: deps.peers } : {},
5263
5518
  ...deps.pricing ? { pricing: deps.pricing } : {}
5264
5519
  };
5520
+ delete config.contextReset;
5265
5521
  delete config.shells;
5266
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;
5267
5530
  if (deps.shells && capabilities.hostCwd === true) {
5268
5531
  config.shells = deps.shells;
5269
5532
  const write = deps.shellAgentWrite ?? "read-only";
@@ -5413,6 +5676,17 @@ function createWorkerServer(options = {}) {
5413
5676
  onError: options.notifications?.onError ?? ((error, context) => diagnose(error, `notification-${context.op}`))
5414
5677
  });
5415
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);
5416
5690
  const registry = new SessionRegistry({ onRegister: (runner) => {
5417
5691
  const detachers = [
5418
5692
  notifier.watch(runner),
@@ -5421,7 +5695,9 @@ function createWorkerServer(options = {}) {
5421
5695
  spendLedger.watch(runner),
5422
5696
  shells?.watch(runner),
5423
5697
  peers?.watch(runner),
5424
- factory.watchAuthSource(runner)
5698
+ factory.watchAuthSource(runner),
5699
+ engineSleep.watch(runner),
5700
+ contextResets?.watch(runner)
5425
5701
  ];
5426
5702
  const profile = runner.info().profile;
5427
5703
  if (profile) detachers.push(runner.subscribe((event) => {
@@ -5467,6 +5743,7 @@ function createWorkerServer(options = {}) {
5467
5743
  let ownShells = shellDirectory;
5468
5744
  if (ownPeers) installPeerDirectory(ownPeers);
5469
5745
  installShellDirectory(shellDirectory);
5746
+ if (options.effortDefaults !== void 0 && !isEffortMap(options.effortDefaults)) throw new Error("createWorkerServer: `effortDefaults` must map model names to effort levels");
5470
5747
  const factory = createSessionFactory({
5471
5748
  adapterFor,
5472
5749
  profiles,
@@ -5475,8 +5752,13 @@ function createWorkerServer(options = {}) {
5475
5752
  allowedCwdRoots: options.allowedCwdRoots,
5476
5753
  disableBypassPermissions: options.disableBypassPermissions,
5477
5754
  approvalTimeoutMs: options.approvalTimeoutMs,
5755
+ effortDefaults: options.effortDefaults,
5478
5756
  requireApiKey: options.requireApiKey,
5479
5757
  peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
5758
+ contextReset: contextResets ? {
5759
+ directory: contextResetDirectoryHandle(() => ownContextResets),
5760
+ defaultEnabled: contextResets.defaultEnabled
5761
+ } : void 0,
5480
5762
  shells: shellDirectory ? shellDirectoryHandle(() => ownShells) : void 0,
5481
5763
  shellAgentWrite: options.shell?.agentWrite,
5482
5764
  pricing: pricing.pricing,
@@ -5532,7 +5814,8 @@ function createWorkerServer(options = {}) {
5532
5814
  factory,
5533
5815
  registry,
5534
5816
  parking,
5535
- peers,
5817
+ engineSleep,
5818
+ peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
5536
5819
  bridge,
5537
5820
  projects,
5538
5821
  queue,
@@ -5579,12 +5862,15 @@ function createWorkerServer(options = {}) {
5579
5862
  parking,
5580
5863
  shells,
5581
5864
  queue,
5865
+ engineSleep,
5582
5866
  closeQueueSockets: queueSockets.clear,
5583
5867
  diagnose,
5584
5868
  releaseDirectories: () => {
5585
5869
  relay?.close();
5586
5870
  ownPeers = void 0;
5587
5871
  ownShells = void 0;
5872
+ contextResets?.close();
5873
+ ownContextResets = void 0;
5588
5874
  }
5589
5875
  });
5590
5876
  return {
@@ -5684,7 +5970,8 @@ function rebuildRunner(factory, record) {
5684
5970
  resume: record.sdkSessionId
5685
5971
  }),
5686
5972
  epoch: (record.info.epoch ?? 0) + 1,
5687
- fallbackTitle: record.info.title
5973
+ fallbackTitle: record.info.title,
5974
+ startAsleep: true
5688
5975
  }, void 0, record.id);
5689
5976
  }
5690
5977
  //#endregion