@workerdeck/server 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.mts +40 -2
- package/build/index.mjs +1268 -459
- package/build/index.mjs.map +1 -1
- package/package.json +7 -7
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, 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, composeInstructions, 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
|
-
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, 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";
|
|
@@ -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,
|
|
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,307 @@ 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) && teamReaches(from.agent, to.agent);
|
|
524
|
+
}
|
|
525
|
+
function relayable(info, exposed) {
|
|
526
|
+
return info.status !== "closed" && info.agent?.lead === void 0 && scopeMatches(exposed, info.scope);
|
|
527
|
+
}
|
|
528
|
+
function relayEntry(info, live) {
|
|
529
|
+
const items = info.checklist;
|
|
530
|
+
return {
|
|
531
|
+
id: info.id,
|
|
532
|
+
engine: info.engine,
|
|
533
|
+
status: info.status,
|
|
534
|
+
title: info.title,
|
|
535
|
+
project: info.project ? {
|
|
536
|
+
name: info.project.name,
|
|
537
|
+
root: info.project.root
|
|
538
|
+
} : void 0,
|
|
539
|
+
cwd: info.cwd,
|
|
540
|
+
profile: info.profile,
|
|
541
|
+
model: info.model,
|
|
542
|
+
permissionMode: info.permissionMode,
|
|
543
|
+
contextUsage: info.contextUsage,
|
|
544
|
+
checklist: items?.length ? {
|
|
545
|
+
done: items.filter((item) => item.status === "completed").length,
|
|
546
|
+
total: items.length
|
|
547
|
+
} : void 0,
|
|
548
|
+
numTurns: info.numTurns,
|
|
549
|
+
createdAt: info.createdAt,
|
|
550
|
+
lastActivityAt: info.lastActivityAt,
|
|
551
|
+
pendingPermissionCount: info.pendingPermissionCount,
|
|
552
|
+
live
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function createPeerService(deps) {
|
|
556
|
+
const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
|
|
557
|
+
const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
|
|
558
|
+
const maxHops = deps.options?.maxHops ?? DEFAULT_MAX_HOPS;
|
|
559
|
+
const sent = /* @__PURE__ */ new Map();
|
|
560
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
561
|
+
const registry = () => {
|
|
562
|
+
const found = deps.refs.registry;
|
|
563
|
+
if (!found) throw new Error("peer messaging is not ready");
|
|
564
|
+
return found;
|
|
565
|
+
};
|
|
566
|
+
const allSessions = async () => {
|
|
567
|
+
const live = registry().list();
|
|
568
|
+
const dormant = await deps.refs.parking?.listInfo() ?? [];
|
|
569
|
+
return [...live, ...dormant].map((info) => deps.projects.withProject(info));
|
|
570
|
+
};
|
|
571
|
+
const infoOf = async (id) => {
|
|
572
|
+
const live = registry().get(id)?.info();
|
|
573
|
+
if (live) return deps.projects.withProject(live);
|
|
574
|
+
const record = await deps.refs.parking?.get(id);
|
|
575
|
+
return record ? deps.projects.withProject(record.info) : void 0;
|
|
576
|
+
};
|
|
577
|
+
const sender = async (from) => {
|
|
578
|
+
const info = await infoOf(from);
|
|
579
|
+
if (!info) throw new Error(`unknown sender session: ${from}`);
|
|
580
|
+
return info;
|
|
581
|
+
};
|
|
582
|
+
const recentOf = (sessionId, recent) => {
|
|
583
|
+
const limit = Math.min(Math.max(recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
|
|
584
|
+
const runner = registry().get(sessionId);
|
|
585
|
+
const events = [];
|
|
586
|
+
if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
|
|
587
|
+
truncateResults: true,
|
|
588
|
+
imageRefs: true
|
|
589
|
+
})();
|
|
590
|
+
return {
|
|
591
|
+
runner,
|
|
592
|
+
lines: recentLines(events, limit)
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
const underRateLimit = (from, to) => {
|
|
596
|
+
const key = `${from}->${to}`;
|
|
597
|
+
const now = Date.now();
|
|
598
|
+
const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
|
|
599
|
+
if (recent.length >= perMinute) {
|
|
600
|
+
sent.set(key, recent);
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
recent.push(now);
|
|
604
|
+
sent.set(key, recent);
|
|
605
|
+
return true;
|
|
606
|
+
};
|
|
607
|
+
const list = async (from) => {
|
|
608
|
+
const me = await sender(from);
|
|
609
|
+
return (await allSessions()).filter((info) => visible(me, info)).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map(peerSummary);
|
|
610
|
+
};
|
|
611
|
+
const peek = async (from, sessionId, options) => {
|
|
612
|
+
const me = await sender(from);
|
|
613
|
+
const info = await infoOf(sessionId);
|
|
614
|
+
if (!info || !visible(me, info)) return;
|
|
615
|
+
const { runner, lines } = recentOf(sessionId, options?.recent);
|
|
616
|
+
return {
|
|
617
|
+
...peerSummary(info),
|
|
618
|
+
live: runner !== void 0,
|
|
619
|
+
checklist: info.checklist,
|
|
620
|
+
pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
|
|
621
|
+
recent: lines
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
const send = async (from, sessionId, text, options) => {
|
|
625
|
+
const me = await sender(from);
|
|
626
|
+
if (sessionId === from) return {
|
|
627
|
+
delivered: false,
|
|
628
|
+
reason: "that is this session"
|
|
629
|
+
};
|
|
630
|
+
if (text.length > maxChars) return {
|
|
631
|
+
delivered: false,
|
|
632
|
+
reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
|
|
633
|
+
};
|
|
634
|
+
const target = await infoOf(sessionId);
|
|
635
|
+
if (!target || !visible(me, target)) return {
|
|
636
|
+
delivered: false,
|
|
637
|
+
reason: `no such session: ${sessionId}`
|
|
638
|
+
};
|
|
639
|
+
const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
|
|
640
|
+
return deliver(target, text, {
|
|
641
|
+
kind: "peer",
|
|
642
|
+
sessionId: from,
|
|
643
|
+
name: me.title,
|
|
644
|
+
engine: me.engine,
|
|
645
|
+
hops
|
|
646
|
+
}, from);
|
|
647
|
+
};
|
|
648
|
+
const deliver = async (target, text, origin, rateKey) => {
|
|
649
|
+
const sessionId = target.id;
|
|
650
|
+
if (target.status === "closed" || target.status === "failed") return {
|
|
651
|
+
delivered: false,
|
|
652
|
+
reason: `session ${sessionId} is ${target.status}`
|
|
653
|
+
};
|
|
654
|
+
const hops = origin.hops ?? [];
|
|
655
|
+
if (hops.length > maxHops) return {
|
|
656
|
+
delivered: false,
|
|
657
|
+
reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
|
|
658
|
+
};
|
|
659
|
+
if (!underRateLimit(rateKey, sessionId)) return {
|
|
660
|
+
delivered: false,
|
|
661
|
+
reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
|
|
662
|
+
};
|
|
663
|
+
const runner = await deps.refs.parking?.ensureLive(sessionId) ?? registry().get(sessionId);
|
|
664
|
+
if (!runner) return {
|
|
665
|
+
delivered: false,
|
|
666
|
+
reason: `session ${sessionId} could not be woken`
|
|
667
|
+
};
|
|
668
|
+
const before = runner.info().status;
|
|
669
|
+
try {
|
|
670
|
+
runner.sendMessage(text, void 0, { origin });
|
|
671
|
+
} catch (error) {
|
|
672
|
+
return {
|
|
673
|
+
delivered: false,
|
|
674
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
inbound.set(sessionId, hops);
|
|
678
|
+
return {
|
|
679
|
+
delivered: true,
|
|
680
|
+
sessionId,
|
|
681
|
+
name: target.title,
|
|
682
|
+
queued: before === "running" || before === "awaiting_approval"
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
const exposedInfo = async (sessionId, exposed) => {
|
|
686
|
+
const info = await infoOf(sessionId);
|
|
687
|
+
return info && relayable(info, exposed) ? info : void 0;
|
|
688
|
+
};
|
|
689
|
+
const relayEntries = async (exposed) => {
|
|
690
|
+
const live = new Set(registry().list().map((info) => info.id));
|
|
691
|
+
return (await allSessions()).filter((info) => relayable(info, exposed)).map((info) => relayEntry(info, live.has(info.id)));
|
|
692
|
+
};
|
|
693
|
+
const relayPeek = async (sessionId, recent, exposed) => {
|
|
694
|
+
const info = await exposedInfo(sessionId, exposed);
|
|
695
|
+
if (!info) return;
|
|
696
|
+
const { runner, lines } = recentOf(sessionId, recent);
|
|
697
|
+
return {
|
|
698
|
+
...relayEntry(info, runner !== void 0),
|
|
699
|
+
checklistItems: info.checklist,
|
|
700
|
+
pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
|
|
701
|
+
recent: lines
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
const relaySend = async (origin, sessionId, text, exposed) => {
|
|
705
|
+
if (text.length === 0 || text.length > maxChars) return {
|
|
706
|
+
delivered: false,
|
|
707
|
+
reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
|
|
708
|
+
};
|
|
709
|
+
const target = await exposedInfo(sessionId, exposed);
|
|
710
|
+
if (!target) return {
|
|
711
|
+
delivered: false,
|
|
712
|
+
reason: `no such session: ${sessionId}`
|
|
713
|
+
};
|
|
714
|
+
const from = `${origin.gateway}:${origin.sessionId}`;
|
|
715
|
+
return deliver(target, text, {
|
|
716
|
+
kind: "peer",
|
|
717
|
+
sessionId: from,
|
|
718
|
+
hostId: origin.gateway,
|
|
719
|
+
name: origin.name,
|
|
720
|
+
engine: origin.engine,
|
|
721
|
+
hops: origin.hops
|
|
722
|
+
}, from);
|
|
723
|
+
};
|
|
724
|
+
const watch = (runner) => runner.subscribe((event) => {
|
|
725
|
+
if (event.type === "user_message" && !event.origin && !event.synthetic && event.parentToolUseId == null && !event.replay) inbound.delete(runner.id);
|
|
726
|
+
}, runner.info().lastSeq);
|
|
727
|
+
return {
|
|
728
|
+
list,
|
|
729
|
+
peek,
|
|
730
|
+
send,
|
|
731
|
+
watch,
|
|
732
|
+
relaySender: sender,
|
|
733
|
+
relayChain: (from) => [...inbound.get(from) ?? [], from],
|
|
734
|
+
relayEntries,
|
|
735
|
+
relayPeek,
|
|
736
|
+
relaySend
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function resolvePeerMentions(rows, text) {
|
|
740
|
+
const tokens = scanPeerMentions(text);
|
|
741
|
+
if (tokens.length === 0) return [];
|
|
742
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
743
|
+
const add = (key, row) => {
|
|
744
|
+
const held = byKey.get(key);
|
|
745
|
+
if (held) held.push(row);
|
|
746
|
+
else byKey.set(key, [row]);
|
|
747
|
+
};
|
|
748
|
+
for (const row of rows) {
|
|
749
|
+
const keys = /* @__PURE__ */ new Set([peerMentionKey(peerMentionSlug(row.title, row.id)), peerMentionKey(row.id)]);
|
|
750
|
+
if (row.agent) keys.add(peerMentionKey(peerMentionSlug(row.agent, row.id)));
|
|
751
|
+
for (const key of keys) add(key, row);
|
|
752
|
+
}
|
|
753
|
+
const resolved = [];
|
|
754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
755
|
+
for (const token of tokens) {
|
|
756
|
+
const key = peerMentionKey(token.body);
|
|
757
|
+
const exact = byKey.get(key);
|
|
758
|
+
const matches = exact ?? (key.length >= 4 ? rows.filter((row) => row.id.startsWith(key)) : []);
|
|
759
|
+
const target = matches[0];
|
|
760
|
+
if (!target || matches.length > 1 && !exact) continue;
|
|
761
|
+
if (seen.has(target.id)) continue;
|
|
762
|
+
seen.add(target.id);
|
|
763
|
+
const others = matches.slice(1, 4).map((row) => row.id);
|
|
764
|
+
resolved.push({
|
|
765
|
+
typed: token.body,
|
|
766
|
+
id: target.id,
|
|
767
|
+
name: target.agent ?? target.title,
|
|
768
|
+
engine: target.engine,
|
|
769
|
+
status: target.status,
|
|
770
|
+
cwd: target.cwd,
|
|
771
|
+
...others.length ? { ambiguousWith: others } : {}
|
|
772
|
+
});
|
|
773
|
+
if (resolved.length >= PEER_MENTION_MAX) break;
|
|
774
|
+
}
|
|
775
|
+
return resolved;
|
|
776
|
+
}
|
|
777
|
+
async function mentionsFor(directory, from, text) {
|
|
778
|
+
return scanPeerMentions(text).length === 0 ? [] : resolvePeerMentions(await directory.list(from), text);
|
|
779
|
+
}
|
|
780
|
+
//#endregion
|
|
781
|
+
//#region src/routes/sleep.ts
|
|
782
|
+
async function sleepRunner(runner) {
|
|
783
|
+
if (!runner.sleep) throw new HttpError(501, `the ${engineOf(runner.info())} engine has no process to put to sleep`);
|
|
784
|
+
const result = await runner.sleep();
|
|
785
|
+
if (!result.ok) throw new HttpError(409, `cannot sleep: ${result.reason}`);
|
|
786
|
+
}
|
|
787
|
+
//#endregion
|
|
467
788
|
//#region src/services/shell-directory.ts
|
|
468
789
|
function createShellDirectory(registry, deps = {}) {
|
|
469
790
|
const take = async (from, shellId, options) => {
|
|
@@ -1853,13 +2174,14 @@ function attachClient(ctx, ws, runner, req, access) {
|
|
|
1853
2174
|
unsubscribe();
|
|
1854
2175
|
detachBridge();
|
|
1855
2176
|
parking.onDetach(runner.id);
|
|
2177
|
+
ctx.engineSleep.onDetach(runner.id);
|
|
1856
2178
|
});
|
|
1857
2179
|
}
|
|
1858
2180
|
async function handleCommand(ctx, frame, runner, access, client) {
|
|
1859
2181
|
const { attachmentStore, bridge } = ctx;
|
|
1860
2182
|
switch (frame.type) {
|
|
1861
2183
|
case "user_message": {
|
|
1862
|
-
const mentions = ctx.peers && !isSlashCommand(frame.text) ? await ctx.peers
|
|
2184
|
+
const mentions = ctx.peers && !isSlashCommand(frame.text) ? await mentionsFor(ctx.peers, runner.id, frame.text).catch(() => []) : [];
|
|
1863
2185
|
const options = mentions.length > 0 ? { mentions } : void 0;
|
|
1864
2186
|
if (!frame.attachmentIds?.length) {
|
|
1865
2187
|
runner.sendMessage(frame.text, void 0, options);
|
|
@@ -1876,6 +2198,13 @@ async function handleCommand(ctx, frame, runner, access, client) {
|
|
|
1876
2198
|
case "interrupt":
|
|
1877
2199
|
await runner.interrupt();
|
|
1878
2200
|
return;
|
|
2201
|
+
case "sleep":
|
|
2202
|
+
await sleepRunner(runner);
|
|
2203
|
+
return;
|
|
2204
|
+
case "background_task":
|
|
2205
|
+
if (!runner.backgroundTask) throw new Error(`the ${engineOf(runner.info())} engine cannot move a task to the background`);
|
|
2206
|
+
await runner.backgroundTask(frame.toolUseId);
|
|
2207
|
+
return;
|
|
1879
2208
|
case "clear_context":
|
|
1880
2209
|
if (!runner.clearContext) throw new Error(`the ${engineOf(runner.info())} engine cannot clear a conversation`);
|
|
1881
2210
|
await runner.clearContext();
|
|
@@ -1892,6 +2221,11 @@ async function handleCommand(ctx, frame, runner, access, client) {
|
|
|
1892
2221
|
case "set_model":
|
|
1893
2222
|
await runner.setModel(frame.model);
|
|
1894
2223
|
return;
|
|
2224
|
+
case "set_effort":
|
|
2225
|
+
if (!runner.setEffort) throw new Error(`the ${engineOf(runner.info())} engine cannot change its reasoning effort`);
|
|
2226
|
+
if (frame.effort !== void 0 && typeof frame.effort !== "string") throw new Error("effort must be a string");
|
|
2227
|
+
await runner.setEffort(frame.effort);
|
|
2228
|
+
return;
|
|
1895
2229
|
case "tool_call_result":
|
|
1896
2230
|
bridge.resolve(runner.id, frame.executionId, {
|
|
1897
2231
|
output: frame.output,
|
|
@@ -1993,53 +2327,623 @@ async function attachShell(shells, client, sessionId, shellId, size) {
|
|
|
1993
2327
|
scrollback
|
|
1994
2328
|
});
|
|
1995
2329
|
}
|
|
1996
|
-
function detachShell(client, shellId) {
|
|
1997
|
-
client.shells.get(shellId)?.();
|
|
1998
|
-
client.shells.delete(shellId);
|
|
1999
|
-
}
|
|
2000
|
-
function detachShells(client) {
|
|
2001
|
-
for (const detach of client.shells.values()) detach();
|
|
2002
|
-
client.shells.clear();
|
|
2003
|
-
}
|
|
2004
|
-
//#endregion
|
|
2005
|
-
//#region src/routes/session-upgrade.ts
|
|
2006
|
-
async function upgradeSession(ctx, wss, req, socket, head) {
|
|
2007
|
-
const { auth, parking } = ctx;
|
|
2008
|
-
const route = parseSessionRoute(ctx.basePath, req.url ?? "/");
|
|
2009
|
-
if (route?.kind !== "ws") {
|
|
2010
|
-
socket.destroy();
|
|
2330
|
+
function detachShell(client, shellId) {
|
|
2331
|
+
client.shells.get(shellId)?.();
|
|
2332
|
+
client.shells.delete(shellId);
|
|
2333
|
+
}
|
|
2334
|
+
function detachShells(client) {
|
|
2335
|
+
for (const detach of client.shells.values()) detach();
|
|
2336
|
+
client.shells.clear();
|
|
2337
|
+
}
|
|
2338
|
+
//#endregion
|
|
2339
|
+
//#region src/routes/session-upgrade.ts
|
|
2340
|
+
async function upgradeSession(ctx, wss, req, socket, head) {
|
|
2341
|
+
const { auth, parking } = ctx;
|
|
2342
|
+
const route = parseSessionRoute(ctx.basePath, req.url ?? "/");
|
|
2343
|
+
if (route?.kind !== "ws") {
|
|
2344
|
+
socket.destroy();
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const authCtx = await auth.authenticate(req);
|
|
2348
|
+
if (!authCtx.ok) {
|
|
2349
|
+
refuseUpgrade(socket, "401 Unauthorized");
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const known = await sessionInfoOf(ctx, route.id);
|
|
2353
|
+
if (known && !auth.canSee(authCtx, known)) {
|
|
2354
|
+
refuseUpgrade(socket, "404 Not Found");
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
const runner = await parking.ensureLive(route.id).catch(() => void 0);
|
|
2358
|
+
if (!runner || !auth.canSee(authCtx, runner.info())) {
|
|
2359
|
+
refuseUpgrade(socket, "404 Not Found");
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2363
|
+
attachClient(ctx, ws, runner, req, { operator: auth.isOperator(authCtx) });
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
//#endregion
|
|
2367
|
+
//#region src/lib/machine-id.ts
|
|
2368
|
+
let cached;
|
|
2369
|
+
function machineId() {
|
|
2370
|
+
cached ??= createHash("sha256").update([
|
|
2371
|
+
hostname(),
|
|
2372
|
+
platform(),
|
|
2373
|
+
arch(),
|
|
2374
|
+
homedir()
|
|
2375
|
+
].join("\0")).digest("hex").slice(0, 32);
|
|
2376
|
+
return cached;
|
|
2377
|
+
}
|
|
2378
|
+
//#endregion
|
|
2379
|
+
//#region src/services/agents.ts
|
|
2380
|
+
const NAMES = [
|
|
2381
|
+
"Atlas",
|
|
2382
|
+
"Juno",
|
|
2383
|
+
"Pip",
|
|
2384
|
+
"Rook",
|
|
2385
|
+
"Marlow",
|
|
2386
|
+
"Orbit",
|
|
2387
|
+
"Fern",
|
|
2388
|
+
"Quill",
|
|
2389
|
+
"Sable",
|
|
2390
|
+
"Wren",
|
|
2391
|
+
"Ember",
|
|
2392
|
+
"Moss",
|
|
2393
|
+
"Nova",
|
|
2394
|
+
"Otto",
|
|
2395
|
+
"Pike",
|
|
2396
|
+
"Rune",
|
|
2397
|
+
"Sage",
|
|
2398
|
+
"Tansy",
|
|
2399
|
+
"Vale",
|
|
2400
|
+
"Yarrow",
|
|
2401
|
+
"Basil",
|
|
2402
|
+
"Cedar",
|
|
2403
|
+
"Dune",
|
|
2404
|
+
"Flint",
|
|
2405
|
+
"Hazel",
|
|
2406
|
+
"Iris",
|
|
2407
|
+
"Kestrel",
|
|
2408
|
+
"Lark",
|
|
2409
|
+
"Maple",
|
|
2410
|
+
"Nettle",
|
|
2411
|
+
"Onyx",
|
|
2412
|
+
"Piper",
|
|
2413
|
+
"Reed",
|
|
2414
|
+
"Sorrel",
|
|
2415
|
+
"Thistle",
|
|
2416
|
+
"Umber",
|
|
2417
|
+
"Willow",
|
|
2418
|
+
"Zephyr"
|
|
2419
|
+
];
|
|
2420
|
+
const MAX_NAME = 64;
|
|
2421
|
+
const MAX_BRIEF = 16e3;
|
|
2422
|
+
const CONFIG_KEYS = [
|
|
2423
|
+
"cwd",
|
|
2424
|
+
"profile",
|
|
2425
|
+
"model",
|
|
2426
|
+
"reasoningEffort",
|
|
2427
|
+
"permissionMode",
|
|
2428
|
+
"brief",
|
|
2429
|
+
"agentContextReset",
|
|
2430
|
+
"sleepAfterMs"
|
|
2431
|
+
];
|
|
2432
|
+
var AgentService = class {
|
|
2433
|
+
#store;
|
|
2434
|
+
#basePath;
|
|
2435
|
+
#avatars;
|
|
2436
|
+
#sleepAfterMs;
|
|
2437
|
+
#now;
|
|
2438
|
+
#agents = /* @__PURE__ */ new Map();
|
|
2439
|
+
constructor(options) {
|
|
2440
|
+
this.#store = options.store;
|
|
2441
|
+
this.#basePath = options.basePath;
|
|
2442
|
+
this.#avatars = options.avatars ?? false;
|
|
2443
|
+
this.#sleepAfterMs = options.sleepAfterMs ?? AGENT_SLEEP_AFTER_MS_DEFAULT;
|
|
2444
|
+
this.#now = options.now ?? Date.now;
|
|
2445
|
+
}
|
|
2446
|
+
async hydrate() {
|
|
2447
|
+
this.#agents = new Map((await this.#store.list()).map((agent) => [agent.id, agent]));
|
|
2448
|
+
}
|
|
2449
|
+
list() {
|
|
2450
|
+
return [...this.#agents.values()].map((agent) => this.public(agent));
|
|
2451
|
+
}
|
|
2452
|
+
get(id) {
|
|
2453
|
+
return this.#agents.get(id);
|
|
2454
|
+
}
|
|
2455
|
+
bySession(sessionId) {
|
|
2456
|
+
for (const agent of this.#agents.values()) if (agent.sessionId === sessionId) return agent;
|
|
2457
|
+
}
|
|
2458
|
+
decorate(info) {
|
|
2459
|
+
const agent = this.bySession(info.id);
|
|
2460
|
+
if (!agent) return info;
|
|
2461
|
+
const leadName = agent.lead === void 0 ? void 0 : this.#agents.get(agent.lead)?.name;
|
|
2462
|
+
return {
|
|
2463
|
+
...info,
|
|
2464
|
+
agent: agentRef(this.public(agent), {
|
|
2465
|
+
leadName,
|
|
2466
|
+
leads: this.hasMembers(agent.id)
|
|
2467
|
+
})
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
briefFor(sessionId) {
|
|
2471
|
+
return (sessionId === void 0 ? void 0 : this.bySession(sessionId)?.config.brief?.trim()) || void 0;
|
|
2472
|
+
}
|
|
2473
|
+
sleepAfterFor(sessionId) {
|
|
2474
|
+
const agent = this.bySession(sessionId);
|
|
2475
|
+
return agent ? agent.config.sleepAfterMs ?? this.#sleepAfterMs : void 0;
|
|
2476
|
+
}
|
|
2477
|
+
public(agent) {
|
|
2478
|
+
const { avatarSeed: _seed, avatarRecipe: _recipe, avatar: _avatar, ...info } = agent;
|
|
2479
|
+
return this.#avatars ? {
|
|
2480
|
+
...info,
|
|
2481
|
+
avatar: `${this.#basePath}/agents/${agent.id}/avatar.png`
|
|
2482
|
+
} : info;
|
|
2483
|
+
}
|
|
2484
|
+
draft(input) {
|
|
2485
|
+
const config = readConfig(input.config);
|
|
2486
|
+
if ("error" in config) return config;
|
|
2487
|
+
const name = input.name === void 0 ? this.#suggestName() : readName(input.name);
|
|
2488
|
+
if (typeof name !== "string") return name;
|
|
2489
|
+
const id = randomUUID();
|
|
2490
|
+
const at = this.#now();
|
|
2491
|
+
const agent = {
|
|
2492
|
+
id,
|
|
2493
|
+
name,
|
|
2494
|
+
createdAt: at,
|
|
2495
|
+
updatedAt: at,
|
|
2496
|
+
avatarSeed: id,
|
|
2497
|
+
pastSessions: [],
|
|
2498
|
+
config
|
|
2499
|
+
};
|
|
2500
|
+
if (input.lead !== void 0 && input.lead !== null) {
|
|
2501
|
+
const refused = typeof input.lead === "string" ? this.leadRefusal(agent, input.lead) : {
|
|
2502
|
+
status: 400,
|
|
2503
|
+
error: "lead must be an agent id"
|
|
2504
|
+
};
|
|
2505
|
+
if (refused) return refused;
|
|
2506
|
+
agent.lead = input.lead;
|
|
2507
|
+
}
|
|
2508
|
+
return agent;
|
|
2509
|
+
}
|
|
2510
|
+
async save(agent) {
|
|
2511
|
+
const stored = {
|
|
2512
|
+
...agent,
|
|
2513
|
+
updatedAt: this.#now()
|
|
2514
|
+
};
|
|
2515
|
+
await this.#store.save(stored);
|
|
2516
|
+
this.#agents.set(stored.id, stored);
|
|
2517
|
+
return stored;
|
|
2518
|
+
}
|
|
2519
|
+
async bind(agent, sessionId) {
|
|
2520
|
+
const past = agent.sessionId !== void 0 && agent.sessionId !== sessionId ? [...agent.pastSessions, agent.sessionId] : agent.pastSessions;
|
|
2521
|
+
return this.save({
|
|
2522
|
+
...agent,
|
|
2523
|
+
sessionId,
|
|
2524
|
+
pastSessions: past
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
hasMembers(id) {
|
|
2528
|
+
for (const agent of this.#agents.values()) if (agent.lead === id) return true;
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
members(id) {
|
|
2532
|
+
return [...this.#agents.values()].filter((agent) => agent.lead === id).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
2533
|
+
}
|
|
2534
|
+
leadRefusal(mover, leadId) {
|
|
2535
|
+
if (leadId === mover.id) return {
|
|
2536
|
+
status: 409,
|
|
2537
|
+
error: "an agent cannot lead its own team"
|
|
2538
|
+
};
|
|
2539
|
+
const lead = this.#agents.get(leadId);
|
|
2540
|
+
if (!lead) return {
|
|
2541
|
+
status: 404,
|
|
2542
|
+
error: `no such agent: ${leadId}`
|
|
2543
|
+
};
|
|
2544
|
+
if (lead.lead !== void 0) return {
|
|
2545
|
+
status: 409,
|
|
2546
|
+
error: `${lead.name} is a member of a team; teams are one level deep`
|
|
2547
|
+
};
|
|
2548
|
+
if (this.hasMembers(mover.id)) return {
|
|
2549
|
+
status: 409,
|
|
2550
|
+
error: `${mover.name} leads a team; teams are one level deep`
|
|
2551
|
+
};
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
async update(id, patch) {
|
|
2555
|
+
const agent = this.#agents.get(id);
|
|
2556
|
+
if (!agent) return {
|
|
2557
|
+
status: 404,
|
|
2558
|
+
error: `no such agent: ${id}`
|
|
2559
|
+
};
|
|
2560
|
+
const next = { ...agent };
|
|
2561
|
+
if (patch.name !== void 0) {
|
|
2562
|
+
const name = readName(patch.name);
|
|
2563
|
+
if (typeof name !== "string") return name;
|
|
2564
|
+
next.name = name;
|
|
2565
|
+
}
|
|
2566
|
+
if (patch.config !== void 0) {
|
|
2567
|
+
if (!isRecord$1(patch.config)) return {
|
|
2568
|
+
status: 400,
|
|
2569
|
+
error: "config must be an object"
|
|
2570
|
+
};
|
|
2571
|
+
const config = readConfig({
|
|
2572
|
+
...agent.config,
|
|
2573
|
+
...patch.config
|
|
2574
|
+
});
|
|
2575
|
+
if ("error" in config) return config;
|
|
2576
|
+
next.config = config;
|
|
2577
|
+
}
|
|
2578
|
+
if (patch.lead === null) delete next.lead;
|
|
2579
|
+
else if (patch.lead !== void 0) {
|
|
2580
|
+
if (typeof patch.lead !== "string") return {
|
|
2581
|
+
status: 400,
|
|
2582
|
+
error: "lead must be an agent id or null"
|
|
2583
|
+
};
|
|
2584
|
+
const refused = this.leadRefusal(agent, patch.lead);
|
|
2585
|
+
if (refused) return refused;
|
|
2586
|
+
next.lead = patch.lead;
|
|
2587
|
+
}
|
|
2588
|
+
if (patch.order !== void 0) {
|
|
2589
|
+
if (typeof patch.order !== "number" || !Number.isFinite(patch.order)) return {
|
|
2590
|
+
status: 400,
|
|
2591
|
+
error: "order must be a number"
|
|
2592
|
+
};
|
|
2593
|
+
next.order = patch.order;
|
|
2594
|
+
}
|
|
2595
|
+
return this.save(next);
|
|
2596
|
+
}
|
|
2597
|
+
async retire(id, members) {
|
|
2598
|
+
const agent = this.#agents.get(id);
|
|
2599
|
+
if (!agent) return {
|
|
2600
|
+
status: 404,
|
|
2601
|
+
error: `no such agent: ${id}`
|
|
2602
|
+
};
|
|
2603
|
+
const crew = this.members(id);
|
|
2604
|
+
const retired = [agent, ...members === "retire" ? crew : []];
|
|
2605
|
+
const released = [];
|
|
2606
|
+
if (members === "release") for (const member of crew) {
|
|
2607
|
+
const { lead: _lead, ...rest } = member;
|
|
2608
|
+
released.push(await this.save(rest));
|
|
2609
|
+
}
|
|
2610
|
+
for (const gone of retired) {
|
|
2611
|
+
await this.#store.delete(gone.id);
|
|
2612
|
+
this.#agents.delete(gone.id);
|
|
2613
|
+
}
|
|
2614
|
+
return {
|
|
2615
|
+
retired,
|
|
2616
|
+
released
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
#suggestName() {
|
|
2620
|
+
const taken = new Set([...this.#agents.values()].map((agent) => agent.name.toLowerCase()));
|
|
2621
|
+
const free = NAMES.find((name) => !taken.has(name.toLowerCase()));
|
|
2622
|
+
if (free) return free;
|
|
2623
|
+
for (let n = 2;; n++) {
|
|
2624
|
+
const name = `${NAMES[n % NAMES.length]} ${n}`;
|
|
2625
|
+
if (!taken.has(name.toLowerCase())) return name;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
function isAgentRefusal(value) {
|
|
2630
|
+
return isRecord$1(value) && typeof value.status === "number" && typeof value.error === "string";
|
|
2631
|
+
}
|
|
2632
|
+
function readName(value) {
|
|
2633
|
+
if (typeof value !== "string" || value.trim() === "") return {
|
|
2634
|
+
status: 400,
|
|
2635
|
+
error: "name must be a non-empty string"
|
|
2636
|
+
};
|
|
2637
|
+
const name = value.trim();
|
|
2638
|
+
return name.length > MAX_NAME ? {
|
|
2639
|
+
status: 400,
|
|
2640
|
+
error: `name is longer than ${MAX_NAME} characters`
|
|
2641
|
+
} : name;
|
|
2642
|
+
}
|
|
2643
|
+
function readConfig(value) {
|
|
2644
|
+
if (value === void 0) return {};
|
|
2645
|
+
if (!isRecord$1(value)) return {
|
|
2646
|
+
status: 400,
|
|
2647
|
+
error: "config must be an object"
|
|
2648
|
+
};
|
|
2649
|
+
const config = {};
|
|
2650
|
+
for (const key of CONFIG_KEYS) if (value[key] !== void 0) config[key] = value[key];
|
|
2651
|
+
for (const key of [
|
|
2652
|
+
"cwd",
|
|
2653
|
+
"profile",
|
|
2654
|
+
"model",
|
|
2655
|
+
"reasoningEffort",
|
|
2656
|
+
"permissionMode",
|
|
2657
|
+
"brief"
|
|
2658
|
+
]) if (config[key] !== void 0 && typeof config[key] !== "string") return {
|
|
2659
|
+
status: 400,
|
|
2660
|
+
error: `config.${key} must be a string`
|
|
2661
|
+
};
|
|
2662
|
+
if (config.agentContextReset !== void 0 && typeof config.agentContextReset !== "boolean") return {
|
|
2663
|
+
status: 400,
|
|
2664
|
+
error: "config.agentContextReset must be a boolean"
|
|
2665
|
+
};
|
|
2666
|
+
const sleep = config.sleepAfterMs;
|
|
2667
|
+
if (sleep !== void 0 && (typeof sleep !== "number" || !Number.isFinite(sleep) || sleep < 0)) return {
|
|
2668
|
+
status: 400,
|
|
2669
|
+
error: "config.sleepAfterMs must be a non-negative number"
|
|
2670
|
+
};
|
|
2671
|
+
if (typeof config.brief === "string" && config.brief.length > MAX_BRIEF) return {
|
|
2672
|
+
status: 400,
|
|
2673
|
+
error: `config.brief is longer than ${MAX_BRIEF} characters`
|
|
2674
|
+
};
|
|
2675
|
+
return config;
|
|
2676
|
+
}
|
|
2677
|
+
function isRecord$1(value) {
|
|
2678
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2679
|
+
}
|
|
2680
|
+
//#endregion
|
|
2681
|
+
//#region src/lib/host-only-keys.ts
|
|
2682
|
+
const HOST_ONLY_KEY_TABLE = {
|
|
2683
|
+
epoch: "durable",
|
|
2684
|
+
pricing: "transient",
|
|
2685
|
+
queryFn: "transient",
|
|
2686
|
+
env: "transient",
|
|
2687
|
+
pathToClaudeCodeExecutable: "durable",
|
|
2688
|
+
extraOptions: "transient",
|
|
2689
|
+
instructions: "transient",
|
|
2690
|
+
defaultApprovalTimeoutMs: "durable",
|
|
2691
|
+
backfillHistory: "durable",
|
|
2692
|
+
historyFn: "transient",
|
|
2693
|
+
sessionInfoFn: "transient",
|
|
2694
|
+
peers: "transient",
|
|
2695
|
+
contextReset: "transient",
|
|
2696
|
+
shells: "transient",
|
|
2697
|
+
shellAgentWrite: "transient",
|
|
2698
|
+
createdByOperator: "durable",
|
|
2699
|
+
fallbackTitle: "transient",
|
|
2700
|
+
startAsleep: "transient",
|
|
2701
|
+
effortDefaults: "transient",
|
|
2702
|
+
connectFn: "transient",
|
|
2703
|
+
codexHome: "durable",
|
|
2704
|
+
codexPathOverride: "durable",
|
|
2705
|
+
languageModel: "transient",
|
|
2706
|
+
tools: "transient",
|
|
2707
|
+
maxSteps: "durable",
|
|
2708
|
+
executor: "transient",
|
|
2709
|
+
executableTools: "transient",
|
|
2710
|
+
vfs: "transient",
|
|
2711
|
+
executionLimits: "durable",
|
|
2712
|
+
executionBackend: "transient",
|
|
2713
|
+
toolTitles: "transient",
|
|
2714
|
+
shouldApprove: "transient",
|
|
2715
|
+
resolveModel: "transient",
|
|
2716
|
+
contextWindow: "transient",
|
|
2717
|
+
reportMcpServers: "transient",
|
|
2718
|
+
onClose: "transient",
|
|
2719
|
+
restore: "transient"
|
|
2720
|
+
};
|
|
2721
|
+
const HOST_ONLY_KEYS = new Set(Object.keys(HOST_ONLY_KEY_TABLE));
|
|
2722
|
+
const DURABLE_HOST_KEYS = Object.entries(HOST_ONLY_KEY_TABLE).filter(([, lifetime]) => lifetime === "durable").map(([key]) => key);
|
|
2723
|
+
//#endregion
|
|
2724
|
+
//#region src/routes/create-vet.ts
|
|
2725
|
+
function vetCreateRequest(ctx, body, auth) {
|
|
2726
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return {
|
|
2727
|
+
ok: false,
|
|
2728
|
+
status: 400,
|
|
2729
|
+
error: "request body must be a JSON object"
|
|
2730
|
+
};
|
|
2731
|
+
const hostOnly = Object.keys(body).filter((key) => HOST_ONLY_KEYS.has(key));
|
|
2732
|
+
if (hostOnly.length > 0) return {
|
|
2733
|
+
ok: false,
|
|
2734
|
+
status: 400,
|
|
2735
|
+
error: `host-only runner config is not accepted on a session request: ${hostOnly.join(", ")} (set it from the gateway's buildRunnerConfig hook)`
|
|
2736
|
+
};
|
|
2737
|
+
const req = pickCreateSessionRequest(body);
|
|
2738
|
+
const { availability, factory } = ctx;
|
|
2739
|
+
const early = factory.applyScope(req, auth) ?? refusal(403, factory.applyBypassPolicy(req));
|
|
2740
|
+
if (early) return {
|
|
2741
|
+
ok: false,
|
|
2742
|
+
...early
|
|
2743
|
+
};
|
|
2744
|
+
const resolved = factory.resolveProfile(req.profile, auth.allowedProfiles);
|
|
2745
|
+
if (!resolved.ok) return resolved;
|
|
2746
|
+
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));
|
|
2747
|
+
if (refused) return {
|
|
2748
|
+
ok: false,
|
|
2749
|
+
...refused
|
|
2750
|
+
};
|
|
2751
|
+
factory.stripInertFields(req, resolved.profile);
|
|
2752
|
+
req.profile = resolved.profile?.name;
|
|
2753
|
+
return {
|
|
2754
|
+
ok: true,
|
|
2755
|
+
request: req
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function refusal(status, error) {
|
|
2759
|
+
return error === null ? null : {
|
|
2760
|
+
status,
|
|
2761
|
+
error
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
function refuseHostAuthority(ctx, req, profile, auth) {
|
|
2765
|
+
if (ctx.auth.isOperator(auth)) return null;
|
|
2766
|
+
const engine = engineOf(profile);
|
|
2767
|
+
const name = profile?.name ?? "default";
|
|
2768
|
+
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`;
|
|
2769
|
+
const mode = refusePermissionMode(req.permissionMode, { operator: false });
|
|
2770
|
+
if (mode) return mode;
|
|
2771
|
+
if (req.allowDangerouslySkipPermissions) return "allowDangerouslySkipPermissions is reserved to operators";
|
|
2772
|
+
if (req.settingSources !== void 0 && req.settingSources.length > 0) return "settingSources is reserved to operators: it loads hooks and settings from the host";
|
|
2773
|
+
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";
|
|
2774
|
+
if (req.resume !== void 0 && !ownsSdkSession(ctx, auth, req.resume)) return "resume is limited to a live session this caller can see";
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
function ownsSdkSession(ctx, auth, sdkSessionId) {
|
|
2778
|
+
return ctx.registry.list().some((info) => info.sdkSessionId === sdkSessionId && ctx.auth.canSee(auth, info));
|
|
2779
|
+
}
|
|
2780
|
+
function isRemoteMcpServer(server) {
|
|
2781
|
+
const type = server?.type;
|
|
2782
|
+
return type === "http" || type === "sse";
|
|
2783
|
+
}
|
|
2784
|
+
//#endregion
|
|
2785
|
+
//#region src/routes/agents.ts
|
|
2786
|
+
async function handleAgents(ctx, req, res, pathname, auth) {
|
|
2787
|
+
const agents = ctx.agents;
|
|
2788
|
+
const rest = pathname.slice((ctx.basePath + "/agents").length).replace(/^\//, "");
|
|
2789
|
+
if (rest === "") {
|
|
2790
|
+
requireMethod(req, "GET", "POST");
|
|
2791
|
+
if (req.method === "GET") {
|
|
2792
|
+
json(res, 200, { agents: agents.list() });
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
await createAgent(ctx, agents, req, res, auth);
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
const [id, action, ...extra] = rest.split("/").map(decodeURIComponent);
|
|
2799
|
+
const agent = id === void 0 ? void 0 : agents.get(id);
|
|
2800
|
+
if (!agent || extra.length > 0) fail(404, "agent not found");
|
|
2801
|
+
if (action === "restart") {
|
|
2802
|
+
requireMethod(req, "POST");
|
|
2803
|
+
const body = await readJsonBody(req, ctx.maxBodyBytes) ?? {};
|
|
2804
|
+
const previous = agent.sessionId === void 0 ? void 0 : ctx.registry.get(agent.sessionId);
|
|
2805
|
+
const created = await startSession(ctx, agent, typeof body.prompt === "string" ? body.prompt : void 0, auth);
|
|
2806
|
+
const status = previous?.info().status;
|
|
2807
|
+
if (previous && status !== "closed" && status !== "failed") previous.close("server");
|
|
2808
|
+
await respond(ctx, res, 200, await bindWithAvatar(ctx, agent, created), created);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
if (action === "avatar.png" || action === "avatar-busy.png") {
|
|
2812
|
+
requireMethod(req, "GET");
|
|
2813
|
+
await sendAvatar(ctx, req, res, agent, action === "avatar-busy.png");
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (action !== void 0) fail(404, "agent not found");
|
|
2817
|
+
requireMethod(req, "GET", "PATCH", "DELETE");
|
|
2818
|
+
if (req.method === "GET") {
|
|
2819
|
+
await respond(ctx, res, 200, agent);
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
if (req.method === "PATCH") {
|
|
2823
|
+
const patch = await readJsonBody(req, ctx.maxBodyBytes);
|
|
2824
|
+
const updated = await agents.update(agent.id, isRecord(patch) ? patch : {});
|
|
2825
|
+
if (isAgentRefusal(updated)) fail(updated.status, updated.error);
|
|
2826
|
+
await respond(ctx, res, 200, updated);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
const members = (await readJsonBody(req, ctx.maxBodyBytes) ?? {}).members === "retire" ? "retire" : "release";
|
|
2830
|
+
const outcome = await agents.retire(agent.id, members);
|
|
2831
|
+
if (isAgentRefusal(outcome)) fail(outcome.status, outcome.error);
|
|
2832
|
+
for (const gone of outcome.retired) {
|
|
2833
|
+
const runner = gone.sessionId === void 0 ? void 0 : ctx.registry.get(gone.sessionId);
|
|
2834
|
+
const status = runner?.info().status;
|
|
2835
|
+
if (runner && status !== "closed" && status !== "failed") runner.close("client");
|
|
2836
|
+
}
|
|
2837
|
+
json(res, 200, {
|
|
2838
|
+
retired: outcome.retired.map((a) => a.id),
|
|
2839
|
+
released: outcome.released.map((a) => a.id)
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
async function createAgent(ctx, agents, req, res, auth) {
|
|
2843
|
+
const body = await readJsonBody(req, ctx.maxBodyBytes);
|
|
2844
|
+
if (!isRecord(body)) fail(400, "request body must be a JSON object");
|
|
2845
|
+
if (body.adopt !== void 0) {
|
|
2846
|
+
await adoptSession(ctx, agents, res, body);
|
|
2011
2847
|
return;
|
|
2012
2848
|
}
|
|
2013
|
-
const
|
|
2014
|
-
if (
|
|
2015
|
-
|
|
2849
|
+
const draft = agents.draft(body);
|
|
2850
|
+
if (isAgentRefusal(draft)) fail(draft.status, draft.error);
|
|
2851
|
+
const created = await startSession(ctx, draft, typeof body.prompt === "string" ? body.prompt : void 0, auth);
|
|
2852
|
+
await respond(ctx, res, 201, await bindWithAvatar(ctx, draft, created), created);
|
|
2853
|
+
}
|
|
2854
|
+
async function adoptSession(ctx, agents, res, body) {
|
|
2855
|
+
if (typeof body.adopt !== "string") fail(400, "adopt must be a session id");
|
|
2856
|
+
const info = await sessionInfoOf(ctx, body.adopt);
|
|
2857
|
+
if (!info) fail(404, "session not found");
|
|
2858
|
+
if (agents.bySession(info.id)) fail(409, "that session already belongs to an agent");
|
|
2859
|
+
const draft = agents.draft({
|
|
2860
|
+
name: body.name ?? info.title,
|
|
2861
|
+
config: {
|
|
2862
|
+
...configOf(info),
|
|
2863
|
+
...body.config
|
|
2864
|
+
},
|
|
2865
|
+
lead: body.lead
|
|
2866
|
+
});
|
|
2867
|
+
if (isAgentRefusal(draft)) fail(draft.status, draft.error);
|
|
2868
|
+
await respond(ctx, res, 201, await bindWithAvatar(ctx, draft, info));
|
|
2869
|
+
}
|
|
2870
|
+
async function bindWithAvatar(ctx, agent, session) {
|
|
2871
|
+
const recipe = agent.avatarRecipe ?? await rollAvatar(ctx, agent, session);
|
|
2872
|
+
return ctx.agents.bind(recipe === void 0 ? agent : {
|
|
2873
|
+
...agent,
|
|
2874
|
+
avatarRecipe: recipe
|
|
2875
|
+
}, session.id);
|
|
2876
|
+
}
|
|
2877
|
+
async function rollAvatar(ctx, agent, session) {
|
|
2878
|
+
if (!ctx.avatars) return;
|
|
2879
|
+
const project = session ? ctx.projects.withProject(session).project?.root ?? session.cwd : void 0;
|
|
2880
|
+
try {
|
|
2881
|
+
return await ctx.avatars.roll(agent.avatarSeed, session?.engine, project || void 0);
|
|
2882
|
+
} catch {
|
|
2016
2883
|
return;
|
|
2017
2884
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2885
|
+
}
|
|
2886
|
+
async function sendAvatar(ctx, req, res, agent, busy) {
|
|
2887
|
+
const avatars = ctx.avatars;
|
|
2888
|
+
if (!avatars) fail(404, "this gateway draws no avatars");
|
|
2889
|
+
let recipe = agent.avatarRecipe;
|
|
2890
|
+
if (recipe === void 0) {
|
|
2891
|
+
recipe = await rollAvatar(ctx, agent, agent.sessionId === void 0 ? void 0 : await sessionInfoOf(ctx, agent.sessionId));
|
|
2892
|
+
if (recipe === void 0) fail(503, "the avatar pack could not be loaded");
|
|
2893
|
+
await ctx.agents.save({
|
|
2894
|
+
...agent,
|
|
2895
|
+
avatarRecipe: recipe
|
|
2896
|
+
});
|
|
2022
2897
|
}
|
|
2023
|
-
const
|
|
2024
|
-
if (!
|
|
2025
|
-
|
|
2898
|
+
const image = busy ? await avatars.busy(recipe) : await avatars.still(recipe);
|
|
2899
|
+
if (!image) fail(404, "this avatar pack has no busy animation");
|
|
2900
|
+
const headers = {
|
|
2901
|
+
"content-type": "image/png",
|
|
2902
|
+
etag: image.etag,
|
|
2903
|
+
"cache-control": "private, max-age=86400"
|
|
2904
|
+
};
|
|
2905
|
+
if (image.durations) headers["x-frame-durations"] = image.durations.join(",");
|
|
2906
|
+
if (req.headers["if-none-match"] === image.etag) {
|
|
2907
|
+
res.writeHead(304, headers).end();
|
|
2026
2908
|
return;
|
|
2027
2909
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2910
|
+
res.writeHead(200, headers).end(Buffer.from(image.png));
|
|
2911
|
+
}
|
|
2912
|
+
async function startSession(ctx, agent, prompt, auth) {
|
|
2913
|
+
const { config } = agent;
|
|
2914
|
+
const vetted = vetCreateRequest(ctx, stripUndefined({
|
|
2915
|
+
cwd: config.cwd,
|
|
2916
|
+
profile: config.profile,
|
|
2917
|
+
model: config.model,
|
|
2918
|
+
reasoningEffort: config.reasoningEffort,
|
|
2919
|
+
permissionMode: config.permissionMode,
|
|
2920
|
+
allowDangerouslySkipPermissions: config.permissionMode === "bypassPermissions" ? true : void 0,
|
|
2921
|
+
agentContextReset: config.agentContextReset ?? true,
|
|
2922
|
+
prompt
|
|
2923
|
+
}), auth);
|
|
2924
|
+
if (!vetted.ok) fail(vetted.status, vetted.error);
|
|
2925
|
+
return (await ctx.factory.createRunner(ctx.factory.buildRunnerConfig(vetted.request, { operator: ctx.auth.isOperator(auth) }), { brief: config.brief?.trim() || void 0 })).info();
|
|
2926
|
+
}
|
|
2927
|
+
async function respond(ctx, res, status, agent, session) {
|
|
2928
|
+
const info = session ?? (agent.sessionId === void 0 ? void 0 : await sessionInfoOf(ctx, agent.sessionId));
|
|
2929
|
+
const body = { agent: ctx.agents.public(agent) };
|
|
2930
|
+
if (info) body.session = ctx.projects.withProject(info);
|
|
2931
|
+
json(res, status, body);
|
|
2932
|
+
}
|
|
2933
|
+
function configOf(info) {
|
|
2934
|
+
return stripUndefined({
|
|
2935
|
+
cwd: info.cwd || void 0,
|
|
2936
|
+
profile: info.profile,
|
|
2937
|
+
model: info.model,
|
|
2938
|
+
reasoningEffort: info.effort ?? void 0,
|
|
2939
|
+
permissionMode: info.permissionMode
|
|
2030
2940
|
});
|
|
2031
2941
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
function
|
|
2036
|
-
|
|
2037
|
-
hostname(),
|
|
2038
|
-
platform(),
|
|
2039
|
-
arch(),
|
|
2040
|
-
homedir()
|
|
2041
|
-
].join("\0")).digest("hex").slice(0, 32);
|
|
2042
|
-
return cached;
|
|
2942
|
+
function stripUndefined(value) {
|
|
2943
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
2944
|
+
}
|
|
2945
|
+
function isRecord(value) {
|
|
2946
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2043
2947
|
}
|
|
2044
2948
|
//#endregion
|
|
2045
2949
|
//#region src/routes/executions.ts
|
|
@@ -2489,106 +3393,6 @@ async function handleWrite({ ctx, hostFiles, req, res }) {
|
|
|
2489
3393
|
});
|
|
2490
3394
|
}
|
|
2491
3395
|
//#endregion
|
|
2492
|
-
//#region src/lib/host-only-keys.ts
|
|
2493
|
-
const HOST_ONLY_KEY_TABLE = {
|
|
2494
|
-
epoch: "durable",
|
|
2495
|
-
pricing: "transient",
|
|
2496
|
-
queryFn: "transient",
|
|
2497
|
-
env: "transient",
|
|
2498
|
-
pathToClaudeCodeExecutable: "durable",
|
|
2499
|
-
extraOptions: "transient",
|
|
2500
|
-
instructions: "transient",
|
|
2501
|
-
defaultApprovalTimeoutMs: "durable",
|
|
2502
|
-
backfillHistory: "durable",
|
|
2503
|
-
historyFn: "transient",
|
|
2504
|
-
sessionInfoFn: "transient",
|
|
2505
|
-
peers: "transient",
|
|
2506
|
-
shells: "transient",
|
|
2507
|
-
shellAgentWrite: "transient",
|
|
2508
|
-
createdByOperator: "durable",
|
|
2509
|
-
fallbackTitle: "transient",
|
|
2510
|
-
connectFn: "transient",
|
|
2511
|
-
codexHome: "durable",
|
|
2512
|
-
codexPathOverride: "durable",
|
|
2513
|
-
languageModel: "transient",
|
|
2514
|
-
tools: "transient",
|
|
2515
|
-
maxSteps: "durable",
|
|
2516
|
-
executor: "transient",
|
|
2517
|
-
executableTools: "transient",
|
|
2518
|
-
vfs: "transient",
|
|
2519
|
-
executionLimits: "durable",
|
|
2520
|
-
executionBackend: "transient",
|
|
2521
|
-
toolTitles: "transient",
|
|
2522
|
-
shouldApprove: "transient",
|
|
2523
|
-
resolveModel: "transient",
|
|
2524
|
-
reportMcpServers: "transient",
|
|
2525
|
-
onClose: "transient",
|
|
2526
|
-
restore: "transient"
|
|
2527
|
-
};
|
|
2528
|
-
const HOST_ONLY_KEYS = new Set(Object.keys(HOST_ONLY_KEY_TABLE));
|
|
2529
|
-
const DURABLE_HOST_KEYS = Object.entries(HOST_ONLY_KEY_TABLE).filter(([, lifetime]) => lifetime === "durable").map(([key]) => key);
|
|
2530
|
-
//#endregion
|
|
2531
|
-
//#region src/routes/create-vet.ts
|
|
2532
|
-
function vetCreateRequest(ctx, body, auth) {
|
|
2533
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) return {
|
|
2534
|
-
ok: false,
|
|
2535
|
-
status: 400,
|
|
2536
|
-
error: "request body must be a JSON object"
|
|
2537
|
-
};
|
|
2538
|
-
const hostOnly = Object.keys(body).filter((key) => HOST_ONLY_KEYS.has(key));
|
|
2539
|
-
if (hostOnly.length > 0) return {
|
|
2540
|
-
ok: false,
|
|
2541
|
-
status: 400,
|
|
2542
|
-
error: `host-only runner config is not accepted on a session request: ${hostOnly.join(", ")} (set it from the gateway's buildRunnerConfig hook)`
|
|
2543
|
-
};
|
|
2544
|
-
const req = pickCreateSessionRequest(body);
|
|
2545
|
-
const { availability, factory } = ctx;
|
|
2546
|
-
const early = factory.applyScope(req, auth) ?? refusal(403, factory.applyBypassPolicy(req));
|
|
2547
|
-
if (early) return {
|
|
2548
|
-
ok: false,
|
|
2549
|
-
...early
|
|
2550
|
-
};
|
|
2551
|
-
const resolved = factory.resolveProfile(req.profile, auth.allowedProfiles);
|
|
2552
|
-
if (!resolved.ok) return resolved;
|
|
2553
|
-
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));
|
|
2554
|
-
if (refused) return {
|
|
2555
|
-
ok: false,
|
|
2556
|
-
...refused
|
|
2557
|
-
};
|
|
2558
|
-
factory.stripInertFields(req, resolved.profile);
|
|
2559
|
-
req.profile = resolved.profile?.name;
|
|
2560
|
-
return {
|
|
2561
|
-
ok: true,
|
|
2562
|
-
request: req
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
function refusal(status, error) {
|
|
2566
|
-
return error === null ? null : {
|
|
2567
|
-
status,
|
|
2568
|
-
error
|
|
2569
|
-
};
|
|
2570
|
-
}
|
|
2571
|
-
function refuseHostAuthority(ctx, req, profile, auth) {
|
|
2572
|
-
if (ctx.auth.isOperator(auth)) return null;
|
|
2573
|
-
const engine = engineOf(profile);
|
|
2574
|
-
const name = profile?.name ?? "default";
|
|
2575
|
-
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`;
|
|
2576
|
-
const mode = refusePermissionMode(req.permissionMode, { operator: false });
|
|
2577
|
-
if (mode) return mode;
|
|
2578
|
-
if (req.allowDangerouslySkipPermissions) return "allowDangerouslySkipPermissions is reserved to operators";
|
|
2579
|
-
if (req.settingSources !== void 0 && req.settingSources.length > 0) return "settingSources is reserved to operators: it loads hooks and settings from the host";
|
|
2580
|
-
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";
|
|
2581
|
-
if (req.resume !== void 0 && !ownsSdkSession(ctx, auth, req.resume)) return "resume is limited to a live session this caller can see";
|
|
2582
|
-
return null;
|
|
2583
|
-
}
|
|
2584
|
-
function ownsSdkSession(ctx, auth, sdkSessionId) {
|
|
2585
|
-
return ctx.registry.list().some((info) => info.sdkSessionId === sdkSessionId && ctx.auth.canSee(auth, info));
|
|
2586
|
-
}
|
|
2587
|
-
function isRemoteMcpServer(server) {
|
|
2588
|
-
const type = server?.type;
|
|
2589
|
-
return type === "http" || type === "sse";
|
|
2590
|
-
}
|
|
2591
|
-
//#endregion
|
|
2592
3396
|
//#region src/routes/jobs.ts
|
|
2593
3397
|
async function handleJobs(ctx, req, res, pathname, auth) {
|
|
2594
3398
|
const { auth: authSvc, basePath, queue } = ctx;
|
|
@@ -3260,12 +4064,15 @@ const ITEM_HANDLERS = {
|
|
|
3260
4064
|
produced: ({ ctx, req, res, route }) => handleProducedFiles(ctx, req, res, route.id, route.producedFileId),
|
|
3261
4065
|
shells: ({ ctx, req, res, route, runner, auth }) => handleShells(ctx, req, res, route, runner ?? null, ctx.auth.isOperator(auth)),
|
|
3262
4066
|
"stop-task": handleStopTask,
|
|
4067
|
+
"background-task": handleBackgroundTask,
|
|
3263
4068
|
"project-icon": ({ ctx, req, res, info }) => handleProjectIcon(ctx.projects, req, res, info.cwd),
|
|
3264
4069
|
"tool-result": ({ req, res, route, runner, parked }) => {
|
|
3265
4070
|
const snapshot = parked && !isDormant(parked) ? parked.snapshot.events : void 0;
|
|
3266
4071
|
handleToolResult(req, res, runner?.eventAt?.bind(runner) ?? (snapshot && ((seq) => snapshot.find((event) => event.seq === seq))), route.resultSeq);
|
|
3267
4072
|
},
|
|
3268
|
-
permission: handlePermission
|
|
4073
|
+
permission: handlePermission,
|
|
4074
|
+
peers: handlePeers,
|
|
4075
|
+
sleep: handleSleep
|
|
3269
4076
|
};
|
|
3270
4077
|
async function handleSessions(ctx, req, res, route, auth) {
|
|
3271
4078
|
if (route.kind === "collection") {
|
|
@@ -3329,6 +4136,11 @@ async function handleSession({ ctx, req, res, route, runner, parked, info }) {
|
|
|
3329
4136
|
status: "closed"
|
|
3330
4137
|
}) });
|
|
3331
4138
|
}
|
|
4139
|
+
async function handlePeers({ ctx, req, res, route }) {
|
|
4140
|
+
requireMethod(req, "GET");
|
|
4141
|
+
if (!ctx.peers) fail(404, "peer messaging is off on this gateway");
|
|
4142
|
+
json(res, 200, { peers: await ctx.peers.list(route.id) });
|
|
4143
|
+
}
|
|
3332
4144
|
function handleFiles({ req, res, route, runner, parked }) {
|
|
3333
4145
|
requireMethod(req, "GET");
|
|
3334
4146
|
const snapshotFiles = parked && !isDormant(parked) ? parked.snapshot.vfs : void 0;
|
|
@@ -3356,6 +4168,18 @@ async function handleStopTask({ req, res, route, runner }) {
|
|
|
3356
4168
|
if (!await live.stopTask(route.stopTaskId)) fail(404, "no running task to stop");
|
|
3357
4169
|
json(res, 200, { ok: true });
|
|
3358
4170
|
}
|
|
4171
|
+
async function handleBackgroundTask({ req, res, route, runner }) {
|
|
4172
|
+
requireMethod(req, "POST");
|
|
4173
|
+
const live = requireLive(runner, "it has no running tasks");
|
|
4174
|
+
if (!live.backgroundTask) fail(501, `the ${engineOf(live.info())} engine cannot move a task to the background`);
|
|
4175
|
+
if (!await live.backgroundTask(route.backgroundTaskId)) fail(404, "no foreground task to move to the background");
|
|
4176
|
+
json(res, 200, { ok: true });
|
|
4177
|
+
}
|
|
4178
|
+
async function handleSleep({ ctx, req, res, runner, info }) {
|
|
4179
|
+
requireMethod(req, "POST");
|
|
4180
|
+
if (runner) await sleepRunner(runner);
|
|
4181
|
+
json(res, 200, { session: ctx.projects.withProject(runner?.info() ?? info) });
|
|
4182
|
+
}
|
|
3359
4183
|
async function handlePermission({ ctx, req, res, route, runner }) {
|
|
3360
4184
|
requireMethod(req, "POST");
|
|
3361
4185
|
const body = await readJsonBody(req, ctx.maxBodyBytes);
|
|
@@ -3403,6 +4227,11 @@ function httpRoutes(ctx) {
|
|
|
3403
4227
|
auth: "any",
|
|
3404
4228
|
handler: (req, res, pathname, auth) => handleProfiles(ctx, req, res, pathname, auth)
|
|
3405
4229
|
}),
|
|
4230
|
+
route({
|
|
4231
|
+
match: pathWhere(under("/agents")),
|
|
4232
|
+
auth: "operator",
|
|
4233
|
+
handler: (req, res, pathname, auth) => handleAgents(ctx, req, res, pathname, auth)
|
|
4234
|
+
}),
|
|
3406
4235
|
route({
|
|
3407
4236
|
match: pathWhere((pathname) => pathname.startsWith(base + "/executions/")),
|
|
3408
4237
|
auth: "any",
|
|
@@ -3470,6 +4299,36 @@ function pathWhere(test) {
|
|
|
3470
4299
|
return (pathname) => test(pathname) ? pathname : void 0;
|
|
3471
4300
|
}
|
|
3472
4301
|
//#endregion
|
|
4302
|
+
//#region src/services/agent-store.ts
|
|
4303
|
+
function createMemoryAgentStore(seed = []) {
|
|
4304
|
+
const agents = new Map(seed.map((a) => [a.id, a]));
|
|
4305
|
+
return {
|
|
4306
|
+
list: () => [...agents.values()],
|
|
4307
|
+
save: (agent) => void agents.set(agent.id, agent),
|
|
4308
|
+
delete: (id) => void agents.delete(id)
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
function createFileAgentStore(path = join(process.cwd(), ".workerdeck", "agents.json")) {
|
|
4312
|
+
const read = () => {
|
|
4313
|
+
const parsed = readJsonOrSync(path, []);
|
|
4314
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Map();
|
|
4315
|
+
return new Map(parsed.filter((a) => a && typeof a.id === "string" && typeof a.name === "string").map((a) => [a.id, a]));
|
|
4316
|
+
};
|
|
4317
|
+
const write = (agents) => writeJsonAtomicSync(path, [...agents.values()], { indent: 2 });
|
|
4318
|
+
return {
|
|
4319
|
+
list: () => [...read().values()],
|
|
4320
|
+
save: (agent) => {
|
|
4321
|
+
const agents = read();
|
|
4322
|
+
agents.set(agent.id, agent);
|
|
4323
|
+
write(agents);
|
|
4324
|
+
},
|
|
4325
|
+
delete: (id) => {
|
|
4326
|
+
const agents = read();
|
|
4327
|
+
if (agents.delete(id)) write(agents);
|
|
4328
|
+
}
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
//#endregion
|
|
3473
4332
|
//#region src/services/attachments.ts
|
|
3474
4333
|
const DEFAULT_MAX_FILE_BYTES = 10485760;
|
|
3475
4334
|
const DEFAULT_MAX_SESSION_BYTES = 67108864;
|
|
@@ -3568,37 +4427,6 @@ function safeName(name) {
|
|
|
3568
4427
|
return cleaned.length > 120 ? cleaned.slice(0, 120) : cleaned;
|
|
3569
4428
|
}
|
|
3570
4429
|
//#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
4430
|
//#region src/services/auth.ts
|
|
3603
4431
|
function createAuthService(deps) {
|
|
3604
4432
|
const { options, registry } = deps;
|
|
@@ -3708,6 +4536,192 @@ var AvailabilityTracker = class {
|
|
|
3708
4536
|
}
|
|
3709
4537
|
};
|
|
3710
4538
|
//#endregion
|
|
4539
|
+
//#region src/services/engine-sleep.ts
|
|
4540
|
+
var EngineSleepTimers = class {
|
|
4541
|
+
#options;
|
|
4542
|
+
#timers = /* @__PURE__ */ new Map();
|
|
4543
|
+
#runners = /* @__PURE__ */ new Map();
|
|
4544
|
+
#closed = false;
|
|
4545
|
+
constructor(options) {
|
|
4546
|
+
this.#options = options;
|
|
4547
|
+
}
|
|
4548
|
+
get enabled() {
|
|
4549
|
+
return this.#options.afterMs > 0 && !this.#closed;
|
|
4550
|
+
}
|
|
4551
|
+
watch(runner) {
|
|
4552
|
+
if (this.#closed || !runner.sleep || !this.enabled && !this.#options.afterMsFor) return;
|
|
4553
|
+
this.#runners.set(runner.id, runner);
|
|
4554
|
+
const unsubscribe = runner.subscribe((event) => {
|
|
4555
|
+
if (event.type === "status_changed") this.#rearm(runner);
|
|
4556
|
+
}, runner.info().lastSeq);
|
|
4557
|
+
this.#rearm(runner);
|
|
4558
|
+
return () => {
|
|
4559
|
+
unsubscribe();
|
|
4560
|
+
this.#disarm(runner.id);
|
|
4561
|
+
if (this.#runners.get(runner.id) === runner) this.#runners.delete(runner.id);
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
onDetach(sessionId) {
|
|
4565
|
+
const runner = this.#runners.get(sessionId);
|
|
4566
|
+
if (runner) this.#rearm(runner);
|
|
4567
|
+
}
|
|
4568
|
+
close() {
|
|
4569
|
+
this.#closed = true;
|
|
4570
|
+
for (const timer of this.#timers.values()) clearTimeout(timer);
|
|
4571
|
+
this.#timers.clear();
|
|
4572
|
+
this.#runners.clear();
|
|
4573
|
+
}
|
|
4574
|
+
#afterMs(sessionId) {
|
|
4575
|
+
return this.#closed ? 0 : this.#options.afterMsFor?.(sessionId) ?? this.#options.afterMs;
|
|
4576
|
+
}
|
|
4577
|
+
#rearm(runner) {
|
|
4578
|
+
this.#disarm(runner.id);
|
|
4579
|
+
const afterMs = this.#afterMs(runner.id);
|
|
4580
|
+
if (afterMs <= 0 || !this.#sleepable(runner)) return;
|
|
4581
|
+
const timer = setTimeout(() => {
|
|
4582
|
+
this.#timers.delete(runner.id);
|
|
4583
|
+
if (this.#afterMs(runner.id) <= 0 || !this.#sleepable(runner)) return;
|
|
4584
|
+
runner.sleep?.().catch((error) => this.#options.onError?.(error, runner.id));
|
|
4585
|
+
}, afterMs);
|
|
4586
|
+
timer.unref?.();
|
|
4587
|
+
this.#timers.set(runner.id, timer);
|
|
4588
|
+
}
|
|
4589
|
+
#sleepable(runner) {
|
|
4590
|
+
const info = runner.info();
|
|
4591
|
+
return info.status === "idle" && info.engineAsleep !== true && this.#options.attachedCount(runner.id) === 0;
|
|
4592
|
+
}
|
|
4593
|
+
#disarm(sessionId) {
|
|
4594
|
+
clearTimeout(this.#timers.get(sessionId));
|
|
4595
|
+
this.#timers.delete(sessionId);
|
|
4596
|
+
}
|
|
4597
|
+
};
|
|
4598
|
+
const HOUR_MS = 36e5;
|
|
4599
|
+
const RESET_TIMEOUT_MS = 6e4;
|
|
4600
|
+
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.";
|
|
4601
|
+
var ContextResetService = class {
|
|
4602
|
+
#options;
|
|
4603
|
+
#runners = /* @__PURE__ */ new Map();
|
|
4604
|
+
#pending = /* @__PURE__ */ new Map();
|
|
4605
|
+
#running = /* @__PURE__ */ new Set();
|
|
4606
|
+
#history = /* @__PURE__ */ new Map();
|
|
4607
|
+
#turnFailed = /* @__PURE__ */ new Set();
|
|
4608
|
+
#closed = false;
|
|
4609
|
+
constructor(options = {}) {
|
|
4610
|
+
this.#options = options;
|
|
4611
|
+
}
|
|
4612
|
+
get defaultEnabled() {
|
|
4613
|
+
return this.#options.default === true;
|
|
4614
|
+
}
|
|
4615
|
+
watch(runner) {
|
|
4616
|
+
if (this.#closed) return;
|
|
4617
|
+
this.#runners.set(runner.id, runner);
|
|
4618
|
+
const unsubscribe = runner.subscribe((event) => this.#observe(runner, event), runner.info().lastSeq);
|
|
4619
|
+
return () => {
|
|
4620
|
+
unsubscribe();
|
|
4621
|
+
if (this.#runners.get(runner.id) === runner) {
|
|
4622
|
+
this.#runners.delete(runner.id);
|
|
4623
|
+
this.#pending.delete(runner.id);
|
|
4624
|
+
}
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
async request(sessionId, request) {
|
|
4628
|
+
const runner = this.#runners.get(sessionId);
|
|
4629
|
+
if (this.#closed || !runner || runner.info().agentContextReset !== true) throw new Error("context reset is not enabled for this session");
|
|
4630
|
+
if (!runner.clearContext) throw new Error("this engine cannot clear its conversation");
|
|
4631
|
+
if (this.#pending.has(sessionId) || this.#running.has(sessionId)) throw new Error("a reset is already scheduled for this session");
|
|
4632
|
+
const refused = this.#rateRefusal(sessionId);
|
|
4633
|
+
if (refused) throw new Error(refused);
|
|
4634
|
+
this.#pending.set(sessionId, {
|
|
4635
|
+
prompt: request.prompt,
|
|
4636
|
+
reason: request.reason
|
|
4637
|
+
});
|
|
4638
|
+
this.#turnFailed.delete(sessionId);
|
|
4639
|
+
return CONTEXT_RESET_SCHEDULED;
|
|
4640
|
+
}
|
|
4641
|
+
close() {
|
|
4642
|
+
this.#closed = true;
|
|
4643
|
+
this.#pending.clear();
|
|
4644
|
+
this.#runners.clear();
|
|
4645
|
+
}
|
|
4646
|
+
#rateRefusal(sessionId) {
|
|
4647
|
+
const now = this.#now();
|
|
4648
|
+
const minInterval = this.#options.minIntervalMs ?? 6e5;
|
|
4649
|
+
const maxPerHour = this.#options.maxPerHour ?? 3;
|
|
4650
|
+
const recent = (this.#history.get(sessionId) ?? []).filter((at) => now - at < HOUR_MS);
|
|
4651
|
+
const last = recent.at(-1);
|
|
4652
|
+
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`;
|
|
4653
|
+
if (maxPerHour <= 0 || recent.length >= maxPerHour) return `refused: this session already reset ${recent.length} time(s) in the last hour (limit ${maxPerHour}); keep working`;
|
|
4654
|
+
return null;
|
|
4655
|
+
}
|
|
4656
|
+
#observe(runner, event) {
|
|
4657
|
+
if (!this.#pending.has(runner.id)) return;
|
|
4658
|
+
if (event.type === "turn_result" && event.isError) {
|
|
4659
|
+
this.#turnFailed.add(runner.id);
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
if (event.type === "session_closed") {
|
|
4663
|
+
this.#pending.delete(runner.id);
|
|
4664
|
+
return;
|
|
4665
|
+
}
|
|
4666
|
+
if (event.type !== "status_changed" || event.status !== "idle") return;
|
|
4667
|
+
const request = this.#pending.get(runner.id);
|
|
4668
|
+
this.#pending.delete(runner.id);
|
|
4669
|
+
if (this.#turnFailed.delete(runner.id)) return;
|
|
4670
|
+
this.#run(runner, request);
|
|
4671
|
+
}
|
|
4672
|
+
async #run(runner, request) {
|
|
4673
|
+
this.#running.add(runner.id);
|
|
4674
|
+
try {
|
|
4675
|
+
const reset = nextReset(runner, this.#options.resetTimeoutMs ?? RESET_TIMEOUT_MS);
|
|
4676
|
+
try {
|
|
4677
|
+
await runner.clearContext({ agentReason: request.reason });
|
|
4678
|
+
await reset.done;
|
|
4679
|
+
} finally {
|
|
4680
|
+
reset.cancel();
|
|
4681
|
+
}
|
|
4682
|
+
const history = (this.#history.get(runner.id) ?? []).filter((at) => this.#now() - at < HOUR_MS);
|
|
4683
|
+
history.push(this.#now());
|
|
4684
|
+
this.#history.set(runner.id, history);
|
|
4685
|
+
runner.sendMessage(request.prompt);
|
|
4686
|
+
} catch (error) {
|
|
4687
|
+
this.#options.onError?.(error, runner.id);
|
|
4688
|
+
} finally {
|
|
4689
|
+
this.#running.delete(runner.id);
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
#now() {
|
|
4693
|
+
return this.#options.now?.() ?? Date.now();
|
|
4694
|
+
}
|
|
4695
|
+
};
|
|
4696
|
+
function nextReset(runner, timeoutMs) {
|
|
4697
|
+
let timer;
|
|
4698
|
+
let unsubscribe;
|
|
4699
|
+
const cancel = () => {
|
|
4700
|
+
clearTimeout(timer);
|
|
4701
|
+
unsubscribe?.();
|
|
4702
|
+
};
|
|
4703
|
+
return {
|
|
4704
|
+
done: new Promise((resolve, reject) => {
|
|
4705
|
+
timer = setTimeout(() => {
|
|
4706
|
+
cancel();
|
|
4707
|
+
reject(/* @__PURE__ */ new Error("the engine did not confirm the reset"));
|
|
4708
|
+
}, timeoutMs);
|
|
4709
|
+
timer.unref?.();
|
|
4710
|
+
unsubscribe = runner.subscribe((event) => {
|
|
4711
|
+
if (event.type === "conversation_reset") {
|
|
4712
|
+
cancel();
|
|
4713
|
+
resolve();
|
|
4714
|
+
}
|
|
4715
|
+
}, runner.info().lastSeq);
|
|
4716
|
+
}),
|
|
4717
|
+
cancel
|
|
4718
|
+
};
|
|
4719
|
+
}
|
|
4720
|
+
function minutes(ms) {
|
|
4721
|
+
const value = Math.max(1, Math.round(ms / 6e4));
|
|
4722
|
+
return `${value} minute${value === 1 ? "" : "s"}`;
|
|
4723
|
+
}
|
|
4724
|
+
//#endregion
|
|
3711
4725
|
//#region src/services/bridge.ts
|
|
3712
4726
|
var BridgeHub = class {
|
|
3713
4727
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -4023,7 +5037,10 @@ var SessionParkManager = class {
|
|
|
4023
5037
|
}
|
|
4024
5038
|
async listInfo() {
|
|
4025
5039
|
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
|
|
5040
|
+
return (await this.#options.store.list()).filter((record) => this.#options.registry.get(record.id) === void 0).map((record) => isDormant(record) ? {
|
|
5041
|
+
...record.info,
|
|
5042
|
+
engineAsleep: true
|
|
5043
|
+
} : record.info);
|
|
4027
5044
|
}
|
|
4028
5045
|
async ensureLive(id) {
|
|
4029
5046
|
const live = this.#options.registry.get(id);
|
|
@@ -4273,7 +5290,7 @@ var SessionParkManager = class {
|
|
|
4273
5290
|
}
|
|
4274
5291
|
};
|
|
4275
5292
|
function idleRecordBase(runner, config) {
|
|
4276
|
-
const info = runner.info();
|
|
5293
|
+
const { engineAsleep: _engineAsleep, ...info } = runner.info();
|
|
4277
5294
|
return {
|
|
4278
5295
|
id: runner.id,
|
|
4279
5296
|
info: {
|
|
@@ -4338,6 +5355,8 @@ var ProfileService = class {
|
|
|
4338
5355
|
if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
|
|
4339
5356
|
} else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
|
|
4340
5357
|
if (disableBypassPermissions && p.defaults?.permissionMode === "bypassPermissions") return `profile '${p.name}' defaults to bypassPermissions but disableBypassPermissions is set`;
|
|
5358
|
+
const efforts = p.defaults?.efforts;
|
|
5359
|
+
if (efforts !== void 0 && !isEffortMap(efforts)) return `profile '${p.name}' defaults.efforts must map model names to effort levels`;
|
|
4341
5360
|
const fallbackMode = p.defaults?.permissionMode;
|
|
4342
5361
|
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
5362
|
return null;
|
|
@@ -4439,6 +5458,9 @@ var ProfileService = class {
|
|
|
4439
5458
|
};
|
|
4440
5459
|
}
|
|
4441
5460
|
};
|
|
5461
|
+
function isEffortMap(value) {
|
|
5462
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.entries(value).every(([model, effort]) => model !== "" && typeof effort === "string" && effort !== "");
|
|
5463
|
+
}
|
|
4442
5464
|
//#endregion
|
|
4443
5465
|
//#region src/services/profile-usage.ts
|
|
4444
5466
|
var ProfileUsageTracker = class {
|
|
@@ -4716,7 +5738,7 @@ function createRelayLink(options, peers, log) {
|
|
|
4716
5738
|
const remoteTarget = parseRelayPeerId;
|
|
4717
5739
|
const reachesRemote = async (from) => {
|
|
4718
5740
|
const me = await peers.relaySender(from);
|
|
4719
|
-
return me.scope === void 0 || Object.keys(me.scope).length === 0;
|
|
5741
|
+
return (me.scope === void 0 || Object.keys(me.scope).length === 0) && me.agent?.lead === void 0;
|
|
4720
5742
|
};
|
|
4721
5743
|
return {
|
|
4722
5744
|
directory: {
|
|
@@ -4783,264 +5805,6 @@ function createRelayLink(options, peers, log) {
|
|
|
4783
5805
|
};
|
|
4784
5806
|
}
|
|
4785
5807
|
//#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
5808
|
//#region src/services/registry.ts
|
|
5045
5809
|
var SessionRegistry = class {
|
|
5046
5810
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -5252,7 +6016,7 @@ function createSessionFactory(deps) {
|
|
|
5252
6016
|
return adapterFor(profile.engine).sessionEnv?.(profile, process.env) ?? process.env;
|
|
5253
6017
|
}
|
|
5254
6018
|
};
|
|
5255
|
-
const buildRunner = async (built, restore, id) => {
|
|
6019
|
+
const buildRunner = async (built, restore, id, options) => {
|
|
5256
6020
|
const name = built.profile;
|
|
5257
6021
|
const profile = name !== void 0 ? profiles.get(name) : void 0;
|
|
5258
6022
|
if (name !== void 0 && !profile) throw new Error(`unknown profile: ${name}`);
|
|
@@ -5262,13 +6026,23 @@ function createSessionFactory(deps) {
|
|
|
5262
6026
|
...deps.peers ? { peers: deps.peers } : {},
|
|
5263
6027
|
...deps.pricing ? { pricing: deps.pricing } : {}
|
|
5264
6028
|
};
|
|
6029
|
+
delete config.contextReset;
|
|
5265
6030
|
delete config.shells;
|
|
5266
6031
|
delete config.shellAgentWrite;
|
|
6032
|
+
delete config.effortDefaults;
|
|
6033
|
+
const efforts = {
|
|
6034
|
+
...deps.effortDefaults,
|
|
6035
|
+
...profile?.defaults?.efforts
|
|
6036
|
+
};
|
|
6037
|
+
if (Object.keys(efforts).length > 0) config.effortDefaults = efforts;
|
|
6038
|
+
if (deps.contextReset && (built.agentContextReset ?? profile?.defaults?.agentContextReset ?? deps.contextReset.defaultEnabled)) config.contextReset = deps.contextReset.directory;
|
|
5267
6039
|
if (deps.shells && capabilities.hostCwd === true) {
|
|
5268
6040
|
config.shells = deps.shells;
|
|
5269
6041
|
const write = deps.shellAgentWrite ?? "read-only";
|
|
5270
6042
|
if (write !== "read-only" && built.createdByOperator === true) config.shellAgentWrite = write;
|
|
5271
6043
|
}
|
|
6044
|
+
const brief = options?.brief ?? deps.agentBrief?.(id ?? restore?.id);
|
|
6045
|
+
if (brief) config.instructions = composeInstructions(config.instructions, brief);
|
|
5272
6046
|
if (config.instructions !== void 0 && capabilities.systemInstructions === false) throw new Error(`the ${engineOf(profile)} engine cannot deliver system instructions`);
|
|
5273
6047
|
const runner = profile && isProviderProfile(profile) ? await deps.createEngineRunner({
|
|
5274
6048
|
config,
|
|
@@ -5286,8 +6060,8 @@ function createSessionFactory(deps) {
|
|
|
5286
6060
|
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()`);
|
|
5287
6061
|
return runner;
|
|
5288
6062
|
};
|
|
5289
|
-
const createRunner = async (config) => {
|
|
5290
|
-
const runner = registry.register(await buildRunner(config));
|
|
6063
|
+
const createRunner = async (config, options) => {
|
|
6064
|
+
const runner = registry.register(await buildRunner(config, void 0, void 0, options));
|
|
5291
6065
|
parking.remember(runner.id, config);
|
|
5292
6066
|
parking.watch(runner);
|
|
5293
6067
|
runner.start();
|
|
@@ -5406,13 +6180,31 @@ function createWorkerServer(options = {}) {
|
|
|
5406
6180
|
}
|
|
5407
6181
|
const generation = randomUUID();
|
|
5408
6182
|
const shells = options.shell?.enabled === true ? shellRegistryFor(options.shell, generation) : null;
|
|
5409
|
-
const
|
|
6183
|
+
const agents = new AgentService({
|
|
6184
|
+
store: options.agentStore ?? createMemoryAgentStore(),
|
|
6185
|
+
basePath,
|
|
6186
|
+
avatars: options.avatars !== void 0,
|
|
6187
|
+
sleepAfterMs: options.agentSleepAfterMs
|
|
6188
|
+
});
|
|
6189
|
+
const projects = new ProjectInfoService({ decorate: (info) => agents.decorate(shells ? shells.decorate(info) : info) });
|
|
5410
6190
|
const notifier = new SessionNotifier({
|
|
5411
6191
|
...options.notifications,
|
|
5412
6192
|
decorateInfo: (info) => projects.withProject(info),
|
|
5413
6193
|
onError: options.notifications?.onError ?? ((error, context) => diagnose(error, `notification-${context.op}`))
|
|
5414
6194
|
});
|
|
5415
6195
|
const producedFiles = new ProducedFileStore();
|
|
6196
|
+
const engineSleep = new EngineSleepTimers({
|
|
6197
|
+
afterMs: options.engineSleepAfterMs ?? 0,
|
|
6198
|
+
afterMsFor: (sessionId) => agents.sleepAfterFor(sessionId),
|
|
6199
|
+
attachedCount: (sessionId) => bridge.attachedCount(sessionId),
|
|
6200
|
+
onError: (error) => diagnose(error, "engine-sleep")
|
|
6201
|
+
});
|
|
6202
|
+
const contextResets = options.agentContextReset === false ? void 0 : new ContextResetService({
|
|
6203
|
+
...options.agentContextReset,
|
|
6204
|
+
onError: (error) => diagnose(error, "context-reset")
|
|
6205
|
+
});
|
|
6206
|
+
let ownContextResets = contextResets;
|
|
6207
|
+
installContextResetDirectory(contextResets);
|
|
5416
6208
|
const registry = new SessionRegistry({ onRegister: (runner) => {
|
|
5417
6209
|
const detachers = [
|
|
5418
6210
|
notifier.watch(runner),
|
|
@@ -5421,7 +6213,9 @@ function createWorkerServer(options = {}) {
|
|
|
5421
6213
|
spendLedger.watch(runner),
|
|
5422
6214
|
shells?.watch(runner),
|
|
5423
6215
|
peers?.watch(runner),
|
|
5424
|
-
factory.watchAuthSource(runner)
|
|
6216
|
+
factory.watchAuthSource(runner),
|
|
6217
|
+
engineSleep.watch(runner),
|
|
6218
|
+
contextResets?.watch(runner)
|
|
5425
6219
|
];
|
|
5426
6220
|
const profile = runner.info().profile;
|
|
5427
6221
|
if (profile) detachers.push(runner.subscribe((event) => {
|
|
@@ -5467,6 +6261,7 @@ function createWorkerServer(options = {}) {
|
|
|
5467
6261
|
let ownShells = shellDirectory;
|
|
5468
6262
|
if (ownPeers) installPeerDirectory(ownPeers);
|
|
5469
6263
|
installShellDirectory(shellDirectory);
|
|
6264
|
+
if (options.effortDefaults !== void 0 && !isEffortMap(options.effortDefaults)) throw new Error("createWorkerServer: `effortDefaults` must map model names to effort levels");
|
|
5470
6265
|
const factory = createSessionFactory({
|
|
5471
6266
|
adapterFor,
|
|
5472
6267
|
profiles,
|
|
@@ -5475,14 +6270,20 @@ function createWorkerServer(options = {}) {
|
|
|
5475
6270
|
allowedCwdRoots: options.allowedCwdRoots,
|
|
5476
6271
|
disableBypassPermissions: options.disableBypassPermissions,
|
|
5477
6272
|
approvalTimeoutMs: options.approvalTimeoutMs,
|
|
6273
|
+
effortDefaults: options.effortDefaults,
|
|
5478
6274
|
requireApiKey: options.requireApiKey,
|
|
5479
6275
|
peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
|
|
6276
|
+
contextReset: contextResets ? {
|
|
6277
|
+
directory: contextResetDirectoryHandle(() => ownContextResets),
|
|
6278
|
+
defaultEnabled: contextResets.defaultEnabled
|
|
6279
|
+
} : void 0,
|
|
5480
6280
|
shells: shellDirectory ? shellDirectoryHandle(() => ownShells) : void 0,
|
|
5481
6281
|
shellAgentWrite: options.shell?.agentWrite,
|
|
5482
6282
|
pricing: pricing.pricing,
|
|
5483
6283
|
registry,
|
|
5484
6284
|
parking,
|
|
5485
|
-
bridge
|
|
6285
|
+
bridge,
|
|
6286
|
+
agentBrief: (sessionId) => agents.briefFor(sessionId)
|
|
5486
6287
|
});
|
|
5487
6288
|
const availability = new AvailabilityTracker({
|
|
5488
6289
|
checkCredentials: options.checkCredentials,
|
|
@@ -5530,9 +6331,12 @@ function createWorkerServer(options = {}) {
|
|
|
5530
6331
|
availability,
|
|
5531
6332
|
auth,
|
|
5532
6333
|
factory,
|
|
6334
|
+
agents,
|
|
6335
|
+
avatars: options.avatars,
|
|
5533
6336
|
registry,
|
|
5534
6337
|
parking,
|
|
5535
|
-
|
|
6338
|
+
engineSleep,
|
|
6339
|
+
peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
|
|
5536
6340
|
bridge,
|
|
5537
6341
|
projects,
|
|
5538
6342
|
queue,
|
|
@@ -5579,12 +6383,15 @@ function createWorkerServer(options = {}) {
|
|
|
5579
6383
|
parking,
|
|
5580
6384
|
shells,
|
|
5581
6385
|
queue,
|
|
6386
|
+
engineSleep,
|
|
5582
6387
|
closeQueueSockets: queueSockets.clear,
|
|
5583
6388
|
diagnose,
|
|
5584
6389
|
releaseDirectories: () => {
|
|
5585
6390
|
relay?.close();
|
|
5586
6391
|
ownPeers = void 0;
|
|
5587
6392
|
ownShells = void 0;
|
|
6393
|
+
contextResets?.close();
|
|
6394
|
+
ownContextResets = void 0;
|
|
5588
6395
|
}
|
|
5589
6396
|
});
|
|
5590
6397
|
return {
|
|
@@ -5597,6 +6404,7 @@ function createWorkerServer(options = {}) {
|
|
|
5597
6404
|
listen: async (port, host) => {
|
|
5598
6405
|
await profiles.refreshStored();
|
|
5599
6406
|
await profiles.seedStore();
|
|
6407
|
+
await agents.hydrate();
|
|
5600
6408
|
await parking.hydrate();
|
|
5601
6409
|
await shells?.hydrate();
|
|
5602
6410
|
return new Promise((resolve, reject) => {
|
|
@@ -5684,7 +6492,8 @@ function rebuildRunner(factory, record) {
|
|
|
5684
6492
|
resume: record.sdkSessionId
|
|
5685
6493
|
}),
|
|
5686
6494
|
epoch: (record.info.epoch ?? 0) + 1,
|
|
5687
|
-
fallbackTitle: record.info.title
|
|
6495
|
+
fallbackTitle: record.info.title,
|
|
6496
|
+
startAsleep: true
|
|
5688
6497
|
}, void 0, record.id);
|
|
5689
6498
|
}
|
|
5690
6499
|
//#endregion
|
|
@@ -5791,6 +6600,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
|
|
|
5791
6600
|
};
|
|
5792
6601
|
}
|
|
5793
6602
|
//#endregion
|
|
5794
|
-
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 };
|
|
6603
|
+
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 };
|
|
5795
6604
|
|
|
5796
6605
|
//# sourceMappingURL=index.mjs.map
|