@workerdeck/server 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.mts +10 -0
- package/build/index.mjs +600 -313
- package/build/index.mjs.map +1 -1
- package/package.json +7 -7
package/build/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { WebSocketServer } from "ws";
|
|
4
|
-
import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SHELL_REFUSAL as SHELL_REFUSAL$1, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentKind, checkClaudeAuth, clampShellTail, createEngineSession, encodeShellKeys, getEngineAdapter, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText, withoutGatewaySecrets } from "@workerdeck/core";
|
|
4
|
+
import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SHELL_REFUSAL as SHELL_REFUSAL$1, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentKind, checkClaudeAuth, clampShellTail, contextResetDirectoryHandle, createEngineSession, encodeShellKeys, getEngineAdapter, installContextResetDirectory, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText, withoutGatewaySecrets } from "@workerdeck/core";
|
|
5
5
|
import { JobQueue } from "@workerdeck/queue";
|
|
6
6
|
import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_ARTIFACT_MAX_BYTES, SHELL_ARTIFACT_TTL_MS, SHELL_ATTACH_REPLAY_BYTES, SHELL_COLS, SHELL_COMMAND_MAX, SHELL_INPUT_MAX, SHELL_LABEL_MAX, SHELL_LINGER_MS, SHELL_MAX_COLS, SHELL_MAX_ROWS, SHELL_MAX_RUNNING_PER_SESSION, SHELL_MAX_RUNNING_TOTAL, SHELL_MIN_COLS, SHELL_MIN_ROWS, SHELL_READ_MAX_LINES, SHELL_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, SHELL_WAIT_DEFAULT_MS, SHELL_WAIT_MAX_MS, costOfByModel, imagePartRef, isJobRun, mergeByModel, mergePricing, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
|
|
7
7
|
import { chmodSync, closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -79,6 +79,7 @@ function closeServer(deps) {
|
|
|
79
79
|
shells?.killAll("server_stopped");
|
|
80
80
|
const flushed = shells?.flush().catch((error) => deps.diagnose(error, "shell-flush")) ?? Promise.resolve();
|
|
81
81
|
parking.close();
|
|
82
|
+
deps.engineSleep.close();
|
|
82
83
|
registry.closeAll();
|
|
83
84
|
for (const ws of wss.clients) ws.close(1001, "server shutting down");
|
|
84
85
|
deps.closeQueueSockets();
|
|
@@ -222,6 +223,23 @@ function cwdAllowed(cwd, roots) {
|
|
|
222
223
|
return resolved === r || resolved.startsWith(r + sep);
|
|
223
224
|
});
|
|
224
225
|
}
|
|
226
|
+
function listDirs(path) {
|
|
227
|
+
try {
|
|
228
|
+
return readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function listMd(path) {
|
|
234
|
+
try {
|
|
235
|
+
return readdirSync(path).filter((file) => file.endsWith(".md")).map((file) => file.slice(0, -3)).sort();
|
|
236
|
+
} catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function count(rules) {
|
|
241
|
+
return Array.isArray(rules) ? rules.length : 0;
|
|
242
|
+
}
|
|
225
243
|
function readProfileConfig(profile) {
|
|
226
244
|
const dir = profile.configDir;
|
|
227
245
|
if (!dir) return {
|
|
@@ -230,22 +248,8 @@ function readProfileConfig(profile) {
|
|
|
230
248
|
agents: [],
|
|
231
249
|
commands: []
|
|
232
250
|
};
|
|
233
|
-
const listDirs = (path) => {
|
|
234
|
-
try {
|
|
235
|
-
return readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
236
|
-
} catch {
|
|
237
|
-
return [];
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
const listMd = (path) => {
|
|
241
|
-
try {
|
|
242
|
-
return readdirSync(path).filter((file) => file.endsWith(".md")).map((file) => file.slice(0, -3)).sort();
|
|
243
|
-
} catch {
|
|
244
|
-
return [];
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
251
|
const snapshot = {
|
|
248
|
-
hasUserMemory: existsSync(join(dir,
|
|
252
|
+
hasUserMemory: ["CLAUDE.md", "AGENTS.md"].some((name) => existsSync(join(dir, name))),
|
|
249
253
|
skills: listDirs(join(dir, "skills")),
|
|
250
254
|
agents: listMd(join(dir, "agents")),
|
|
251
255
|
commands: listMd(join(dir, "commands"))
|
|
@@ -253,7 +257,6 @@ function readProfileConfig(profile) {
|
|
|
253
257
|
try {
|
|
254
258
|
const raw = JSON.parse(readFileSync(join(dir, "settings.json"), "utf8"));
|
|
255
259
|
const permissions = raw.permissions ?? {};
|
|
256
|
-
const count = (rules) => Array.isArray(rules) ? rules.length : 0;
|
|
257
260
|
snapshot.settings = {
|
|
258
261
|
model: typeof raw.model === "string" ? raw.model : void 0,
|
|
259
262
|
defaultPermissionMode: typeof permissions.defaultMode === "string" ? permissions.defaultMode : void 0,
|
|
@@ -388,6 +391,15 @@ function itemRoute(rawId, parts) {
|
|
|
388
391
|
id: decode(rawId),
|
|
389
392
|
stopTaskId: decode(third)
|
|
390
393
|
};
|
|
394
|
+
if (length === 3 && section === "tasks" && third === "background") return {
|
|
395
|
+
kind: "background-task",
|
|
396
|
+
id: decode(rawId)
|
|
397
|
+
};
|
|
398
|
+
if (length === 4 && section === "tasks" && fourth === "background") return {
|
|
399
|
+
kind: "background-task",
|
|
400
|
+
id: decode(rawId),
|
|
401
|
+
backgroundTaskId: decode(third)
|
|
402
|
+
};
|
|
391
403
|
if (length === 3 && section === "project" && third === "icon") return {
|
|
392
404
|
kind: "project-icon",
|
|
393
405
|
id: decode(rawId)
|
|
@@ -400,6 +412,14 @@ function itemRoute(rawId, parts) {
|
|
|
400
412
|
resultSeq: seq
|
|
401
413
|
} : null;
|
|
402
414
|
}
|
|
415
|
+
if (length === 2 && section === "sleep") return {
|
|
416
|
+
kind: "sleep",
|
|
417
|
+
id: decode(rawId)
|
|
418
|
+
};
|
|
419
|
+
if (length === 2 && section === "peers") return {
|
|
420
|
+
kind: "peers",
|
|
421
|
+
id: decode(rawId)
|
|
422
|
+
};
|
|
403
423
|
if (length <= 3 && section === "mcp") return {
|
|
404
424
|
kind: "mcp",
|
|
405
425
|
id: decode(rawId),
|
|
@@ -464,6 +484,303 @@ function permissionDecision(input) {
|
|
|
464
484
|
};
|
|
465
485
|
}
|
|
466
486
|
//#endregion
|
|
487
|
+
//#region src/lib/scope.ts
|
|
488
|
+
const MAX_SCOPE_KEYS = 16;
|
|
489
|
+
const MAX_SCOPE_LEN = 200;
|
|
490
|
+
function readScope(value) {
|
|
491
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return;
|
|
492
|
+
const entries = Object.entries(value);
|
|
493
|
+
if (entries.some(([, v]) => typeof v !== "string")) return;
|
|
494
|
+
return Object.fromEntries(entries);
|
|
495
|
+
}
|
|
496
|
+
function checkScope(value) {
|
|
497
|
+
if (value === void 0) return null;
|
|
498
|
+
const scope = readScope(value);
|
|
499
|
+
if (!scope) return "scope must be an object of string values";
|
|
500
|
+
const entries = Object.entries(scope);
|
|
501
|
+
if (entries.length > MAX_SCOPE_KEYS) return `scope may carry at most ${MAX_SCOPE_KEYS} keys`;
|
|
502
|
+
for (const [key, val] of entries) {
|
|
503
|
+
if (key.length === 0) return "scope keys must not be empty";
|
|
504
|
+
if (key.length > MAX_SCOPE_LEN || val.length > MAX_SCOPE_LEN) return `scope keys and values must be at most ${MAX_SCOPE_LEN} characters`;
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
function sameScope(a, b) {
|
|
509
|
+
const left = Object.entries(a ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
|
|
510
|
+
const right = Object.entries(b ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
|
|
511
|
+
return left.length === right.length && left.every(([key, value], i) => right[i][0] === key && right[i][1] === value);
|
|
512
|
+
}
|
|
513
|
+
function scopeMatches(principal, session) {
|
|
514
|
+
if (!principal) return true;
|
|
515
|
+
return Object.entries(principal).every(([key, value]) => session?.[key] === value);
|
|
516
|
+
}
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/services/peers.ts
|
|
519
|
+
const DEFAULT_PER_MINUTE = 10;
|
|
520
|
+
const DEFAULT_MAX_HOPS = 12;
|
|
521
|
+
const WINDOW_MS = 6e4;
|
|
522
|
+
function visible(from, to) {
|
|
523
|
+
return to.id !== from.id && scopeMatches(from.scope, to.scope);
|
|
524
|
+
}
|
|
525
|
+
function relayEntry(info, live) {
|
|
526
|
+
const items = info.checklist;
|
|
527
|
+
return {
|
|
528
|
+
id: info.id,
|
|
529
|
+
engine: info.engine,
|
|
530
|
+
status: info.status,
|
|
531
|
+
title: info.title,
|
|
532
|
+
project: info.project ? {
|
|
533
|
+
name: info.project.name,
|
|
534
|
+
root: info.project.root
|
|
535
|
+
} : void 0,
|
|
536
|
+
cwd: info.cwd,
|
|
537
|
+
profile: info.profile,
|
|
538
|
+
model: info.model,
|
|
539
|
+
permissionMode: info.permissionMode,
|
|
540
|
+
contextUsage: info.contextUsage,
|
|
541
|
+
checklist: items?.length ? {
|
|
542
|
+
done: items.filter((item) => item.status === "completed").length,
|
|
543
|
+
total: items.length
|
|
544
|
+
} : void 0,
|
|
545
|
+
numTurns: info.numTurns,
|
|
546
|
+
createdAt: info.createdAt,
|
|
547
|
+
lastActivityAt: info.lastActivityAt,
|
|
548
|
+
pendingPermissionCount: info.pendingPermissionCount,
|
|
549
|
+
live
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function createPeerService(deps) {
|
|
553
|
+
const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
|
|
554
|
+
const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
|
|
555
|
+
const maxHops = deps.options?.maxHops ?? DEFAULT_MAX_HOPS;
|
|
556
|
+
const sent = /* @__PURE__ */ new Map();
|
|
557
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
558
|
+
const registry = () => {
|
|
559
|
+
const found = deps.refs.registry;
|
|
560
|
+
if (!found) throw new Error("peer messaging is not ready");
|
|
561
|
+
return found;
|
|
562
|
+
};
|
|
563
|
+
const allSessions = async () => {
|
|
564
|
+
const live = registry().list();
|
|
565
|
+
const dormant = await deps.refs.parking?.listInfo() ?? [];
|
|
566
|
+
return [...live, ...dormant].map((info) => deps.projects.withProject(info));
|
|
567
|
+
};
|
|
568
|
+
const infoOf = async (id) => {
|
|
569
|
+
const live = registry().get(id)?.info();
|
|
570
|
+
if (live) return deps.projects.withProject(live);
|
|
571
|
+
const record = await deps.refs.parking?.get(id);
|
|
572
|
+
return record ? deps.projects.withProject(record.info) : void 0;
|
|
573
|
+
};
|
|
574
|
+
const sender = async (from) => {
|
|
575
|
+
const info = await infoOf(from);
|
|
576
|
+
if (!info) throw new Error(`unknown sender session: ${from}`);
|
|
577
|
+
return info;
|
|
578
|
+
};
|
|
579
|
+
const recentOf = (sessionId, recent) => {
|
|
580
|
+
const limit = Math.min(Math.max(recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
|
|
581
|
+
const runner = registry().get(sessionId);
|
|
582
|
+
const events = [];
|
|
583
|
+
if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
|
|
584
|
+
truncateResults: true,
|
|
585
|
+
imageRefs: true
|
|
586
|
+
})();
|
|
587
|
+
return {
|
|
588
|
+
runner,
|
|
589
|
+
lines: recentLines(events, limit)
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
const underRateLimit = (from, to) => {
|
|
593
|
+
const key = `${from}->${to}`;
|
|
594
|
+
const now = Date.now();
|
|
595
|
+
const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
|
|
596
|
+
if (recent.length >= perMinute) {
|
|
597
|
+
sent.set(key, recent);
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
recent.push(now);
|
|
601
|
+
sent.set(key, recent);
|
|
602
|
+
return true;
|
|
603
|
+
};
|
|
604
|
+
const list = async (from) => {
|
|
605
|
+
const me = await sender(from);
|
|
606
|
+
return (await allSessions()).filter((info) => visible(me, info)).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map(peerSummary);
|
|
607
|
+
};
|
|
608
|
+
const peek = async (from, sessionId, options) => {
|
|
609
|
+
const me = await sender(from);
|
|
610
|
+
const info = await infoOf(sessionId);
|
|
611
|
+
if (!info || !visible(me, info)) return;
|
|
612
|
+
const { runner, lines } = recentOf(sessionId, options?.recent);
|
|
613
|
+
return {
|
|
614
|
+
...peerSummary(info),
|
|
615
|
+
live: runner !== void 0,
|
|
616
|
+
checklist: info.checklist,
|
|
617
|
+
pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
|
|
618
|
+
recent: lines
|
|
619
|
+
};
|
|
620
|
+
};
|
|
621
|
+
const send = async (from, sessionId, text, options) => {
|
|
622
|
+
const me = await sender(from);
|
|
623
|
+
if (sessionId === from) return {
|
|
624
|
+
delivered: false,
|
|
625
|
+
reason: "that is this session"
|
|
626
|
+
};
|
|
627
|
+
if (text.length > maxChars) return {
|
|
628
|
+
delivered: false,
|
|
629
|
+
reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
|
|
630
|
+
};
|
|
631
|
+
const target = await infoOf(sessionId);
|
|
632
|
+
if (!target || !visible(me, target)) return {
|
|
633
|
+
delivered: false,
|
|
634
|
+
reason: `no such session: ${sessionId}`
|
|
635
|
+
};
|
|
636
|
+
const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
|
|
637
|
+
return deliver(target, text, {
|
|
638
|
+
kind: "peer",
|
|
639
|
+
sessionId: from,
|
|
640
|
+
name: me.title,
|
|
641
|
+
engine: me.engine,
|
|
642
|
+
hops
|
|
643
|
+
}, from);
|
|
644
|
+
};
|
|
645
|
+
const deliver = async (target, text, origin, rateKey) => {
|
|
646
|
+
const sessionId = target.id;
|
|
647
|
+
if (target.status === "closed" || target.status === "failed") return {
|
|
648
|
+
delivered: false,
|
|
649
|
+
reason: `session ${sessionId} is ${target.status}`
|
|
650
|
+
};
|
|
651
|
+
const hops = origin.hops ?? [];
|
|
652
|
+
if (hops.length > maxHops) return {
|
|
653
|
+
delivered: false,
|
|
654
|
+
reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
|
|
655
|
+
};
|
|
656
|
+
if (!underRateLimit(rateKey, sessionId)) return {
|
|
657
|
+
delivered: false,
|
|
658
|
+
reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
|
|
659
|
+
};
|
|
660
|
+
const runner = await deps.refs.parking?.ensureLive(sessionId) ?? registry().get(sessionId);
|
|
661
|
+
if (!runner) return {
|
|
662
|
+
delivered: false,
|
|
663
|
+
reason: `session ${sessionId} could not be woken`
|
|
664
|
+
};
|
|
665
|
+
const before = runner.info().status;
|
|
666
|
+
try {
|
|
667
|
+
runner.sendMessage(text, void 0, { origin });
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return {
|
|
670
|
+
delivered: false,
|
|
671
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
inbound.set(sessionId, hops);
|
|
675
|
+
return {
|
|
676
|
+
delivered: true,
|
|
677
|
+
sessionId,
|
|
678
|
+
name: target.title,
|
|
679
|
+
queued: before === "running" || before === "awaiting_approval"
|
|
680
|
+
};
|
|
681
|
+
};
|
|
682
|
+
const exposedInfo = async (sessionId, exposed) => {
|
|
683
|
+
const info = await infoOf(sessionId);
|
|
684
|
+
return info && info.status !== "closed" && scopeMatches(exposed, info.scope) ? info : void 0;
|
|
685
|
+
};
|
|
686
|
+
const relayEntries = async (exposed) => {
|
|
687
|
+
const live = new Set(registry().list().map((info) => info.id));
|
|
688
|
+
return (await allSessions()).filter((info) => info.status !== "closed" && scopeMatches(exposed, info.scope)).map((info) => relayEntry(info, live.has(info.id)));
|
|
689
|
+
};
|
|
690
|
+
const relayPeek = async (sessionId, recent, exposed) => {
|
|
691
|
+
const info = await exposedInfo(sessionId, exposed);
|
|
692
|
+
if (!info) return;
|
|
693
|
+
const { runner, lines } = recentOf(sessionId, recent);
|
|
694
|
+
return {
|
|
695
|
+
...relayEntry(info, runner !== void 0),
|
|
696
|
+
checklistItems: info.checklist,
|
|
697
|
+
pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
|
|
698
|
+
recent: lines
|
|
699
|
+
};
|
|
700
|
+
};
|
|
701
|
+
const relaySend = async (origin, sessionId, text, exposed) => {
|
|
702
|
+
if (text.length === 0 || text.length > maxChars) return {
|
|
703
|
+
delivered: false,
|
|
704
|
+
reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
|
|
705
|
+
};
|
|
706
|
+
const target = await exposedInfo(sessionId, exposed);
|
|
707
|
+
if (!target) return {
|
|
708
|
+
delivered: false,
|
|
709
|
+
reason: `no such session: ${sessionId}`
|
|
710
|
+
};
|
|
711
|
+
const from = `${origin.gateway}:${origin.sessionId}`;
|
|
712
|
+
return deliver(target, text, {
|
|
713
|
+
kind: "peer",
|
|
714
|
+
sessionId: from,
|
|
715
|
+
hostId: origin.gateway,
|
|
716
|
+
name: origin.name,
|
|
717
|
+
engine: origin.engine,
|
|
718
|
+
hops: origin.hops
|
|
719
|
+
}, from);
|
|
720
|
+
};
|
|
721
|
+
const watch = (runner) => runner.subscribe((event) => {
|
|
722
|
+
if (event.type === "user_message" && !event.origin && !event.synthetic && event.parentToolUseId == null && !event.replay) inbound.delete(runner.id);
|
|
723
|
+
}, runner.info().lastSeq);
|
|
724
|
+
return {
|
|
725
|
+
list,
|
|
726
|
+
peek,
|
|
727
|
+
send,
|
|
728
|
+
watch,
|
|
729
|
+
relaySender: sender,
|
|
730
|
+
relayChain: (from) => [...inbound.get(from) ?? [], from],
|
|
731
|
+
relayEntries,
|
|
732
|
+
relayPeek,
|
|
733
|
+
relaySend
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function resolvePeerMentions(rows, text) {
|
|
737
|
+
const tokens = scanPeerMentions(text);
|
|
738
|
+
if (tokens.length === 0) return [];
|
|
739
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
740
|
+
const add = (key, row) => {
|
|
741
|
+
const held = byKey.get(key);
|
|
742
|
+
if (held) held.push(row);
|
|
743
|
+
else byKey.set(key, [row]);
|
|
744
|
+
};
|
|
745
|
+
for (const row of rows) {
|
|
746
|
+
add(peerMentionKey(peerMentionSlug(row.title, row.id)), row);
|
|
747
|
+
add(peerMentionKey(row.id), row);
|
|
748
|
+
}
|
|
749
|
+
const resolved = [];
|
|
750
|
+
const seen = /* @__PURE__ */ new Set();
|
|
751
|
+
for (const token of tokens) {
|
|
752
|
+
const key = peerMentionKey(token.body);
|
|
753
|
+
const exact = byKey.get(key);
|
|
754
|
+
const matches = exact ?? (key.length >= 4 ? rows.filter((row) => row.id.startsWith(key)) : []);
|
|
755
|
+
const target = matches[0];
|
|
756
|
+
if (!target || matches.length > 1 && !exact) continue;
|
|
757
|
+
if (seen.has(target.id)) continue;
|
|
758
|
+
seen.add(target.id);
|
|
759
|
+
const others = matches.slice(1, 4).map((row) => row.id);
|
|
760
|
+
resolved.push({
|
|
761
|
+
typed: token.body,
|
|
762
|
+
id: target.id,
|
|
763
|
+
name: target.title,
|
|
764
|
+
engine: target.engine,
|
|
765
|
+
status: target.status,
|
|
766
|
+
cwd: target.cwd,
|
|
767
|
+
...others.length ? { ambiguousWith: others } : {}
|
|
768
|
+
});
|
|
769
|
+
if (resolved.length >= PEER_MENTION_MAX) break;
|
|
770
|
+
}
|
|
771
|
+
return resolved;
|
|
772
|
+
}
|
|
773
|
+
async function mentionsFor(directory, from, text) {
|
|
774
|
+
return scanPeerMentions(text).length === 0 ? [] : resolvePeerMentions(await directory.list(from), text);
|
|
775
|
+
}
|
|
776
|
+
//#endregion
|
|
777
|
+
//#region src/routes/sleep.ts
|
|
778
|
+
async function sleepRunner(runner) {
|
|
779
|
+
if (!runner.sleep) throw new HttpError(501, `the ${engineOf(runner.info())} engine has no process to put to sleep`);
|
|
780
|
+
const result = await runner.sleep();
|
|
781
|
+
if (!result.ok) throw new HttpError(409, `cannot sleep: ${result.reason}`);
|
|
782
|
+
}
|
|
783
|
+
//#endregion
|
|
467
784
|
//#region src/services/shell-directory.ts
|
|
468
785
|
function createShellDirectory(registry, deps = {}) {
|
|
469
786
|
const take = async (from, shellId, options) => {
|
|
@@ -1853,13 +2170,14 @@ function attachClient(ctx, ws, runner, req, access) {
|
|
|
1853
2170
|
unsubscribe();
|
|
1854
2171
|
detachBridge();
|
|
1855
2172
|
parking.onDetach(runner.id);
|
|
2173
|
+
ctx.engineSleep.onDetach(runner.id);
|
|
1856
2174
|
});
|
|
1857
2175
|
}
|
|
1858
2176
|
async function handleCommand(ctx, frame, runner, access, client) {
|
|
1859
2177
|
const { attachmentStore, bridge } = ctx;
|
|
1860
2178
|
switch (frame.type) {
|
|
1861
2179
|
case "user_message": {
|
|
1862
|
-
const mentions = ctx.peers && !isSlashCommand(frame.text) ? await ctx.peers
|
|
2180
|
+
const mentions = ctx.peers && !isSlashCommand(frame.text) ? await mentionsFor(ctx.peers, runner.id, frame.text).catch(() => []) : [];
|
|
1863
2181
|
const options = mentions.length > 0 ? { mentions } : void 0;
|
|
1864
2182
|
if (!frame.attachmentIds?.length) {
|
|
1865
2183
|
runner.sendMessage(frame.text, void 0, options);
|
|
@@ -1876,6 +2194,13 @@ async function handleCommand(ctx, frame, runner, access, client) {
|
|
|
1876
2194
|
case "interrupt":
|
|
1877
2195
|
await runner.interrupt();
|
|
1878
2196
|
return;
|
|
2197
|
+
case "sleep":
|
|
2198
|
+
await sleepRunner(runner);
|
|
2199
|
+
return;
|
|
2200
|
+
case "background_task":
|
|
2201
|
+
if (!runner.backgroundTask) throw new Error(`the ${engineOf(runner.info())} engine cannot move a task to the background`);
|
|
2202
|
+
await runner.backgroundTask(frame.toolUseId);
|
|
2203
|
+
return;
|
|
1879
2204
|
case "clear_context":
|
|
1880
2205
|
if (!runner.clearContext) throw new Error(`the ${engineOf(runner.info())} engine cannot clear a conversation`);
|
|
1881
2206
|
await runner.clearContext();
|
|
@@ -1892,6 +2217,11 @@ async function handleCommand(ctx, frame, runner, access, client) {
|
|
|
1892
2217
|
case "set_model":
|
|
1893
2218
|
await runner.setModel(frame.model);
|
|
1894
2219
|
return;
|
|
2220
|
+
case "set_effort":
|
|
2221
|
+
if (!runner.setEffort) throw new Error(`the ${engineOf(runner.info())} engine cannot change its reasoning effort`);
|
|
2222
|
+
if (frame.effort !== void 0 && typeof frame.effort !== "string") throw new Error("effort must be a string");
|
|
2223
|
+
await runner.setEffort(frame.effort);
|
|
2224
|
+
return;
|
|
1895
2225
|
case "tool_call_result":
|
|
1896
2226
|
bridge.resolve(runner.id, frame.executionId, {
|
|
1897
2227
|
output: frame.output,
|
|
@@ -2503,10 +2833,13 @@ const HOST_ONLY_KEY_TABLE = {
|
|
|
2503
2833
|
historyFn: "transient",
|
|
2504
2834
|
sessionInfoFn: "transient",
|
|
2505
2835
|
peers: "transient",
|
|
2836
|
+
contextReset: "transient",
|
|
2506
2837
|
shells: "transient",
|
|
2507
2838
|
shellAgentWrite: "transient",
|
|
2508
2839
|
createdByOperator: "durable",
|
|
2509
2840
|
fallbackTitle: "transient",
|
|
2841
|
+
startAsleep: "transient",
|
|
2842
|
+
effortDefaults: "transient",
|
|
2510
2843
|
connectFn: "transient",
|
|
2511
2844
|
codexHome: "durable",
|
|
2512
2845
|
codexPathOverride: "durable",
|
|
@@ -2521,6 +2854,7 @@ const HOST_ONLY_KEY_TABLE = {
|
|
|
2521
2854
|
toolTitles: "transient",
|
|
2522
2855
|
shouldApprove: "transient",
|
|
2523
2856
|
resolveModel: "transient",
|
|
2857
|
+
contextWindow: "transient",
|
|
2524
2858
|
reportMcpServers: "transient",
|
|
2525
2859
|
onClose: "transient",
|
|
2526
2860
|
restore: "transient"
|
|
@@ -3260,12 +3594,15 @@ const ITEM_HANDLERS = {
|
|
|
3260
3594
|
produced: ({ ctx, req, res, route }) => handleProducedFiles(ctx, req, res, route.id, route.producedFileId),
|
|
3261
3595
|
shells: ({ ctx, req, res, route, runner, auth }) => handleShells(ctx, req, res, route, runner ?? null, ctx.auth.isOperator(auth)),
|
|
3262
3596
|
"stop-task": handleStopTask,
|
|
3597
|
+
"background-task": handleBackgroundTask,
|
|
3263
3598
|
"project-icon": ({ ctx, req, res, info }) => handleProjectIcon(ctx.projects, req, res, info.cwd),
|
|
3264
3599
|
"tool-result": ({ req, res, route, runner, parked }) => {
|
|
3265
3600
|
const snapshot = parked && !isDormant(parked) ? parked.snapshot.events : void 0;
|
|
3266
3601
|
handleToolResult(req, res, runner?.eventAt?.bind(runner) ?? (snapshot && ((seq) => snapshot.find((event) => event.seq === seq))), route.resultSeq);
|
|
3267
3602
|
},
|
|
3268
|
-
permission: handlePermission
|
|
3603
|
+
permission: handlePermission,
|
|
3604
|
+
peers: handlePeers,
|
|
3605
|
+
sleep: handleSleep
|
|
3269
3606
|
};
|
|
3270
3607
|
async function handleSessions(ctx, req, res, route, auth) {
|
|
3271
3608
|
if (route.kind === "collection") {
|
|
@@ -3329,6 +3666,11 @@ async function handleSession({ ctx, req, res, route, runner, parked, info }) {
|
|
|
3329
3666
|
status: "closed"
|
|
3330
3667
|
}) });
|
|
3331
3668
|
}
|
|
3669
|
+
async function handlePeers({ ctx, req, res, route }) {
|
|
3670
|
+
requireMethod(req, "GET");
|
|
3671
|
+
if (!ctx.peers) fail(404, "peer messaging is off on this gateway");
|
|
3672
|
+
json(res, 200, { peers: await ctx.peers.list(route.id) });
|
|
3673
|
+
}
|
|
3332
3674
|
function handleFiles({ req, res, route, runner, parked }) {
|
|
3333
3675
|
requireMethod(req, "GET");
|
|
3334
3676
|
const snapshotFiles = parked && !isDormant(parked) ? parked.snapshot.vfs : void 0;
|
|
@@ -3356,6 +3698,18 @@ async function handleStopTask({ req, res, route, runner }) {
|
|
|
3356
3698
|
if (!await live.stopTask(route.stopTaskId)) fail(404, "no running task to stop");
|
|
3357
3699
|
json(res, 200, { ok: true });
|
|
3358
3700
|
}
|
|
3701
|
+
async function handleBackgroundTask({ req, res, route, runner }) {
|
|
3702
|
+
requireMethod(req, "POST");
|
|
3703
|
+
const live = requireLive(runner, "it has no running tasks");
|
|
3704
|
+
if (!live.backgroundTask) fail(501, `the ${engineOf(live.info())} engine cannot move a task to the background`);
|
|
3705
|
+
if (!await live.backgroundTask(route.backgroundTaskId)) fail(404, "no foreground task to move to the background");
|
|
3706
|
+
json(res, 200, { ok: true });
|
|
3707
|
+
}
|
|
3708
|
+
async function handleSleep({ ctx, req, res, runner, info }) {
|
|
3709
|
+
requireMethod(req, "POST");
|
|
3710
|
+
if (runner) await sleepRunner(runner);
|
|
3711
|
+
json(res, 200, { session: ctx.projects.withProject(runner?.info() ?? info) });
|
|
3712
|
+
}
|
|
3359
3713
|
async function handlePermission({ ctx, req, res, route, runner }) {
|
|
3360
3714
|
requireMethod(req, "POST");
|
|
3361
3715
|
const body = await readJsonBody(req, ctx.maxBodyBytes);
|
|
@@ -3568,37 +3922,6 @@ function safeName(name) {
|
|
|
3568
3922
|
return cleaned.length > 120 ? cleaned.slice(0, 120) : cleaned;
|
|
3569
3923
|
}
|
|
3570
3924
|
//#endregion
|
|
3571
|
-
//#region src/lib/scope.ts
|
|
3572
|
-
const MAX_SCOPE_KEYS = 16;
|
|
3573
|
-
const MAX_SCOPE_LEN = 200;
|
|
3574
|
-
function readScope(value) {
|
|
3575
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return;
|
|
3576
|
-
const entries = Object.entries(value);
|
|
3577
|
-
if (entries.some(([, v]) => typeof v !== "string")) return;
|
|
3578
|
-
return Object.fromEntries(entries);
|
|
3579
|
-
}
|
|
3580
|
-
function checkScope(value) {
|
|
3581
|
-
if (value === void 0) return null;
|
|
3582
|
-
const scope = readScope(value);
|
|
3583
|
-
if (!scope) return "scope must be an object of string values";
|
|
3584
|
-
const entries = Object.entries(scope);
|
|
3585
|
-
if (entries.length > MAX_SCOPE_KEYS) return `scope may carry at most ${MAX_SCOPE_KEYS} keys`;
|
|
3586
|
-
for (const [key, val] of entries) {
|
|
3587
|
-
if (key.length === 0) return "scope keys must not be empty";
|
|
3588
|
-
if (key.length > MAX_SCOPE_LEN || val.length > MAX_SCOPE_LEN) return `scope keys and values must be at most ${MAX_SCOPE_LEN} characters`;
|
|
3589
|
-
}
|
|
3590
|
-
return null;
|
|
3591
|
-
}
|
|
3592
|
-
function sameScope(a, b) {
|
|
3593
|
-
const left = Object.entries(a ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
|
|
3594
|
-
const right = Object.entries(b ?? {}).sort(([x], [y]) => x < y ? -1 : 1);
|
|
3595
|
-
return left.length === right.length && left.every(([key, value], i) => right[i][0] === key && right[i][1] === value);
|
|
3596
|
-
}
|
|
3597
|
-
function scopeMatches(principal, session) {
|
|
3598
|
-
if (!principal) return true;
|
|
3599
|
-
return Object.entries(principal).every(([key, value]) => session?.[key] === value);
|
|
3600
|
-
}
|
|
3601
|
-
//#endregion
|
|
3602
3925
|
//#region src/services/auth.ts
|
|
3603
3926
|
function createAuthService(deps) {
|
|
3604
3927
|
const { options, registry } = deps;
|
|
@@ -3708,6 +4031,188 @@ var AvailabilityTracker = class {
|
|
|
3708
4031
|
}
|
|
3709
4032
|
};
|
|
3710
4033
|
//#endregion
|
|
4034
|
+
//#region src/services/engine-sleep.ts
|
|
4035
|
+
var EngineSleepTimers = class {
|
|
4036
|
+
#options;
|
|
4037
|
+
#timers = /* @__PURE__ */ new Map();
|
|
4038
|
+
#runners = /* @__PURE__ */ new Map();
|
|
4039
|
+
#closed = false;
|
|
4040
|
+
constructor(options) {
|
|
4041
|
+
this.#options = options;
|
|
4042
|
+
}
|
|
4043
|
+
get enabled() {
|
|
4044
|
+
return this.#options.afterMs > 0 && !this.#closed;
|
|
4045
|
+
}
|
|
4046
|
+
watch(runner) {
|
|
4047
|
+
if (!this.enabled || !runner.sleep) return;
|
|
4048
|
+
this.#runners.set(runner.id, runner);
|
|
4049
|
+
const unsubscribe = runner.subscribe((event) => {
|
|
4050
|
+
if (event.type === "status_changed") this.#rearm(runner);
|
|
4051
|
+
}, runner.info().lastSeq);
|
|
4052
|
+
this.#rearm(runner);
|
|
4053
|
+
return () => {
|
|
4054
|
+
unsubscribe();
|
|
4055
|
+
this.#disarm(runner.id);
|
|
4056
|
+
if (this.#runners.get(runner.id) === runner) this.#runners.delete(runner.id);
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
4059
|
+
onDetach(sessionId) {
|
|
4060
|
+
const runner = this.#runners.get(sessionId);
|
|
4061
|
+
if (runner) this.#rearm(runner);
|
|
4062
|
+
}
|
|
4063
|
+
close() {
|
|
4064
|
+
this.#closed = true;
|
|
4065
|
+
for (const timer of this.#timers.values()) clearTimeout(timer);
|
|
4066
|
+
this.#timers.clear();
|
|
4067
|
+
this.#runners.clear();
|
|
4068
|
+
}
|
|
4069
|
+
#rearm(runner) {
|
|
4070
|
+
this.#disarm(runner.id);
|
|
4071
|
+
if (!this.enabled || !this.#sleepable(runner)) return;
|
|
4072
|
+
const timer = setTimeout(() => {
|
|
4073
|
+
this.#timers.delete(runner.id);
|
|
4074
|
+
if (!this.enabled || !this.#sleepable(runner)) return;
|
|
4075
|
+
runner.sleep?.().catch((error) => this.#options.onError?.(error, runner.id));
|
|
4076
|
+
}, this.#options.afterMs);
|
|
4077
|
+
timer.unref?.();
|
|
4078
|
+
this.#timers.set(runner.id, timer);
|
|
4079
|
+
}
|
|
4080
|
+
#sleepable(runner) {
|
|
4081
|
+
const info = runner.info();
|
|
4082
|
+
return info.status === "idle" && info.engineAsleep !== true && this.#options.attachedCount(runner.id) === 0;
|
|
4083
|
+
}
|
|
4084
|
+
#disarm(sessionId) {
|
|
4085
|
+
clearTimeout(this.#timers.get(sessionId));
|
|
4086
|
+
this.#timers.delete(sessionId);
|
|
4087
|
+
}
|
|
4088
|
+
};
|
|
4089
|
+
const HOUR_MS = 36e5;
|
|
4090
|
+
const RESET_TIMEOUT_MS = 6e4;
|
|
4091
|
+
const CONTEXT_RESET_SCHEDULED = "Reset scheduled. It runs when this turn ends: your conversation is cleared and your prompt becomes the first message of a fresh one. End your turn now and start no new work. If the turn is interrupted or ends in an error, the reset is cancelled.";
|
|
4092
|
+
var ContextResetService = class {
|
|
4093
|
+
#options;
|
|
4094
|
+
#runners = /* @__PURE__ */ new Map();
|
|
4095
|
+
#pending = /* @__PURE__ */ new Map();
|
|
4096
|
+
#running = /* @__PURE__ */ new Set();
|
|
4097
|
+
#history = /* @__PURE__ */ new Map();
|
|
4098
|
+
#turnFailed = /* @__PURE__ */ new Set();
|
|
4099
|
+
#closed = false;
|
|
4100
|
+
constructor(options = {}) {
|
|
4101
|
+
this.#options = options;
|
|
4102
|
+
}
|
|
4103
|
+
get defaultEnabled() {
|
|
4104
|
+
return this.#options.default === true;
|
|
4105
|
+
}
|
|
4106
|
+
watch(runner) {
|
|
4107
|
+
if (this.#closed) return;
|
|
4108
|
+
this.#runners.set(runner.id, runner);
|
|
4109
|
+
const unsubscribe = runner.subscribe((event) => this.#observe(runner, event), runner.info().lastSeq);
|
|
4110
|
+
return () => {
|
|
4111
|
+
unsubscribe();
|
|
4112
|
+
if (this.#runners.get(runner.id) === runner) {
|
|
4113
|
+
this.#runners.delete(runner.id);
|
|
4114
|
+
this.#pending.delete(runner.id);
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
async request(sessionId, request) {
|
|
4119
|
+
const runner = this.#runners.get(sessionId);
|
|
4120
|
+
if (this.#closed || !runner || runner.info().agentContextReset !== true) throw new Error("context reset is not enabled for this session");
|
|
4121
|
+
if (!runner.clearContext) throw new Error("this engine cannot clear its conversation");
|
|
4122
|
+
if (this.#pending.has(sessionId) || this.#running.has(sessionId)) throw new Error("a reset is already scheduled for this session");
|
|
4123
|
+
const refused = this.#rateRefusal(sessionId);
|
|
4124
|
+
if (refused) throw new Error(refused);
|
|
4125
|
+
this.#pending.set(sessionId, {
|
|
4126
|
+
prompt: request.prompt,
|
|
4127
|
+
reason: request.reason
|
|
4128
|
+
});
|
|
4129
|
+
this.#turnFailed.delete(sessionId);
|
|
4130
|
+
return CONTEXT_RESET_SCHEDULED;
|
|
4131
|
+
}
|
|
4132
|
+
close() {
|
|
4133
|
+
this.#closed = true;
|
|
4134
|
+
this.#pending.clear();
|
|
4135
|
+
this.#runners.clear();
|
|
4136
|
+
}
|
|
4137
|
+
#rateRefusal(sessionId) {
|
|
4138
|
+
const now = this.#now();
|
|
4139
|
+
const minInterval = this.#options.minIntervalMs ?? 6e5;
|
|
4140
|
+
const maxPerHour = this.#options.maxPerHour ?? 3;
|
|
4141
|
+
const recent = (this.#history.get(sessionId) ?? []).filter((at) => now - at < HOUR_MS);
|
|
4142
|
+
const last = recent.at(-1);
|
|
4143
|
+
if (last !== void 0 && now - last < minInterval) return `refused: the last reset was ${minutes(now - last)} ago and resets need ${minutes(minInterval)} between them; keep working`;
|
|
4144
|
+
if (maxPerHour <= 0 || recent.length >= maxPerHour) return `refused: this session already reset ${recent.length} time(s) in the last hour (limit ${maxPerHour}); keep working`;
|
|
4145
|
+
return null;
|
|
4146
|
+
}
|
|
4147
|
+
#observe(runner, event) {
|
|
4148
|
+
if (!this.#pending.has(runner.id)) return;
|
|
4149
|
+
if (event.type === "turn_result" && event.isError) {
|
|
4150
|
+
this.#turnFailed.add(runner.id);
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
if (event.type === "session_closed") {
|
|
4154
|
+
this.#pending.delete(runner.id);
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
if (event.type !== "status_changed" || event.status !== "idle") return;
|
|
4158
|
+
const request = this.#pending.get(runner.id);
|
|
4159
|
+
this.#pending.delete(runner.id);
|
|
4160
|
+
if (this.#turnFailed.delete(runner.id)) return;
|
|
4161
|
+
this.#run(runner, request);
|
|
4162
|
+
}
|
|
4163
|
+
async #run(runner, request) {
|
|
4164
|
+
this.#running.add(runner.id);
|
|
4165
|
+
try {
|
|
4166
|
+
const reset = nextReset(runner, this.#options.resetTimeoutMs ?? RESET_TIMEOUT_MS);
|
|
4167
|
+
try {
|
|
4168
|
+
await runner.clearContext({ agentReason: request.reason });
|
|
4169
|
+
await reset.done;
|
|
4170
|
+
} finally {
|
|
4171
|
+
reset.cancel();
|
|
4172
|
+
}
|
|
4173
|
+
const history = (this.#history.get(runner.id) ?? []).filter((at) => this.#now() - at < HOUR_MS);
|
|
4174
|
+
history.push(this.#now());
|
|
4175
|
+
this.#history.set(runner.id, history);
|
|
4176
|
+
runner.sendMessage(request.prompt);
|
|
4177
|
+
} catch (error) {
|
|
4178
|
+
this.#options.onError?.(error, runner.id);
|
|
4179
|
+
} finally {
|
|
4180
|
+
this.#running.delete(runner.id);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
#now() {
|
|
4184
|
+
return this.#options.now?.() ?? Date.now();
|
|
4185
|
+
}
|
|
4186
|
+
};
|
|
4187
|
+
function nextReset(runner, timeoutMs) {
|
|
4188
|
+
let timer;
|
|
4189
|
+
let unsubscribe;
|
|
4190
|
+
const cancel = () => {
|
|
4191
|
+
clearTimeout(timer);
|
|
4192
|
+
unsubscribe?.();
|
|
4193
|
+
};
|
|
4194
|
+
return {
|
|
4195
|
+
done: new Promise((resolve, reject) => {
|
|
4196
|
+
timer = setTimeout(() => {
|
|
4197
|
+
cancel();
|
|
4198
|
+
reject(/* @__PURE__ */ new Error("the engine did not confirm the reset"));
|
|
4199
|
+
}, timeoutMs);
|
|
4200
|
+
timer.unref?.();
|
|
4201
|
+
unsubscribe = runner.subscribe((event) => {
|
|
4202
|
+
if (event.type === "conversation_reset") {
|
|
4203
|
+
cancel();
|
|
4204
|
+
resolve();
|
|
4205
|
+
}
|
|
4206
|
+
}, runner.info().lastSeq);
|
|
4207
|
+
}),
|
|
4208
|
+
cancel
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
function minutes(ms) {
|
|
4212
|
+
const value = Math.max(1, Math.round(ms / 6e4));
|
|
4213
|
+
return `${value} minute${value === 1 ? "" : "s"}`;
|
|
4214
|
+
}
|
|
4215
|
+
//#endregion
|
|
3711
4216
|
//#region src/services/bridge.ts
|
|
3712
4217
|
var BridgeHub = class {
|
|
3713
4218
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -4023,7 +4528,10 @@ var SessionParkManager = class {
|
|
|
4023
4528
|
}
|
|
4024
4529
|
async listInfo() {
|
|
4025
4530
|
await Promise.all(this.#storeOps.values());
|
|
4026
|
-
return (await this.#options.store.list()).filter((record) => this.#options.registry.get(record.id) === void 0).map((record) => record
|
|
4531
|
+
return (await this.#options.store.list()).filter((record) => this.#options.registry.get(record.id) === void 0).map((record) => isDormant(record) ? {
|
|
4532
|
+
...record.info,
|
|
4533
|
+
engineAsleep: true
|
|
4534
|
+
} : record.info);
|
|
4027
4535
|
}
|
|
4028
4536
|
async ensureLive(id) {
|
|
4029
4537
|
const live = this.#options.registry.get(id);
|
|
@@ -4273,7 +4781,7 @@ var SessionParkManager = class {
|
|
|
4273
4781
|
}
|
|
4274
4782
|
};
|
|
4275
4783
|
function idleRecordBase(runner, config) {
|
|
4276
|
-
const info = runner.info();
|
|
4784
|
+
const { engineAsleep: _engineAsleep, ...info } = runner.info();
|
|
4277
4785
|
return {
|
|
4278
4786
|
id: runner.id,
|
|
4279
4787
|
info: {
|
|
@@ -4338,6 +4846,8 @@ var ProfileService = class {
|
|
|
4338
4846
|
if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
|
|
4339
4847
|
} else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
|
|
4340
4848
|
if (disableBypassPermissions && p.defaults?.permissionMode === "bypassPermissions") return `profile '${p.name}' defaults to bypassPermissions but disableBypassPermissions is set`;
|
|
4849
|
+
const efforts = p.defaults?.efforts;
|
|
4850
|
+
if (efforts !== void 0 && !isEffortMap(efforts)) return `profile '${p.name}' defaults.efforts must map model names to effort levels`;
|
|
4341
4851
|
const fallbackMode = p.defaults?.permissionMode;
|
|
4342
4852
|
if (fallbackMode && !supportsPermissionMode(p.engine, fallbackMode)) return `profile '${p.name}' defaults to permission mode '${fallbackMode}', which engine '${engineOf(p)}' does not support (supported: ${adapterFor(engineOf(p)).capabilities.permissionModes.join(", ")})`;
|
|
4343
4853
|
return null;
|
|
@@ -4439,6 +4949,9 @@ var ProfileService = class {
|
|
|
4439
4949
|
};
|
|
4440
4950
|
}
|
|
4441
4951
|
};
|
|
4952
|
+
function isEffortMap(value) {
|
|
4953
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.entries(value).every(([model, effort]) => model !== "" && typeof effort === "string" && effort !== "");
|
|
4954
|
+
}
|
|
4442
4955
|
//#endregion
|
|
4443
4956
|
//#region src/services/profile-usage.ts
|
|
4444
4957
|
var ProfileUsageTracker = class {
|
|
@@ -4783,264 +5296,6 @@ function createRelayLink(options, peers, log) {
|
|
|
4783
5296
|
};
|
|
4784
5297
|
}
|
|
4785
5298
|
//#endregion
|
|
4786
|
-
//#region src/services/peers.ts
|
|
4787
|
-
const DEFAULT_PER_MINUTE = 10;
|
|
4788
|
-
const DEFAULT_MAX_HOPS = 12;
|
|
4789
|
-
const WINDOW_MS = 6e4;
|
|
4790
|
-
function visible(from, to) {
|
|
4791
|
-
return to.id !== from.id && scopeMatches(from.scope, to.scope);
|
|
4792
|
-
}
|
|
4793
|
-
function relayEntry(info, live) {
|
|
4794
|
-
const items = info.checklist;
|
|
4795
|
-
return {
|
|
4796
|
-
id: info.id,
|
|
4797
|
-
engine: info.engine,
|
|
4798
|
-
status: info.status,
|
|
4799
|
-
title: info.title,
|
|
4800
|
-
project: info.project ? {
|
|
4801
|
-
name: info.project.name,
|
|
4802
|
-
root: info.project.root
|
|
4803
|
-
} : void 0,
|
|
4804
|
-
cwd: info.cwd,
|
|
4805
|
-
profile: info.profile,
|
|
4806
|
-
model: info.model,
|
|
4807
|
-
permissionMode: info.permissionMode,
|
|
4808
|
-
contextUsage: info.contextUsage,
|
|
4809
|
-
checklist: items?.length ? {
|
|
4810
|
-
done: items.filter((item) => item.status === "completed").length,
|
|
4811
|
-
total: items.length
|
|
4812
|
-
} : void 0,
|
|
4813
|
-
numTurns: info.numTurns,
|
|
4814
|
-
createdAt: info.createdAt,
|
|
4815
|
-
lastActivityAt: info.lastActivityAt,
|
|
4816
|
-
pendingPermissionCount: info.pendingPermissionCount,
|
|
4817
|
-
live
|
|
4818
|
-
};
|
|
4819
|
-
}
|
|
4820
|
-
function createPeerService(deps) {
|
|
4821
|
-
const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
|
|
4822
|
-
const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
|
|
4823
|
-
const maxHops = deps.options?.maxHops ?? DEFAULT_MAX_HOPS;
|
|
4824
|
-
const sent = /* @__PURE__ */ new Map();
|
|
4825
|
-
const inbound = /* @__PURE__ */ new Map();
|
|
4826
|
-
const registry = () => {
|
|
4827
|
-
const found = deps.refs.registry;
|
|
4828
|
-
if (!found) throw new Error("peer messaging is not ready");
|
|
4829
|
-
return found;
|
|
4830
|
-
};
|
|
4831
|
-
const allSessions = async () => {
|
|
4832
|
-
const live = registry().list();
|
|
4833
|
-
const dormant = await deps.refs.parking?.listInfo() ?? [];
|
|
4834
|
-
return [...live, ...dormant].map((info) => deps.projects.withProject(info));
|
|
4835
|
-
};
|
|
4836
|
-
const infoOf = async (id) => {
|
|
4837
|
-
const live = registry().get(id)?.info();
|
|
4838
|
-
if (live) return deps.projects.withProject(live);
|
|
4839
|
-
const record = await deps.refs.parking?.get(id);
|
|
4840
|
-
return record ? deps.projects.withProject(record.info) : void 0;
|
|
4841
|
-
};
|
|
4842
|
-
const sender = async (from) => {
|
|
4843
|
-
const info = await infoOf(from);
|
|
4844
|
-
if (!info) throw new Error(`unknown sender session: ${from}`);
|
|
4845
|
-
return info;
|
|
4846
|
-
};
|
|
4847
|
-
const recentOf = (sessionId, recent) => {
|
|
4848
|
-
const limit = Math.min(Math.max(recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
|
|
4849
|
-
const runner = registry().get(sessionId);
|
|
4850
|
-
const events = [];
|
|
4851
|
-
if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
|
|
4852
|
-
truncateResults: true,
|
|
4853
|
-
imageRefs: true
|
|
4854
|
-
})();
|
|
4855
|
-
return {
|
|
4856
|
-
runner,
|
|
4857
|
-
lines: recentLines(events, limit)
|
|
4858
|
-
};
|
|
4859
|
-
};
|
|
4860
|
-
const underRateLimit = (from, to) => {
|
|
4861
|
-
const key = `${from}->${to}`;
|
|
4862
|
-
const now = Date.now();
|
|
4863
|
-
const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
|
|
4864
|
-
if (recent.length >= perMinute) {
|
|
4865
|
-
sent.set(key, recent);
|
|
4866
|
-
return false;
|
|
4867
|
-
}
|
|
4868
|
-
recent.push(now);
|
|
4869
|
-
sent.set(key, recent);
|
|
4870
|
-
return true;
|
|
4871
|
-
};
|
|
4872
|
-
const list = async (from) => {
|
|
4873
|
-
const me = await sender(from);
|
|
4874
|
-
return (await allSessions()).filter((info) => visible(me, info)).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map(peerSummary);
|
|
4875
|
-
};
|
|
4876
|
-
const peek = async (from, sessionId, options) => {
|
|
4877
|
-
const me = await sender(from);
|
|
4878
|
-
const info = await infoOf(sessionId);
|
|
4879
|
-
if (!info || !visible(me, info)) return;
|
|
4880
|
-
const { runner, lines } = recentOf(sessionId, options?.recent);
|
|
4881
|
-
return {
|
|
4882
|
-
...peerSummary(info),
|
|
4883
|
-
live: runner !== void 0,
|
|
4884
|
-
checklist: info.checklist,
|
|
4885
|
-
pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
|
|
4886
|
-
recent: lines
|
|
4887
|
-
};
|
|
4888
|
-
};
|
|
4889
|
-
const send = async (from, sessionId, text, options) => {
|
|
4890
|
-
const me = await sender(from);
|
|
4891
|
-
if (sessionId === from) return {
|
|
4892
|
-
delivered: false,
|
|
4893
|
-
reason: "that is this session"
|
|
4894
|
-
};
|
|
4895
|
-
if (text.length > maxChars) return {
|
|
4896
|
-
delivered: false,
|
|
4897
|
-
reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
|
|
4898
|
-
};
|
|
4899
|
-
const target = await infoOf(sessionId);
|
|
4900
|
-
if (!target || !visible(me, target)) return {
|
|
4901
|
-
delivered: false,
|
|
4902
|
-
reason: `no such session: ${sessionId}`
|
|
4903
|
-
};
|
|
4904
|
-
const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
|
|
4905
|
-
return deliver(target, text, {
|
|
4906
|
-
kind: "peer",
|
|
4907
|
-
sessionId: from,
|
|
4908
|
-
name: me.title,
|
|
4909
|
-
engine: me.engine,
|
|
4910
|
-
hops
|
|
4911
|
-
}, from);
|
|
4912
|
-
};
|
|
4913
|
-
const deliver = async (target, text, origin, rateKey) => {
|
|
4914
|
-
const sessionId = target.id;
|
|
4915
|
-
if (target.status === "closed" || target.status === "failed") return {
|
|
4916
|
-
delivered: false,
|
|
4917
|
-
reason: `session ${sessionId} is ${target.status}`
|
|
4918
|
-
};
|
|
4919
|
-
const hops = origin.hops ?? [];
|
|
4920
|
-
if (hops.length > maxHops) return {
|
|
4921
|
-
delivered: false,
|
|
4922
|
-
reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
|
|
4923
|
-
};
|
|
4924
|
-
if (!underRateLimit(rateKey, sessionId)) return {
|
|
4925
|
-
delivered: false,
|
|
4926
|
-
reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
|
|
4927
|
-
};
|
|
4928
|
-
const runner = await deps.refs.parking?.ensureLive(sessionId) ?? registry().get(sessionId);
|
|
4929
|
-
if (!runner) return {
|
|
4930
|
-
delivered: false,
|
|
4931
|
-
reason: `session ${sessionId} could not be woken`
|
|
4932
|
-
};
|
|
4933
|
-
const before = runner.info().status;
|
|
4934
|
-
try {
|
|
4935
|
-
runner.sendMessage(text, void 0, { origin });
|
|
4936
|
-
} catch (error) {
|
|
4937
|
-
return {
|
|
4938
|
-
delivered: false,
|
|
4939
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
4940
|
-
};
|
|
4941
|
-
}
|
|
4942
|
-
inbound.set(sessionId, hops);
|
|
4943
|
-
return {
|
|
4944
|
-
delivered: true,
|
|
4945
|
-
sessionId,
|
|
4946
|
-
name: target.title,
|
|
4947
|
-
queued: before === "running" || before === "awaiting_approval"
|
|
4948
|
-
};
|
|
4949
|
-
};
|
|
4950
|
-
const exposedInfo = async (sessionId, exposed) => {
|
|
4951
|
-
const info = await infoOf(sessionId);
|
|
4952
|
-
return info && info.status !== "closed" && scopeMatches(exposed, info.scope) ? info : void 0;
|
|
4953
|
-
};
|
|
4954
|
-
const relayEntries = async (exposed) => {
|
|
4955
|
-
const live = new Set(registry().list().map((info) => info.id));
|
|
4956
|
-
return (await allSessions()).filter((info) => info.status !== "closed" && scopeMatches(exposed, info.scope)).map((info) => relayEntry(info, live.has(info.id)));
|
|
4957
|
-
};
|
|
4958
|
-
const relayPeek = async (sessionId, recent, exposed) => {
|
|
4959
|
-
const info = await exposedInfo(sessionId, exposed);
|
|
4960
|
-
if (!info) return;
|
|
4961
|
-
const { runner, lines } = recentOf(sessionId, recent);
|
|
4962
|
-
return {
|
|
4963
|
-
...relayEntry(info, runner !== void 0),
|
|
4964
|
-
checklistItems: info.checklist,
|
|
4965
|
-
pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
|
|
4966
|
-
recent: lines
|
|
4967
|
-
};
|
|
4968
|
-
};
|
|
4969
|
-
const relaySend = async (origin, sessionId, text, exposed) => {
|
|
4970
|
-
if (text.length === 0 || text.length > maxChars) return {
|
|
4971
|
-
delivered: false,
|
|
4972
|
-
reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
|
|
4973
|
-
};
|
|
4974
|
-
const target = await exposedInfo(sessionId, exposed);
|
|
4975
|
-
if (!target) return {
|
|
4976
|
-
delivered: false,
|
|
4977
|
-
reason: `no such session: ${sessionId}`
|
|
4978
|
-
};
|
|
4979
|
-
const from = `${origin.gateway}:${origin.sessionId}`;
|
|
4980
|
-
return deliver(target, text, {
|
|
4981
|
-
kind: "peer",
|
|
4982
|
-
sessionId: from,
|
|
4983
|
-
hostId: origin.gateway,
|
|
4984
|
-
name: origin.name,
|
|
4985
|
-
engine: origin.engine,
|
|
4986
|
-
hops: origin.hops
|
|
4987
|
-
}, from);
|
|
4988
|
-
};
|
|
4989
|
-
const mentions = async (from, text) => {
|
|
4990
|
-
const tokens = scanPeerMentions(text);
|
|
4991
|
-
if (tokens.length === 0) return [];
|
|
4992
|
-
const rows = await list(from);
|
|
4993
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
4994
|
-
const add = (key, row) => {
|
|
4995
|
-
const held = byKey.get(key);
|
|
4996
|
-
if (held) held.push(row);
|
|
4997
|
-
else byKey.set(key, [row]);
|
|
4998
|
-
};
|
|
4999
|
-
for (const row of rows) {
|
|
5000
|
-
add(peerMentionKey(peerMentionSlug(row.title, row.id)), row);
|
|
5001
|
-
add(peerMentionKey(row.id), row);
|
|
5002
|
-
}
|
|
5003
|
-
const resolved = [];
|
|
5004
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5005
|
-
for (const token of tokens) {
|
|
5006
|
-
const key = peerMentionKey(token.body);
|
|
5007
|
-
const exact = byKey.get(key);
|
|
5008
|
-
const matches = exact ?? (key.length >= 4 ? rows.filter((row) => row.id.startsWith(key)) : []);
|
|
5009
|
-
const target = matches[0];
|
|
5010
|
-
if (!target || matches.length > 1 && !exact) continue;
|
|
5011
|
-
if (seen.has(target.id)) continue;
|
|
5012
|
-
seen.add(target.id);
|
|
5013
|
-
const others = matches.slice(1, 4).map((row) => row.id);
|
|
5014
|
-
resolved.push({
|
|
5015
|
-
typed: token.body,
|
|
5016
|
-
id: target.id,
|
|
5017
|
-
name: target.title,
|
|
5018
|
-
engine: target.engine,
|
|
5019
|
-
status: target.status,
|
|
5020
|
-
cwd: target.cwd,
|
|
5021
|
-
...others.length ? { ambiguousWith: others } : {}
|
|
5022
|
-
});
|
|
5023
|
-
if (resolved.length >= PEER_MENTION_MAX) break;
|
|
5024
|
-
}
|
|
5025
|
-
return resolved;
|
|
5026
|
-
};
|
|
5027
|
-
const watch = (runner) => runner.subscribe((event) => {
|
|
5028
|
-
if (event.type === "user_message" && !event.origin && !event.synthetic && event.parentToolUseId == null && !event.replay) inbound.delete(runner.id);
|
|
5029
|
-
}, runner.info().lastSeq);
|
|
5030
|
-
return {
|
|
5031
|
-
list,
|
|
5032
|
-
peek,
|
|
5033
|
-
send,
|
|
5034
|
-
mentions,
|
|
5035
|
-
watch,
|
|
5036
|
-
relaySender: sender,
|
|
5037
|
-
relayChain: (from) => [...inbound.get(from) ?? [], from],
|
|
5038
|
-
relayEntries,
|
|
5039
|
-
relayPeek,
|
|
5040
|
-
relaySend
|
|
5041
|
-
};
|
|
5042
|
-
}
|
|
5043
|
-
//#endregion
|
|
5044
5299
|
//#region src/services/registry.ts
|
|
5045
5300
|
var SessionRegistry = class {
|
|
5046
5301
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -5262,8 +5517,16 @@ function createSessionFactory(deps) {
|
|
|
5262
5517
|
...deps.peers ? { peers: deps.peers } : {},
|
|
5263
5518
|
...deps.pricing ? { pricing: deps.pricing } : {}
|
|
5264
5519
|
};
|
|
5520
|
+
delete config.contextReset;
|
|
5265
5521
|
delete config.shells;
|
|
5266
5522
|
delete config.shellAgentWrite;
|
|
5523
|
+
delete config.effortDefaults;
|
|
5524
|
+
const efforts = {
|
|
5525
|
+
...deps.effortDefaults,
|
|
5526
|
+
...profile?.defaults?.efforts
|
|
5527
|
+
};
|
|
5528
|
+
if (Object.keys(efforts).length > 0) config.effortDefaults = efforts;
|
|
5529
|
+
if (deps.contextReset && (built.agentContextReset ?? profile?.defaults?.agentContextReset ?? deps.contextReset.defaultEnabled)) config.contextReset = deps.contextReset.directory;
|
|
5267
5530
|
if (deps.shells && capabilities.hostCwd === true) {
|
|
5268
5531
|
config.shells = deps.shells;
|
|
5269
5532
|
const write = deps.shellAgentWrite ?? "read-only";
|
|
@@ -5413,6 +5676,17 @@ function createWorkerServer(options = {}) {
|
|
|
5413
5676
|
onError: options.notifications?.onError ?? ((error, context) => diagnose(error, `notification-${context.op}`))
|
|
5414
5677
|
});
|
|
5415
5678
|
const producedFiles = new ProducedFileStore();
|
|
5679
|
+
const engineSleep = new EngineSleepTimers({
|
|
5680
|
+
afterMs: options.engineSleepAfterMs ?? 0,
|
|
5681
|
+
attachedCount: (sessionId) => bridge.attachedCount(sessionId),
|
|
5682
|
+
onError: (error) => diagnose(error, "engine-sleep")
|
|
5683
|
+
});
|
|
5684
|
+
const contextResets = options.agentContextReset === false ? void 0 : new ContextResetService({
|
|
5685
|
+
...options.agentContextReset,
|
|
5686
|
+
onError: (error) => diagnose(error, "context-reset")
|
|
5687
|
+
});
|
|
5688
|
+
let ownContextResets = contextResets;
|
|
5689
|
+
installContextResetDirectory(contextResets);
|
|
5416
5690
|
const registry = new SessionRegistry({ onRegister: (runner) => {
|
|
5417
5691
|
const detachers = [
|
|
5418
5692
|
notifier.watch(runner),
|
|
@@ -5421,7 +5695,9 @@ function createWorkerServer(options = {}) {
|
|
|
5421
5695
|
spendLedger.watch(runner),
|
|
5422
5696
|
shells?.watch(runner),
|
|
5423
5697
|
peers?.watch(runner),
|
|
5424
|
-
factory.watchAuthSource(runner)
|
|
5698
|
+
factory.watchAuthSource(runner),
|
|
5699
|
+
engineSleep.watch(runner),
|
|
5700
|
+
contextResets?.watch(runner)
|
|
5425
5701
|
];
|
|
5426
5702
|
const profile = runner.info().profile;
|
|
5427
5703
|
if (profile) detachers.push(runner.subscribe((event) => {
|
|
@@ -5467,6 +5743,7 @@ function createWorkerServer(options = {}) {
|
|
|
5467
5743
|
let ownShells = shellDirectory;
|
|
5468
5744
|
if (ownPeers) installPeerDirectory(ownPeers);
|
|
5469
5745
|
installShellDirectory(shellDirectory);
|
|
5746
|
+
if (options.effortDefaults !== void 0 && !isEffortMap(options.effortDefaults)) throw new Error("createWorkerServer: `effortDefaults` must map model names to effort levels");
|
|
5470
5747
|
const factory = createSessionFactory({
|
|
5471
5748
|
adapterFor,
|
|
5472
5749
|
profiles,
|
|
@@ -5475,8 +5752,13 @@ function createWorkerServer(options = {}) {
|
|
|
5475
5752
|
allowedCwdRoots: options.allowedCwdRoots,
|
|
5476
5753
|
disableBypassPermissions: options.disableBypassPermissions,
|
|
5477
5754
|
approvalTimeoutMs: options.approvalTimeoutMs,
|
|
5755
|
+
effortDefaults: options.effortDefaults,
|
|
5478
5756
|
requireApiKey: options.requireApiKey,
|
|
5479
5757
|
peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
|
|
5758
|
+
contextReset: contextResets ? {
|
|
5759
|
+
directory: contextResetDirectoryHandle(() => ownContextResets),
|
|
5760
|
+
defaultEnabled: contextResets.defaultEnabled
|
|
5761
|
+
} : void 0,
|
|
5480
5762
|
shells: shellDirectory ? shellDirectoryHandle(() => ownShells) : void 0,
|
|
5481
5763
|
shellAgentWrite: options.shell?.agentWrite,
|
|
5482
5764
|
pricing: pricing.pricing,
|
|
@@ -5532,7 +5814,8 @@ function createWorkerServer(options = {}) {
|
|
|
5532
5814
|
factory,
|
|
5533
5815
|
registry,
|
|
5534
5816
|
parking,
|
|
5535
|
-
|
|
5817
|
+
engineSleep,
|
|
5818
|
+
peers: peers ? peerDirectoryHandle(() => ownPeers) : void 0,
|
|
5536
5819
|
bridge,
|
|
5537
5820
|
projects,
|
|
5538
5821
|
queue,
|
|
@@ -5579,12 +5862,15 @@ function createWorkerServer(options = {}) {
|
|
|
5579
5862
|
parking,
|
|
5580
5863
|
shells,
|
|
5581
5864
|
queue,
|
|
5865
|
+
engineSleep,
|
|
5582
5866
|
closeQueueSockets: queueSockets.clear,
|
|
5583
5867
|
diagnose,
|
|
5584
5868
|
releaseDirectories: () => {
|
|
5585
5869
|
relay?.close();
|
|
5586
5870
|
ownPeers = void 0;
|
|
5587
5871
|
ownShells = void 0;
|
|
5872
|
+
contextResets?.close();
|
|
5873
|
+
ownContextResets = void 0;
|
|
5588
5874
|
}
|
|
5589
5875
|
});
|
|
5590
5876
|
return {
|
|
@@ -5684,7 +5970,8 @@ function rebuildRunner(factory, record) {
|
|
|
5684
5970
|
resume: record.sdkSessionId
|
|
5685
5971
|
}),
|
|
5686
5972
|
epoch: (record.info.epoch ?? 0) + 1,
|
|
5687
|
-
fallbackTitle: record.info.title
|
|
5973
|
+
fallbackTitle: record.info.title,
|
|
5974
|
+
startAsleep: true
|
|
5688
5975
|
}, void 0, record.id);
|
|
5689
5976
|
}
|
|
5690
5977
|
//#endregion
|