@workerdeck/server 3.4.0 → 3.6.1

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,9 +1,9 @@
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, contextResetDirectoryHandle, createEngineSession, encodeShellKeys, getEngineAdapter, installContextResetDirectory, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText, withoutGatewaySecrets } from "@workerdeck/core";
4
+ import { AVATAR_SEED_MAX, 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, avatarDirectoryHandle, checkClaudeAuth, clampShellTail, composeInstructions, contextResetDirectoryHandle, createEngineSession, encodeShellKeys, getEngineAdapter, installAvatarDirectory, 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
- 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";
6
+ import { AGENT_SLEEP_AFTER_MS_DEFAULT, 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, agentRef, costOfByModel, imagePartRef, isJobRun, mergeByModel, mergePricing, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, readStatusLabelInput, scanPeerMentions, sessionState, subscriptionComparison, supportsPermissionMode, teamReaches } 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";
8
8
  import { arch, homedir, hostname, platform, tmpdir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -281,6 +281,661 @@ function reloadPlan(runner) {
281
281
  return "carry";
282
282
  }
283
283
  //#endregion
284
+ //#region src/services/agents.ts
285
+ const NAMES = [
286
+ "Atlas",
287
+ "Juno",
288
+ "Pip",
289
+ "Rook",
290
+ "Marlow",
291
+ "Orbit",
292
+ "Fern",
293
+ "Quill",
294
+ "Sable",
295
+ "Wren",
296
+ "Ember",
297
+ "Moss",
298
+ "Nova",
299
+ "Otto",
300
+ "Pike",
301
+ "Rune",
302
+ "Sage",
303
+ "Tansy",
304
+ "Vale",
305
+ "Yarrow",
306
+ "Basil",
307
+ "Cedar",
308
+ "Dune",
309
+ "Flint",
310
+ "Hazel",
311
+ "Iris",
312
+ "Kestrel",
313
+ "Lark",
314
+ "Maple",
315
+ "Nettle",
316
+ "Onyx",
317
+ "Piper",
318
+ "Reed",
319
+ "Sorrel",
320
+ "Thistle",
321
+ "Umber",
322
+ "Willow",
323
+ "Zephyr"
324
+ ];
325
+ const MAX_NAME = 64;
326
+ const MAX_BRIEF = 16e3;
327
+ const CONFIG_KEYS = [
328
+ "cwd",
329
+ "profile",
330
+ "model",
331
+ "reasoningEffort",
332
+ "permissionMode",
333
+ "brief",
334
+ "agentContextReset",
335
+ "sleepAfterMs"
336
+ ];
337
+ var AgentService = class {
338
+ #store;
339
+ #basePath;
340
+ #avatars;
341
+ #sleepAfterMs;
342
+ #now;
343
+ #agents = /* @__PURE__ */ new Map();
344
+ constructor(options) {
345
+ this.#store = options.store;
346
+ this.#basePath = options.basePath;
347
+ this.#avatars = options.avatars ?? false;
348
+ this.#sleepAfterMs = options.sleepAfterMs ?? AGENT_SLEEP_AFTER_MS_DEFAULT;
349
+ this.#now = options.now ?? Date.now;
350
+ }
351
+ async hydrate() {
352
+ this.#agents = new Map((await this.#store.list()).map((agent) => [agent.id, agent]));
353
+ }
354
+ list() {
355
+ return [...this.#agents.values()].map((agent) => this.public(agent));
356
+ }
357
+ get(id) {
358
+ return this.#agents.get(id);
359
+ }
360
+ bySession(sessionId) {
361
+ for (const agent of this.#agents.values()) if (agent.sessionId === sessionId) return agent;
362
+ }
363
+ decorate(info) {
364
+ const agent = this.bySession(info.id);
365
+ if (!agent) return info;
366
+ const leadName = agent.lead === void 0 ? void 0 : this.#agents.get(agent.lead)?.name;
367
+ return {
368
+ ...info,
369
+ agent: agentRef(this.public(agent), {
370
+ leadName,
371
+ leads: this.hasMembers(agent.id)
372
+ })
373
+ };
374
+ }
375
+ briefFor(sessionId) {
376
+ return (sessionId === void 0 ? void 0 : this.bySession(sessionId)?.config.brief?.trim()) || void 0;
377
+ }
378
+ sleepAfterFor(sessionId) {
379
+ const agent = this.bySession(sessionId);
380
+ return agent ? agent.config.sleepAfterMs ?? this.#sleepAfterMs : void 0;
381
+ }
382
+ public(agent) {
383
+ const { avatarSeed: _seed, avatarRecipe: _recipe, avatar: _avatar, ...info } = agent;
384
+ if (!this.#avatars) return info;
385
+ const version = agent.avatarSeed === agent.id ? "" : `?v=${createHash("sha1").update(agent.avatarSeed).digest("hex").slice(0, 10)}`;
386
+ return {
387
+ ...info,
388
+ avatar: `${this.#basePath}/agents/${agent.id}/avatar.png${version}`
389
+ };
390
+ }
391
+ draft(input) {
392
+ const config = readConfig(input.config);
393
+ if ("error" in config) return config;
394
+ const name = input.name === void 0 ? this.#suggestName() : readName(input.name);
395
+ if (typeof name !== "string") return name;
396
+ const id = randomUUID();
397
+ const at = this.#now();
398
+ const agent = {
399
+ id,
400
+ name,
401
+ createdAt: at,
402
+ updatedAt: at,
403
+ avatarSeed: id,
404
+ pastSessions: [],
405
+ config
406
+ };
407
+ if (input.lead !== void 0 && input.lead !== null) {
408
+ const refused = typeof input.lead === "string" ? this.leadRefusal(agent, input.lead) : {
409
+ status: 400,
410
+ error: "lead must be an agent id"
411
+ };
412
+ if (refused) return refused;
413
+ agent.lead = input.lead;
414
+ }
415
+ return agent;
416
+ }
417
+ async save(agent) {
418
+ const stored = {
419
+ ...agent,
420
+ updatedAt: this.#now()
421
+ };
422
+ await this.#store.save(stored);
423
+ this.#agents.set(stored.id, stored);
424
+ return stored;
425
+ }
426
+ async bind(agent, sessionId) {
427
+ const past = agent.sessionId !== void 0 && agent.sessionId !== sessionId ? [...agent.pastSessions, agent.sessionId] : agent.pastSessions;
428
+ return this.save({
429
+ ...agent,
430
+ sessionId,
431
+ pastSessions: past
432
+ });
433
+ }
434
+ hasMembers(id) {
435
+ for (const agent of this.#agents.values()) if (agent.lead === id) return true;
436
+ return false;
437
+ }
438
+ members(id) {
439
+ return [...this.#agents.values()].filter((agent) => agent.lead === id).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
440
+ }
441
+ leadRefusal(mover, leadId) {
442
+ if (leadId === mover.id) return {
443
+ status: 409,
444
+ error: "an agent cannot lead its own team"
445
+ };
446
+ const lead = this.#agents.get(leadId);
447
+ if (!lead) return {
448
+ status: 404,
449
+ error: `no such agent: ${leadId}`
450
+ };
451
+ if (lead.lead !== void 0) return {
452
+ status: 409,
453
+ error: `${lead.name} is a member of a team; teams are one level deep`
454
+ };
455
+ if (this.hasMembers(mover.id)) return {
456
+ status: 409,
457
+ error: `${mover.name} leads a team; teams are one level deep`
458
+ };
459
+ return null;
460
+ }
461
+ async update(id, patch) {
462
+ const agent = this.#agents.get(id);
463
+ if (!agent) return {
464
+ status: 404,
465
+ error: `no such agent: ${id}`
466
+ };
467
+ const next = { ...agent };
468
+ if (patch.name !== void 0) {
469
+ const name = readName(patch.name);
470
+ if (typeof name !== "string") return name;
471
+ next.name = name;
472
+ }
473
+ if (patch.config !== void 0) {
474
+ if (!isRecord$1(patch.config)) return {
475
+ status: 400,
476
+ error: "config must be an object"
477
+ };
478
+ const config = readConfig({
479
+ ...agent.config,
480
+ ...patch.config
481
+ });
482
+ if ("error" in config) return config;
483
+ next.config = config;
484
+ }
485
+ if (patch.lead === null) delete next.lead;
486
+ else if (patch.lead !== void 0) {
487
+ if (typeof patch.lead !== "string") return {
488
+ status: 400,
489
+ error: "lead must be an agent id or null"
490
+ };
491
+ const refused = this.leadRefusal(agent, patch.lead);
492
+ if (refused) return refused;
493
+ next.lead = patch.lead;
494
+ }
495
+ if (patch.order !== void 0) {
496
+ if (typeof patch.order !== "number" || !Number.isFinite(patch.order)) return {
497
+ status: 400,
498
+ error: "order must be a number"
499
+ };
500
+ next.order = patch.order;
501
+ }
502
+ return this.save(next);
503
+ }
504
+ async retire(id, members) {
505
+ const agent = this.#agents.get(id);
506
+ if (!agent) return {
507
+ status: 404,
508
+ error: `no such agent: ${id}`
509
+ };
510
+ const crew = this.members(id);
511
+ const retired = [agent, ...members === "retire" ? crew : []];
512
+ const released = [];
513
+ if (members === "release") for (const member of crew) {
514
+ const { lead: _lead, ...rest } = member;
515
+ released.push(await this.save(rest));
516
+ }
517
+ for (const gone of retired) {
518
+ await this.#store.delete(gone.id);
519
+ this.#agents.delete(gone.id);
520
+ }
521
+ return {
522
+ retired,
523
+ released
524
+ };
525
+ }
526
+ #suggestName() {
527
+ const taken = new Set([...this.#agents.values()].map((agent) => agent.name.toLowerCase()));
528
+ const free = NAMES.find((name) => !taken.has(name.toLowerCase()));
529
+ if (free) return free;
530
+ for (let n = 2;; n++) {
531
+ const name = `${NAMES[n % NAMES.length]} ${n}`;
532
+ if (!taken.has(name.toLowerCase())) return name;
533
+ }
534
+ }
535
+ };
536
+ function isAgentRefusal(value) {
537
+ return isRecord$1(value) && typeof value.status === "number" && typeof value.error === "string";
538
+ }
539
+ function readName(value) {
540
+ if (typeof value !== "string" || value.trim() === "") return {
541
+ status: 400,
542
+ error: "name must be a non-empty string"
543
+ };
544
+ const name = value.trim();
545
+ return name.length > MAX_NAME ? {
546
+ status: 400,
547
+ error: `name is longer than ${MAX_NAME} characters`
548
+ } : name;
549
+ }
550
+ function readConfig(value) {
551
+ if (value === void 0) return {};
552
+ if (!isRecord$1(value)) return {
553
+ status: 400,
554
+ error: "config must be an object"
555
+ };
556
+ const config = {};
557
+ for (const key of CONFIG_KEYS) if (value[key] !== void 0) config[key] = value[key];
558
+ for (const key of [
559
+ "cwd",
560
+ "profile",
561
+ "model",
562
+ "reasoningEffort",
563
+ "permissionMode",
564
+ "brief"
565
+ ]) if (config[key] !== void 0 && typeof config[key] !== "string") return {
566
+ status: 400,
567
+ error: `config.${key} must be a string`
568
+ };
569
+ if (config.agentContextReset !== void 0 && typeof config.agentContextReset !== "boolean") return {
570
+ status: 400,
571
+ error: "config.agentContextReset must be a boolean"
572
+ };
573
+ const sleep = config.sleepAfterMs;
574
+ if (sleep !== void 0 && (typeof sleep !== "number" || !Number.isFinite(sleep) || sleep < 0)) return {
575
+ status: 400,
576
+ error: "config.sleepAfterMs must be a non-negative number"
577
+ };
578
+ if (typeof config.brief === "string" && config.brief.length > MAX_BRIEF) return {
579
+ status: 400,
580
+ error: `config.brief is longer than ${MAX_BRIEF} characters`
581
+ };
582
+ return config;
583
+ }
584
+ function isRecord$1(value) {
585
+ return typeof value === "object" && value !== null && !Array.isArray(value);
586
+ }
587
+ //#endregion
588
+ //#region src/lib/host-only-keys.ts
589
+ const HOST_ONLY_KEY_TABLE = {
590
+ epoch: "durable",
591
+ pricing: "transient",
592
+ queryFn: "transient",
593
+ env: "transient",
594
+ pathToClaudeCodeExecutable: "durable",
595
+ extraOptions: "transient",
596
+ instructions: "transient",
597
+ defaultApprovalTimeoutMs: "durable",
598
+ backfillHistory: "durable",
599
+ historyFn: "transient",
600
+ sessionInfoFn: "transient",
601
+ peers: "transient",
602
+ contextReset: "transient",
603
+ avatar: "transient",
604
+ shells: "transient",
605
+ shellAgentWrite: "transient",
606
+ createdByOperator: "durable",
607
+ fallbackTitle: "transient",
608
+ startAsleep: "transient",
609
+ effortDefaults: "transient",
610
+ statusLabel: "durable",
611
+ connectFn: "transient",
612
+ codexHome: "durable",
613
+ codexPathOverride: "durable",
614
+ languageModel: "transient",
615
+ tools: "transient",
616
+ maxSteps: "durable",
617
+ executor: "transient",
618
+ executableTools: "transient",
619
+ vfs: "transient",
620
+ executionLimits: "durable",
621
+ executionBackend: "transient",
622
+ toolTitles: "transient",
623
+ shouldApprove: "transient",
624
+ resolveModel: "transient",
625
+ contextWindow: "transient",
626
+ reportMcpServers: "transient",
627
+ onClose: "transient",
628
+ restore: "transient"
629
+ };
630
+ const HOST_ONLY_KEYS = new Set(Object.keys(HOST_ONLY_KEY_TABLE));
631
+ const DURABLE_HOST_KEYS = Object.entries(HOST_ONLY_KEY_TABLE).filter(([, lifetime]) => lifetime === "durable").map(([key]) => key);
632
+ //#endregion
633
+ //#region src/lib/permissions.ts
634
+ const OPERATOR_ONLY_MODES = /* @__PURE__ */ new Set(["bypassPermissions", "dontAsk"]);
635
+ function refusePermissionMode(mode, policy) {
636
+ if (mode === "bypassPermissions" && policy.disableBypass) return "bypassPermissions is disabled on this server (disableBypassPermissions)";
637
+ if (!policy.operator && mode !== void 0 && OPERATOR_ONLY_MODES.has(mode)) return `permission mode '${mode}' is reserved to operators`;
638
+ return null;
639
+ }
640
+ function permissionDecision(input) {
641
+ return input.behavior === "allow" ? {
642
+ behavior: "allow",
643
+ updatedInput: input.updatedInput
644
+ } : {
645
+ behavior: "deny",
646
+ message: input.message,
647
+ interrupt: input.interrupt
648
+ };
649
+ }
650
+ //#endregion
651
+ //#region src/routes/create-vet.ts
652
+ function vetCreateRequest(ctx, body, auth) {
653
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return {
654
+ ok: false,
655
+ status: 400,
656
+ error: "request body must be a JSON object"
657
+ };
658
+ const hostOnly = Object.keys(body).filter((key) => HOST_ONLY_KEYS.has(key));
659
+ if (hostOnly.length > 0) return {
660
+ ok: false,
661
+ status: 400,
662
+ error: `host-only runner config is not accepted on a session request: ${hostOnly.join(", ")} (set it from the gateway's buildRunnerConfig hook)`
663
+ };
664
+ const req = pickCreateSessionRequest(body);
665
+ const { availability, factory } = ctx;
666
+ const early = factory.applyScope(req, auth) ?? refusal(403, factory.applyBypassPolicy(req));
667
+ if (early) return {
668
+ ok: false,
669
+ ...early
670
+ };
671
+ const resolved = factory.resolveProfile(req.profile, auth.allowedProfiles);
672
+ if (!resolved.ok) return resolved;
673
+ const refused = refusal(403, refuseHostAuthority(ctx, req, resolved.profile, auth)) ?? availability.checkAvailable(resolved.profile) ?? factory.checkCwd(req, resolved.profile) ?? refusal(400, factory.checkPermissionMode(req.permissionMode, resolved.profile) ?? factory.checkEngineGrants(req, resolved.profile));
674
+ if (refused) return {
675
+ ok: false,
676
+ ...refused
677
+ };
678
+ factory.stripInertFields(req, resolved.profile);
679
+ req.profile = resolved.profile?.name;
680
+ return {
681
+ ok: true,
682
+ request: req
683
+ };
684
+ }
685
+ function refusal(status, error) {
686
+ return error === null ? null : {
687
+ status,
688
+ error
689
+ };
690
+ }
691
+ function refuseHostAuthority(ctx, req, profile, auth) {
692
+ if (ctx.auth.isOperator(auth)) return null;
693
+ const engine = engineOf(profile);
694
+ const name = profile?.name ?? "default";
695
+ if ((engine === "claude" || engine === "codex") && !auth.allowedProfiles?.includes(name)) return `profile '${name}' runs the ${engine} engine on the host; a non-operator caller needs it named in its allowedProfiles`;
696
+ const mode = refusePermissionMode(req.permissionMode, { operator: false });
697
+ if (mode) return mode;
698
+ if (req.allowDangerouslySkipPermissions) return "allowDangerouslySkipPermissions is reserved to operators";
699
+ if (req.settingSources !== void 0 && req.settingSources.length > 0) return "settingSources is reserved to operators: it loads hooks and settings from the host";
700
+ if (!(typeof req.mcpServers === "object" && req.mcpServers !== null ? Object.values(req.mcpServers) : []).every(isRemoteMcpServer)) return "a stdio MCP server is reserved to operators: it runs a command on the host";
701
+ if (req.resume !== void 0 && !ownsSdkSession(ctx, auth, req.resume)) return "resume is limited to a live session this caller can see";
702
+ return null;
703
+ }
704
+ function ownsSdkSession(ctx, auth, sdkSessionId) {
705
+ return ctx.registry.list().some((info) => info.sdkSessionId === sdkSessionId && ctx.auth.canSee(auth, info));
706
+ }
707
+ function isRemoteMcpServer(server) {
708
+ const type = server?.type;
709
+ return type === "http" || type === "sse";
710
+ }
711
+ //#endregion
712
+ //#region src/routes/session-lookup.ts
713
+ async function sessionInfoOf(ctx, id) {
714
+ return ctx.registry.get(id)?.info() ?? (await ctx.parking.get(id))?.info;
715
+ }
716
+ async function resolveSession(ctx, id, auth) {
717
+ const runner = ctx.registry.get(id);
718
+ const parked = runner ? null : await ctx.parking.get(id);
719
+ const info = runner?.info() ?? parked?.info;
720
+ if (!info || !ctx.auth.canSee(auth, info)) fail(404, "session not found");
721
+ return {
722
+ runner,
723
+ parked,
724
+ info
725
+ };
726
+ }
727
+ function requireLive(runner, why) {
728
+ if (!runner) fail(409, `session is parked (${why})`);
729
+ return runner;
730
+ }
731
+ //#endregion
732
+ //#region src/routes/agents.ts
733
+ async function handleAgents(ctx, req, res, pathname, auth) {
734
+ const agents = ctx.agents;
735
+ const rest = pathname.slice((ctx.basePath + "/agents").length).replace(/^\//, "");
736
+ if (rest === "") {
737
+ requireMethod(req, "GET", "POST");
738
+ if (req.method === "GET") {
739
+ json(res, 200, { agents: agents.list() });
740
+ return;
741
+ }
742
+ await createAgent(ctx, agents, req, res, auth);
743
+ return;
744
+ }
745
+ const [id, action, ...extra] = rest.split("/").map(decodeURIComponent);
746
+ const agent = id === void 0 ? void 0 : agents.get(id);
747
+ if (!agent || extra.length > 0) fail(404, "agent not found");
748
+ if (action === "restart") {
749
+ requireMethod(req, "POST");
750
+ const body = await readJsonBody(req, ctx.maxBodyBytes) ?? {};
751
+ const previous = agent.sessionId === void 0 ? void 0 : ctx.registry.get(agent.sessionId);
752
+ const created = await startSession(ctx, agent, typeof body.prompt === "string" ? body.prompt : void 0, auth);
753
+ const status = previous?.info().status;
754
+ if (previous && status !== "closed" && status !== "failed") previous.close("server");
755
+ await respond(ctx, res, 200, await bindWithAvatar(ctx, agent, created), created);
756
+ return;
757
+ }
758
+ if (action === "avatar") {
759
+ requireMethod(req, "POST");
760
+ await respond(ctx, res, 200, await rerollAvatar(ctx, agent, readSeed((await readJsonBody(req, ctx.maxBodyBytes) ?? {}).seed)));
761
+ return;
762
+ }
763
+ if (action === "avatar-preview.png") {
764
+ requireMethod(req, "GET");
765
+ const seed = readSeed(new URL(req.url ?? "", "http://x").searchParams.get("seed") ?? void 0);
766
+ if (seed === void 0) fail(400, "seed is required");
767
+ const recipe = await rollFor(ctx, agent, seed);
768
+ await sendImage(req, res, await ctx.avatars.still(recipe));
769
+ return;
770
+ }
771
+ if (action === "avatar.png" || action === "avatar-busy.png") {
772
+ requireMethod(req, "GET");
773
+ await sendAvatar(ctx, req, res, agent, action === "avatar-busy.png");
774
+ return;
775
+ }
776
+ if (action !== void 0) fail(404, "agent not found");
777
+ requireMethod(req, "GET", "PATCH", "DELETE");
778
+ if (req.method === "GET") {
779
+ await respond(ctx, res, 200, agent);
780
+ return;
781
+ }
782
+ if (req.method === "PATCH") {
783
+ const patch = await readJsonBody(req, ctx.maxBodyBytes);
784
+ const updated = await agents.update(agent.id, isRecord(patch) ? patch : {});
785
+ if (isAgentRefusal(updated)) fail(updated.status, updated.error);
786
+ await respond(ctx, res, 200, updated);
787
+ return;
788
+ }
789
+ const members = (await readJsonBody(req, ctx.maxBodyBytes) ?? {}).members === "retire" ? "retire" : "release";
790
+ const outcome = await agents.retire(agent.id, members);
791
+ if (isAgentRefusal(outcome)) fail(outcome.status, outcome.error);
792
+ for (const gone of outcome.retired) {
793
+ const runner = gone.sessionId === void 0 ? void 0 : ctx.registry.get(gone.sessionId);
794
+ const status = runner?.info().status;
795
+ if (runner && status !== "closed" && status !== "failed") runner.close("client");
796
+ }
797
+ json(res, 200, {
798
+ retired: outcome.retired.map((a) => a.id),
799
+ released: outcome.released.map((a) => a.id)
800
+ });
801
+ }
802
+ async function createAgent(ctx, agents, req, res, auth) {
803
+ const body = await readJsonBody(req, ctx.maxBodyBytes);
804
+ if (!isRecord(body)) fail(400, "request body must be a JSON object");
805
+ if (body.adopt !== void 0) {
806
+ await adoptSession(ctx, agents, res, body);
807
+ return;
808
+ }
809
+ const draft = agents.draft(body);
810
+ if (isAgentRefusal(draft)) fail(draft.status, draft.error);
811
+ const created = await startSession(ctx, draft, typeof body.prompt === "string" ? body.prompt : void 0, auth);
812
+ await respond(ctx, res, 201, await bindWithAvatar(ctx, draft, created), created);
813
+ }
814
+ async function adoptSession(ctx, agents, res, body) {
815
+ if (typeof body.adopt !== "string") fail(400, "adopt must be a session id");
816
+ const info = await sessionInfoOf(ctx, body.adopt);
817
+ if (!info) fail(404, "session not found");
818
+ if (agents.bySession(info.id)) fail(409, "that session already belongs to an agent");
819
+ const draft = agents.draft({
820
+ name: body.name ?? info.title,
821
+ config: {
822
+ ...configOf(info),
823
+ ...body.config
824
+ },
825
+ lead: body.lead
826
+ });
827
+ if (isAgentRefusal(draft)) fail(draft.status, draft.error);
828
+ await respond(ctx, res, 201, await bindWithAvatar(ctx, draft, info));
829
+ }
830
+ function readSeed(seed) {
831
+ if (seed === void 0 || seed === null || seed === "") return;
832
+ if (typeof seed !== "string" || seed.trim().length === 0 || seed.length > AVATAR_SEED_MAX) fail(400, `seed must be a string of at most ${AVATAR_SEED_MAX} characters`);
833
+ return seed.trim();
834
+ }
835
+ async function rollFor(ctx, agent, seed) {
836
+ if (!ctx.avatars) fail(404, "this gateway draws no avatars");
837
+ const session = agent.sessionId === void 0 ? void 0 : await sessionInfoOf(ctx, agent.sessionId);
838
+ const recipe = await rollAvatar(ctx, {
839
+ ...agent,
840
+ avatarSeed: seed
841
+ }, session);
842
+ if (recipe === void 0) fail(503, "the avatar pack could not be loaded");
843
+ return recipe;
844
+ }
845
+ async function rerollAvatar(ctx, agent, seed) {
846
+ const next = seed ?? randomUUID();
847
+ const recipe = await rollFor(ctx, agent, next);
848
+ return ctx.agents.save({
849
+ ...agent,
850
+ avatarSeed: next,
851
+ avatarRecipe: recipe
852
+ });
853
+ }
854
+ async function bindWithAvatar(ctx, agent, session) {
855
+ const recipe = agent.avatarRecipe ?? await rollAvatar(ctx, agent, session);
856
+ return ctx.agents.bind(recipe === void 0 ? agent : {
857
+ ...agent,
858
+ avatarRecipe: recipe
859
+ }, session.id);
860
+ }
861
+ async function rollAvatar(ctx, agent, session) {
862
+ if (!ctx.avatars) return;
863
+ const project = session ? ctx.projects.withProject(session).project?.root ?? session.cwd : void 0;
864
+ try {
865
+ return await ctx.avatars.roll(agent.avatarSeed, session?.engine, project || void 0);
866
+ } catch {
867
+ return;
868
+ }
869
+ }
870
+ async function sendAvatar(ctx, req, res, agent, busy) {
871
+ const avatars = ctx.avatars;
872
+ if (!avatars) fail(404, "this gateway draws no avatars");
873
+ let recipe = agent.avatarRecipe;
874
+ if (recipe === void 0) {
875
+ recipe = await rollAvatar(ctx, agent, agent.sessionId === void 0 ? void 0 : await sessionInfoOf(ctx, agent.sessionId));
876
+ if (recipe === void 0) fail(503, "the avatar pack could not be loaded");
877
+ await ctx.agents.save({
878
+ ...agent,
879
+ avatarRecipe: recipe
880
+ });
881
+ }
882
+ const image = busy ? await avatars.busy(recipe) : await avatars.still(recipe);
883
+ if (!image) fail(404, "this avatar pack has no busy animation");
884
+ await sendImage(req, res, image);
885
+ }
886
+ async function sendImage(req, res, image) {
887
+ const headers = {
888
+ "content-type": "image/png",
889
+ etag: image.etag,
890
+ "cache-control": "private, no-cache"
891
+ };
892
+ if (image.durations) headers["x-frame-durations"] = image.durations.join(",");
893
+ if (req.headers["if-none-match"] === image.etag) {
894
+ res.writeHead(304, headers).end();
895
+ return;
896
+ }
897
+ res.writeHead(200, headers).end(Buffer.from(image.png));
898
+ }
899
+ async function startSession(ctx, agent, prompt, auth) {
900
+ const { config } = agent;
901
+ const vetted = vetCreateRequest(ctx, stripUndefined({
902
+ cwd: config.cwd,
903
+ profile: config.profile,
904
+ model: config.model,
905
+ reasoningEffort: config.reasoningEffort,
906
+ permissionMode: config.permissionMode,
907
+ allowDangerouslySkipPermissions: config.permissionMode === "bypassPermissions" ? true : void 0,
908
+ agentContextReset: config.agentContextReset ?? true,
909
+ prompt
910
+ }), auth);
911
+ if (!vetted.ok) fail(vetted.status, vetted.error);
912
+ return (await ctx.factory.createRunner(ctx.factory.buildRunnerConfig(vetted.request, { operator: ctx.auth.isOperator(auth) }), {
913
+ brief: config.brief?.trim() || void 0,
914
+ agent: true
915
+ })).info();
916
+ }
917
+ async function respond(ctx, res, status, agent, session) {
918
+ const info = session ?? (agent.sessionId === void 0 ? void 0 : await sessionInfoOf(ctx, agent.sessionId));
919
+ const body = { agent: ctx.agents.public(agent) };
920
+ if (info) body.session = ctx.projects.withProject(info);
921
+ json(res, status, body);
922
+ }
923
+ function configOf(info) {
924
+ return stripUndefined({
925
+ cwd: info.cwd || void 0,
926
+ profile: info.profile,
927
+ model: info.model,
928
+ reasoningEffort: info.effort ?? void 0,
929
+ permissionMode: info.permissionMode
930
+ });
931
+ }
932
+ function stripUndefined(value) {
933
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
934
+ }
935
+ function isRecord(value) {
936
+ return typeof value === "object" && value !== null && !Array.isArray(value);
937
+ }
938
+ //#endregion
284
939
  //#region src/routes/queue-ws.ts
285
940
  function createQueueSocketHub(deps) {
286
941
  const sockets = /* @__PURE__ */ new Set();
@@ -446,44 +1101,6 @@ function decode(segment) {
446
1101
  }
447
1102
  }
448
1103
  //#endregion
449
- //#region src/routes/session-lookup.ts
450
- async function sessionInfoOf(ctx, id) {
451
- return ctx.registry.get(id)?.info() ?? (await ctx.parking.get(id))?.info;
452
- }
453
- async function resolveSession(ctx, id, auth) {
454
- const runner = ctx.registry.get(id);
455
- const parked = runner ? null : await ctx.parking.get(id);
456
- const info = runner?.info() ?? parked?.info;
457
- if (!info || !ctx.auth.canSee(auth, info)) fail(404, "session not found");
458
- return {
459
- runner,
460
- parked,
461
- info
462
- };
463
- }
464
- function requireLive(runner, why) {
465
- if (!runner) fail(409, `session is parked (${why})`);
466
- return runner;
467
- }
468
- //#endregion
469
- //#region src/lib/permissions.ts
470
- const OPERATOR_ONLY_MODES = /* @__PURE__ */ new Set(["bypassPermissions", "dontAsk"]);
471
- function refusePermissionMode(mode, policy) {
472
- if (mode === "bypassPermissions" && policy.disableBypass) return "bypassPermissions is disabled on this server (disableBypassPermissions)";
473
- if (!policy.operator && mode !== void 0 && OPERATOR_ONLY_MODES.has(mode)) return `permission mode '${mode}' is reserved to operators`;
474
- return null;
475
- }
476
- function permissionDecision(input) {
477
- return input.behavior === "allow" ? {
478
- behavior: "allow",
479
- updatedInput: input.updatedInput
480
- } : {
481
- behavior: "deny",
482
- message: input.message,
483
- interrupt: input.interrupt
484
- };
485
- }
486
- //#endregion
487
1104
  //#region src/lib/scope.ts
488
1105
  const MAX_SCOPE_KEYS = 16;
489
1106
  const MAX_SCOPE_LEN = 200;
@@ -520,7 +1137,10 @@ const DEFAULT_PER_MINUTE = 10;
520
1137
  const DEFAULT_MAX_HOPS = 12;
521
1138
  const WINDOW_MS = 6e4;
522
1139
  function visible(from, to) {
523
- return to.id !== from.id && scopeMatches(from.scope, to.scope);
1140
+ return to.id !== from.id && scopeMatches(from.scope, to.scope) && teamReaches(from.agent, to.agent);
1141
+ }
1142
+ function relayable(info, exposed) {
1143
+ return info.status !== "closed" && info.agent?.lead === void 0 && scopeMatches(exposed, info.scope);
524
1144
  }
525
1145
  function relayEntry(info, live) {
526
1146
  const items = info.checklist;
@@ -681,11 +1301,11 @@ function createPeerService(deps) {
681
1301
  };
682
1302
  const exposedInfo = async (sessionId, exposed) => {
683
1303
  const info = await infoOf(sessionId);
684
- return info && info.status !== "closed" && scopeMatches(exposed, info.scope) ? info : void 0;
1304
+ return info && relayable(info, exposed) ? info : void 0;
685
1305
  };
686
1306
  const relayEntries = async (exposed) => {
687
1307
  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)));
1308
+ return (await allSessions()).filter((info) => relayable(info, exposed)).map((info) => relayEntry(info, live.has(info.id)));
689
1309
  };
690
1310
  const relayPeek = async (sessionId, recent, exposed) => {
691
1311
  const info = await exposedInfo(sessionId, exposed);
@@ -743,8 +1363,9 @@ function resolvePeerMentions(rows, text) {
743
1363
  else byKey.set(key, [row]);
744
1364
  };
745
1365
  for (const row of rows) {
746
- add(peerMentionKey(peerMentionSlug(row.title, row.id)), row);
747
- add(peerMentionKey(row.id), row);
1366
+ const keys = /* @__PURE__ */ new Set([peerMentionKey(peerMentionSlug(row.title, row.id)), peerMentionKey(row.id)]);
1367
+ if (row.agent) keys.add(peerMentionKey(peerMentionSlug(row.agent, row.id)));
1368
+ for (const key of keys) add(key, row);
748
1369
  }
749
1370
  const resolved = [];
750
1371
  const seen = /* @__PURE__ */ new Set();
@@ -760,7 +1381,7 @@ function resolvePeerMentions(rows, text) {
760
1381
  resolved.push({
761
1382
  typed: token.body,
762
1383
  id: target.id,
763
- name: target.title,
1384
+ name: target.agent ?? target.title,
764
1385
  engine: target.engine,
765
1386
  status: target.status,
766
1387
  cwd: target.cwd,
@@ -2819,110 +3440,6 @@ async function handleWrite({ ctx, hostFiles, req, res }) {
2819
3440
  });
2820
3441
  }
2821
3442
  //#endregion
2822
- //#region src/lib/host-only-keys.ts
2823
- const HOST_ONLY_KEY_TABLE = {
2824
- epoch: "durable",
2825
- pricing: "transient",
2826
- queryFn: "transient",
2827
- env: "transient",
2828
- pathToClaudeCodeExecutable: "durable",
2829
- extraOptions: "transient",
2830
- instructions: "transient",
2831
- defaultApprovalTimeoutMs: "durable",
2832
- backfillHistory: "durable",
2833
- historyFn: "transient",
2834
- sessionInfoFn: "transient",
2835
- peers: "transient",
2836
- contextReset: "transient",
2837
- shells: "transient",
2838
- shellAgentWrite: "transient",
2839
- createdByOperator: "durable",
2840
- fallbackTitle: "transient",
2841
- startAsleep: "transient",
2842
- effortDefaults: "transient",
2843
- connectFn: "transient",
2844
- codexHome: "durable",
2845
- codexPathOverride: "durable",
2846
- languageModel: "transient",
2847
- tools: "transient",
2848
- maxSteps: "durable",
2849
- executor: "transient",
2850
- executableTools: "transient",
2851
- vfs: "transient",
2852
- executionLimits: "durable",
2853
- executionBackend: "transient",
2854
- toolTitles: "transient",
2855
- shouldApprove: "transient",
2856
- resolveModel: "transient",
2857
- contextWindow: "transient",
2858
- reportMcpServers: "transient",
2859
- onClose: "transient",
2860
- restore: "transient"
2861
- };
2862
- const HOST_ONLY_KEYS = new Set(Object.keys(HOST_ONLY_KEY_TABLE));
2863
- const DURABLE_HOST_KEYS = Object.entries(HOST_ONLY_KEY_TABLE).filter(([, lifetime]) => lifetime === "durable").map(([key]) => key);
2864
- //#endregion
2865
- //#region src/routes/create-vet.ts
2866
- function vetCreateRequest(ctx, body, auth) {
2867
- if (typeof body !== "object" || body === null || Array.isArray(body)) return {
2868
- ok: false,
2869
- status: 400,
2870
- error: "request body must be a JSON object"
2871
- };
2872
- const hostOnly = Object.keys(body).filter((key) => HOST_ONLY_KEYS.has(key));
2873
- if (hostOnly.length > 0) return {
2874
- ok: false,
2875
- status: 400,
2876
- error: `host-only runner config is not accepted on a session request: ${hostOnly.join(", ")} (set it from the gateway's buildRunnerConfig hook)`
2877
- };
2878
- const req = pickCreateSessionRequest(body);
2879
- const { availability, factory } = ctx;
2880
- const early = factory.applyScope(req, auth) ?? refusal(403, factory.applyBypassPolicy(req));
2881
- if (early) return {
2882
- ok: false,
2883
- ...early
2884
- };
2885
- const resolved = factory.resolveProfile(req.profile, auth.allowedProfiles);
2886
- if (!resolved.ok) return resolved;
2887
- const refused = refusal(403, refuseHostAuthority(ctx, req, resolved.profile, auth)) ?? availability.checkAvailable(resolved.profile) ?? factory.checkCwd(req, resolved.profile) ?? refusal(400, factory.checkPermissionMode(req.permissionMode, resolved.profile) ?? factory.checkEngineGrants(req, resolved.profile));
2888
- if (refused) return {
2889
- ok: false,
2890
- ...refused
2891
- };
2892
- factory.stripInertFields(req, resolved.profile);
2893
- req.profile = resolved.profile?.name;
2894
- return {
2895
- ok: true,
2896
- request: req
2897
- };
2898
- }
2899
- function refusal(status, error) {
2900
- return error === null ? null : {
2901
- status,
2902
- error
2903
- };
2904
- }
2905
- function refuseHostAuthority(ctx, req, profile, auth) {
2906
- if (ctx.auth.isOperator(auth)) return null;
2907
- const engine = engineOf(profile);
2908
- const name = profile?.name ?? "default";
2909
- if ((engine === "claude" || engine === "codex") && !auth.allowedProfiles?.includes(name)) return `profile '${name}' runs the ${engine} engine on the host; a non-operator caller needs it named in its allowedProfiles`;
2910
- const mode = refusePermissionMode(req.permissionMode, { operator: false });
2911
- if (mode) return mode;
2912
- if (req.allowDangerouslySkipPermissions) return "allowDangerouslySkipPermissions is reserved to operators";
2913
- if (req.settingSources !== void 0 && req.settingSources.length > 0) return "settingSources is reserved to operators: it loads hooks and settings from the host";
2914
- if (!(typeof req.mcpServers === "object" && req.mcpServers !== null ? Object.values(req.mcpServers) : []).every(isRemoteMcpServer)) return "a stdio MCP server is reserved to operators: it runs a command on the host";
2915
- if (req.resume !== void 0 && !ownsSdkSession(ctx, auth, req.resume)) return "resume is limited to a live session this caller can see";
2916
- return null;
2917
- }
2918
- function ownsSdkSession(ctx, auth, sdkSessionId) {
2919
- return ctx.registry.list().some((info) => info.sdkSessionId === sdkSessionId && ctx.auth.canSee(auth, info));
2920
- }
2921
- function isRemoteMcpServer(server) {
2922
- const type = server?.type;
2923
- return type === "http" || type === "sse";
2924
- }
2925
- //#endregion
2926
3443
  //#region src/routes/jobs.ts
2927
3444
  async function handleJobs(ctx, req, res, pathname, auth) {
2928
3445
  const { auth: authSvc, basePath, queue } = ctx;
@@ -3643,8 +4160,16 @@ async function handleSession({ ctx, req, res, route, runner, parked, info }) {
3643
4160
  const body = await readJsonBody(req, ctx.maxBodyBytes);
3644
4161
  if (body?.title !== void 0 && body.title !== null && typeof body.title !== "string") fail(400, "title must be a string or null");
3645
4162
  const title = typeof body?.title === "string" ? body.title.trim() || void 0 : void 0;
4163
+ const label = body?.statusLabel === void 0 ? void 0 : readStatusLabelInput(body.statusLabel);
4164
+ if (typeof label === "string") fail(400, label);
3646
4165
  if (!runner && parked) {
3647
- const renamed = body?.title === void 0 ? parked.info : await parking.retitle(route.id, title);
4166
+ let renamed = parked.info;
4167
+ if (body?.title !== void 0) renamed = await parking.retitle(route.id, title);
4168
+ if (label !== void 0 && renamed) {
4169
+ const relabeled = await parking.relabel(route.id, label);
4170
+ if (relabeled === "parked") fail(409, "session is parked mid-turn; set its status once it wakes");
4171
+ renamed = relabeled;
4172
+ }
3648
4173
  json(res, 200, { session: projects.withProject(requireRenamed(renamed)) });
3649
4174
  return;
3650
4175
  }
@@ -3653,6 +4178,10 @@ async function handleSession({ ctx, req, res, route, runner, parked, info }) {
3653
4178
  live.setTitle(title);
3654
4179
  parking.touch(live);
3655
4180
  }
4181
+ if (label !== void 0) {
4182
+ if (!live.setStatusLabel) fail(501, "this engine does not carry a status label");
4183
+ live.setStatusLabel(label);
4184
+ }
3656
4185
  json(res, 200, { session: projects.withProject(live.info()) });
3657
4186
  return;
3658
4187
  }
@@ -3757,6 +4286,11 @@ function httpRoutes(ctx) {
3757
4286
  auth: "any",
3758
4287
  handler: (req, res, pathname, auth) => handleProfiles(ctx, req, res, pathname, auth)
3759
4288
  }),
4289
+ route({
4290
+ match: pathWhere(under("/agents")),
4291
+ auth: "operator",
4292
+ handler: (req, res, pathname, auth) => handleAgents(ctx, req, res, pathname, auth)
4293
+ }),
3760
4294
  route({
3761
4295
  match: pathWhere((pathname) => pathname.startsWith(base + "/executions/")),
3762
4296
  auth: "any",
@@ -3824,6 +4358,36 @@ function pathWhere(test) {
3824
4358
  return (pathname) => test(pathname) ? pathname : void 0;
3825
4359
  }
3826
4360
  //#endregion
4361
+ //#region src/services/agent-store.ts
4362
+ function createMemoryAgentStore(seed = []) {
4363
+ const agents = new Map(seed.map((a) => [a.id, a]));
4364
+ return {
4365
+ list: () => [...agents.values()],
4366
+ save: (agent) => void agents.set(agent.id, agent),
4367
+ delete: (id) => void agents.delete(id)
4368
+ };
4369
+ }
4370
+ function createFileAgentStore(path = join(process.cwd(), ".workerdeck", "agents.json")) {
4371
+ const read = () => {
4372
+ const parsed = readJsonOrSync(path, []);
4373
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Map();
4374
+ return new Map(parsed.filter((a) => a && typeof a.id === "string" && typeof a.name === "string").map((a) => [a.id, a]));
4375
+ };
4376
+ const write = (agents) => writeJsonAtomicSync(path, [...agents.values()], { indent: 2 });
4377
+ return {
4378
+ list: () => [...read().values()],
4379
+ save: (agent) => {
4380
+ const agents = read();
4381
+ agents.set(agent.id, agent);
4382
+ write(agents);
4383
+ },
4384
+ delete: (id) => {
4385
+ const agents = read();
4386
+ if (agents.delete(id)) write(agents);
4387
+ }
4388
+ };
4389
+ }
4390
+ //#endregion
3827
4391
  //#region src/services/attachments.ts
3828
4392
  const DEFAULT_MAX_FILE_BYTES = 10485760;
3829
4393
  const DEFAULT_MAX_SESSION_BYTES = 67108864;
@@ -4044,7 +4608,7 @@ var EngineSleepTimers = class {
4044
4608
  return this.#options.afterMs > 0 && !this.#closed;
4045
4609
  }
4046
4610
  watch(runner) {
4047
- if (!this.enabled || !runner.sleep) return;
4611
+ if (this.#closed || !runner.sleep || !this.enabled && !this.#options.afterMsFor) return;
4048
4612
  this.#runners.set(runner.id, runner);
4049
4613
  const unsubscribe = runner.subscribe((event) => {
4050
4614
  if (event.type === "status_changed") this.#rearm(runner);
@@ -4066,14 +4630,18 @@ var EngineSleepTimers = class {
4066
4630
  this.#timers.clear();
4067
4631
  this.#runners.clear();
4068
4632
  }
4633
+ #afterMs(sessionId) {
4634
+ return this.#closed ? 0 : this.#options.afterMsFor?.(sessionId) ?? this.#options.afterMs;
4635
+ }
4069
4636
  #rearm(runner) {
4070
4637
  this.#disarm(runner.id);
4071
- if (!this.enabled || !this.#sleepable(runner)) return;
4638
+ const afterMs = this.#afterMs(runner.id);
4639
+ if (afterMs <= 0 || !this.#sleepable(runner)) return;
4072
4640
  const timer = setTimeout(() => {
4073
4641
  this.#timers.delete(runner.id);
4074
- if (!this.enabled || !this.#sleepable(runner)) return;
4642
+ if (this.#afterMs(runner.id) <= 0 || !this.#sleepable(runner)) return;
4075
4643
  runner.sleep?.().catch((error) => this.#options.onError?.(error, runner.id));
4076
- }, this.#options.afterMs);
4644
+ }, afterMs);
4077
4645
  timer.unref?.();
4078
4646
  this.#timers.set(runner.id, timer);
4079
4647
  }
@@ -4437,6 +5005,7 @@ var SessionParkManager = class {
4437
5005
  this.#persistLive(runner);
4438
5006
  return;
4439
5007
  case "system_init":
5008
+ case "status_label":
4440
5009
  this.#rememberDormant(runner);
4441
5010
  return;
4442
5011
  case "conversation_reset":
@@ -4526,6 +5095,30 @@ var SessionParkManager = class {
4526
5095
  return info;
4527
5096
  });
4528
5097
  }
5098
+ relabel(id, input) {
5099
+ return this.#queue(id, async () => {
5100
+ const record = await this.#options.store.get(id);
5101
+ if (!record || this.#options.registry.get(id)) return;
5102
+ if (!isDormant(record)) return "parked";
5103
+ const statusLabel = input ? {
5104
+ ...input,
5105
+ setAt: Date.now()
5106
+ } : void 0;
5107
+ const info = {
5108
+ ...record.info,
5109
+ statusLabel
5110
+ };
5111
+ await this.#options.store.save({
5112
+ ...record,
5113
+ info,
5114
+ config: {
5115
+ ...record.config,
5116
+ statusLabel
5117
+ }
5118
+ });
5119
+ return info;
5120
+ });
5121
+ }
4529
5122
  async listInfo() {
4530
5123
  await Promise.all(this.#storeOps.values());
4531
5124
  return (await this.#options.store.list()).filter((record) => this.#options.registry.get(record.id) === void 0).map((record) => isDormant(record) ? {
@@ -4791,7 +5384,8 @@ function idleRecordBase(runner, config) {
4791
5384
  profile: info.profile,
4792
5385
  config: {
4793
5386
  ...config,
4794
- meta: info.meta
5387
+ meta: info.meta,
5388
+ statusLabel: info.statusLabel
4795
5389
  },
4796
5390
  cost: runner.costState?.()
4797
5391
  };
@@ -5229,7 +5823,7 @@ function createRelayLink(options, peers, log) {
5229
5823
  const remoteTarget = parseRelayPeerId;
5230
5824
  const reachesRemote = async (from) => {
5231
5825
  const me = await peers.relaySender(from);
5232
- return me.scope === void 0 || Object.keys(me.scope).length === 0;
5826
+ return (me.scope === void 0 || Object.keys(me.scope).length === 0) && me.agent?.lead === void 0;
5233
5827
  };
5234
5828
  return {
5235
5829
  directory: {
@@ -5507,7 +6101,7 @@ function createSessionFactory(deps) {
5507
6101
  return adapterFor(profile.engine).sessionEnv?.(profile, process.env) ?? process.env;
5508
6102
  }
5509
6103
  };
5510
- const buildRunner = async (built, restore, id) => {
6104
+ const buildRunner = async (built, restore, id, options) => {
5511
6105
  const name = built.profile;
5512
6106
  const profile = name !== void 0 ? profiles.get(name) : void 0;
5513
6107
  if (name !== void 0 && !profile) throw new Error(`unknown profile: ${name}`);
@@ -5518,6 +6112,7 @@ function createSessionFactory(deps) {
5518
6112
  ...deps.pricing ? { pricing: deps.pricing } : {}
5519
6113
  };
5520
6114
  delete config.contextReset;
6115
+ delete config.avatar;
5521
6116
  delete config.shells;
5522
6117
  delete config.shellAgentWrite;
5523
6118
  delete config.effortDefaults;
@@ -5532,6 +6127,9 @@ function createSessionFactory(deps) {
5532
6127
  const write = deps.shellAgentWrite ?? "read-only";
5533
6128
  if (write !== "read-only" && built.createdByOperator === true) config.shellAgentWrite = write;
5534
6129
  }
6130
+ if (deps.avatar && (options?.agent === true || deps.avatar.isAgent(id ?? restore?.id))) config.avatar = deps.avatar.directory;
6131
+ const brief = options?.brief ?? deps.agentBrief?.(id ?? restore?.id);
6132
+ if (brief) config.instructions = composeInstructions(config.instructions, brief);
5535
6133
  if (config.instructions !== void 0 && capabilities.systemInstructions === false) throw new Error(`the ${engineOf(profile)} engine cannot deliver system instructions`);
5536
6134
  const runner = profile && isProviderProfile(profile) ? await deps.createEngineRunner({
5537
6135
  config,
@@ -5549,8 +6147,8 @@ function createSessionFactory(deps) {
5549
6147
  if (!sameScope(reported, config.scope)) throw new Error(`runner for session ${runner.id} reports scope ${JSON.stringify(reported)}, expected ${JSON.stringify(config.scope)} - echo config.scope from info()`);
5550
6148
  return runner;
5551
6149
  };
5552
- const createRunner = async (config) => {
5553
- const runner = registry.register(await buildRunner(config));
6150
+ const createRunner = async (config, options) => {
6151
+ const runner = registry.register(await buildRunner(config, void 0, void 0, options));
5554
6152
  parking.remember(runner.id, config);
5555
6153
  parking.watch(runner);
5556
6154
  runner.start();
@@ -5669,7 +6267,13 @@ function createWorkerServer(options = {}) {
5669
6267
  }
5670
6268
  const generation = randomUUID();
5671
6269
  const shells = options.shell?.enabled === true ? shellRegistryFor(options.shell, generation) : null;
5672
- const projects = new ProjectInfoService({ decorate: (info) => shells ? shells.decorate(info) : info });
6270
+ const agents = new AgentService({
6271
+ store: options.agentStore ?? createMemoryAgentStore(),
6272
+ basePath,
6273
+ avatars: options.avatars !== void 0,
6274
+ sleepAfterMs: options.agentSleepAfterMs
6275
+ });
6276
+ const projects = new ProjectInfoService({ decorate: (info) => agents.decorate(shells ? shells.decorate(info) : info) });
5673
6277
  const notifier = new SessionNotifier({
5674
6278
  ...options.notifications,
5675
6279
  decorateInfo: (info) => projects.withProject(info),
@@ -5678,6 +6282,7 @@ function createWorkerServer(options = {}) {
5678
6282
  const producedFiles = new ProducedFileStore();
5679
6283
  const engineSleep = new EngineSleepTimers({
5680
6284
  afterMs: options.engineSleepAfterMs ?? 0,
6285
+ afterMsFor: (sessionId) => agents.sleepAfterFor(sessionId),
5681
6286
  attachedCount: (sessionId) => bridge.attachedCount(sessionId),
5682
6287
  onError: (error) => diagnose(error, "engine-sleep")
5683
6288
  });
@@ -5687,6 +6292,14 @@ function createWorkerServer(options = {}) {
5687
6292
  });
5688
6293
  let ownContextResets = contextResets;
5689
6294
  installContextResetDirectory(contextResets);
6295
+ const avatarChanges = options.avatars ? { change: async (sessionId, seed) => {
6296
+ const agent = agents.bySession(sessionId);
6297
+ if (!agent) throw new Error("only an agent has an avatar, and this session is not one");
6298
+ await rerollAvatar(ctx, agent, seed);
6299
+ return seed ? `Your avatar is changed (seed "${seed}"); the human sees it beside your name now.` : "Your avatar is changed; the human sees it beside your name now.";
6300
+ } } : void 0;
6301
+ let ownAvatarChanges = avatarChanges;
6302
+ installAvatarDirectory(avatarChanges);
5690
6303
  const registry = new SessionRegistry({ onRegister: (runner) => {
5691
6304
  const detachers = [
5692
6305
  notifier.watch(runner),
@@ -5764,7 +6377,12 @@ function createWorkerServer(options = {}) {
5764
6377
  pricing: pricing.pricing,
5765
6378
  registry,
5766
6379
  parking,
5767
- bridge
6380
+ bridge,
6381
+ agentBrief: (sessionId) => agents.briefFor(sessionId),
6382
+ avatar: avatarChanges ? {
6383
+ directory: avatarDirectoryHandle(() => ownAvatarChanges),
6384
+ isAgent: (sessionId) => sessionId !== void 0 && agents.bySession(sessionId) !== void 0
6385
+ } : void 0
5768
6386
  });
5769
6387
  const availability = new AvailabilityTracker({
5770
6388
  checkCredentials: options.checkCredentials,
@@ -5812,6 +6430,8 @@ function createWorkerServer(options = {}) {
5812
6430
  availability,
5813
6431
  auth,
5814
6432
  factory,
6433
+ agents,
6434
+ avatars: options.avatars,
5815
6435
  registry,
5816
6436
  parking,
5817
6437
  engineSleep,
@@ -5871,6 +6491,7 @@ function createWorkerServer(options = {}) {
5871
6491
  ownShells = void 0;
5872
6492
  contextResets?.close();
5873
6493
  ownContextResets = void 0;
6494
+ ownAvatarChanges = void 0;
5874
6495
  }
5875
6496
  });
5876
6497
  return {
@@ -5883,6 +6504,7 @@ function createWorkerServer(options = {}) {
5883
6504
  listen: async (port, host) => {
5884
6505
  await profiles.refreshStored();
5885
6506
  await profiles.seedStore();
6507
+ await agents.hydrate();
5886
6508
  await parking.hydrate();
5887
6509
  await shells?.hydrate();
5888
6510
  return new Promise((resolve, reject) => {
@@ -6078,6 +6700,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
6078
6700
  };
6079
6701
  }
6080
6702
  //#endregion
6081
- export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, isMissing, readJsonOr, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord, writeFileAtomic, writeJsonAtomic };
6703
+ export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileAgentStore, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryAgentStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, isMissing, readJsonOr, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord, writeFileAtomic, writeJsonAtomic };
6082
6704
 
6083
6705
  //# sourceMappingURL=index.mjs.map