@workerdeck/server 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
1
2
  import { createServer } from "node:http";
2
3
  import { WebSocketServer } from "ws";
3
- import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SessionRunner, attachmentKind, checkClaudeAuth, createEngineSession, getEngineAdapter, installPeerDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines } from "@workerdeck/core";
4
+ import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SessionRunner, attachmentKind, checkClaudeAuth, clampShellTail, createEngineSession, getEngineAdapter, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellSummary, shellTail, ttyText } from "@workerdeck/core";
4
5
  import { JobQueue } from "@workerdeck/queue";
5
- import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_COMMAND_MAX, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
6
- import { createHash, randomUUID } from "node:crypto";
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_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
7
+ import { arch, homedir, hostname, platform, tmpdir } from "node:os";
7
8
  import { closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
8
- import { constants as constants$1, homedir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
11
- import { spawn } from "node:child_process";
10
+ import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
11
+ import { spawnSync } from "node:child_process";
12
12
  //#region src/lib/http.ts
13
13
  const CONTENT_TYPES = {
14
14
  json: "application/json; charset=utf-8",
@@ -61,6 +61,18 @@ function contentTypeFor(filename) {
61
61
  return CONTENT_TYPES[ext] ?? "text/plain; charset=utf-8";
62
62
  }
63
63
  //#endregion
64
+ //#region src/lib/machine-id.ts
65
+ let cached;
66
+ function machineId() {
67
+ cached ??= createHash("sha256").update([
68
+ hostname(),
69
+ platform(),
70
+ arch(),
71
+ homedir()
72
+ ].join("\0")).digest("hex").slice(0, 32);
73
+ return cached;
74
+ }
75
+ //#endregion
64
76
  //#region src/lib/profile-env.ts
65
77
  function isProviderProfile(profile) {
66
78
  return profile.engine === "provider";
@@ -183,6 +195,16 @@ function parseSessionRoute(basePath, url) {
183
195
  produced: true,
184
196
  producedFileId: parts[2] === void 0 ? void 0 : decodeURIComponent(parts[2])
185
197
  };
198
+ if (parts[1] === "shells" && parts.length <= 4) {
199
+ const action = parts[3];
200
+ if (action !== void 0 && action !== "output" && action !== "kill") return null;
201
+ return {
202
+ id: decodeURIComponent(parts[0]),
203
+ shells: true,
204
+ shellId: parts[2] === void 0 ? void 0 : decodeURIComponent(parts[2]),
205
+ shellAction: action
206
+ };
207
+ }
186
208
  if (parts.length === 3 && parts[1] === "project" && parts[2] === "icon") return {
187
209
  id: decodeURIComponent(parts[0]),
188
210
  projectIcon: true
@@ -763,6 +785,7 @@ const HOST_ONLY_KEYS = new Set(Object.keys({
763
785
  historyFn: true,
764
786
  sessionInfoFn: true,
765
787
  peers: true,
788
+ shells: true,
766
789
  connectFn: true,
767
790
  codexHome: true,
768
791
  codexPathOverride: true,
@@ -1110,7 +1133,7 @@ function createFileSessionStore(options = {}) {
1110
1133
  try {
1111
1134
  raw = await readFile(path, "utf8");
1112
1135
  } catch (error) {
1113
- if (isMissing(error)) return null;
1136
+ if (isMissing$1(error)) return null;
1114
1137
  options.onError?.(error, {
1115
1138
  path,
1116
1139
  op: "read"
@@ -1165,7 +1188,7 @@ function createFileSessionStore(options = {}) {
1165
1188
  try {
1166
1189
  names = await readdir(dir);
1167
1190
  } catch (error) {
1168
- if (isMissing(error)) return [];
1191
+ if (isMissing$1(error)) return [];
1169
1192
  options.onError?.(error, {
1170
1193
  path: dir,
1171
1194
  op: "read"
@@ -1189,7 +1212,7 @@ function createFileSessionStore(options = {}) {
1189
1212
  await rm(path);
1190
1213
  return true;
1191
1214
  } catch (error) {
1192
- if (isMissing(error)) return false;
1215
+ if (isMissing$1(error)) return false;
1193
1216
  options.onError?.(error, {
1194
1217
  path,
1195
1218
  op: "delete"
@@ -1199,7 +1222,7 @@ function createFileSessionStore(options = {}) {
1199
1222
  }
1200
1223
  };
1201
1224
  }
1202
- function isMissing(error) {
1225
+ function isMissing$1(error) {
1203
1226
  return error.code === "ENOENT";
1204
1227
  }
1205
1228
  function parseRecord(value) {
@@ -1359,17 +1382,19 @@ const DEFAULT_TTL_MS = 3e4;
1359
1382
  const SWEEP_ABOVE = 256;
1360
1383
  var ProjectInfoService = class {
1361
1384
  #ttlMs;
1385
+ #decorate;
1362
1386
  #byCwd = /* @__PURE__ */ new Map();
1363
1387
  constructor(options = {}) {
1364
1388
  this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
1389
+ this.#decorate = options.decorate ?? ((info) => info);
1365
1390
  }
1366
1391
  withProject(info) {
1367
- if (!info.cwd) return info;
1392
+ if (!info.cwd) return this.#decorate(info);
1368
1393
  const project = this.#resolve(info.cwd).project;
1369
- return project ? {
1394
+ return this.#decorate(project ? {
1370
1395
  ...info,
1371
1396
  project
1372
- } : info;
1397
+ } : info);
1373
1398
  }
1374
1399
  iconFor(cwd) {
1375
1400
  if (!cwd) return;
@@ -1518,6 +1543,1062 @@ function handleProjectIcon(projects, req, res, cwd) {
1518
1543
  res.end(read.data);
1519
1544
  }
1520
1545
  //#endregion
1546
+ //#region src/services/process-tree.ts
1547
+ const PS_ARGS = ["-eo", "pid=,ppid=,pgid="];
1548
+ const PS_TIMEOUT_MS = 2e3;
1549
+ const PS_MAX_BUFFER = 67108864;
1550
+ const MAX_ROUNDS = 5;
1551
+ function readProcessTable() {
1552
+ const result = spawnSync("ps", PS_ARGS, {
1553
+ encoding: "utf8",
1554
+ timeout: PS_TIMEOUT_MS,
1555
+ maxBuffer: PS_MAX_BUFFER,
1556
+ stdio: [
1557
+ "ignore",
1558
+ "pipe",
1559
+ "ignore"
1560
+ ]
1561
+ });
1562
+ if (result.error || result.status !== 0 || typeof result.stdout !== "string") return null;
1563
+ return parseProcessTable(result.stdout);
1564
+ }
1565
+ function parseProcessTable(text) {
1566
+ const rows = [];
1567
+ for (const line of text.split("\n")) {
1568
+ const fields = line.trim().split(/\s+/);
1569
+ if (fields.length !== 3) continue;
1570
+ const [pid, ppid, pgid] = fields.map(Number);
1571
+ if (Number.isInteger(pid) && Number.isInteger(ppid) && Number.isInteger(pgid)) rows.push({
1572
+ pid,
1573
+ ppid,
1574
+ pgid
1575
+ });
1576
+ }
1577
+ return rows;
1578
+ }
1579
+ function killProcessTrees(roots, deps) {
1580
+ const { self } = deps;
1581
+ const signal = (pid, name) => {
1582
+ try {
1583
+ deps.signal(pid, name);
1584
+ return true;
1585
+ } catch {
1586
+ return false;
1587
+ }
1588
+ };
1589
+ const owned = (pid) => Number.isInteger(pid) && pid > 1 && pid !== self;
1590
+ const live = new Set(roots.filter(owned));
1591
+ const closure = /* @__PURE__ */ new Map();
1592
+ const rootRows = /* @__PURE__ */ new Map();
1593
+ const foreign = [];
1594
+ let scanned = false;
1595
+ let selfPgid;
1596
+ for (let round = 0; round < MAX_ROUNDS; round++) {
1597
+ const table = readTable(deps.table);
1598
+ if (table === null) break;
1599
+ const byPid = /* @__PURE__ */ new Map();
1600
+ const byParent = /* @__PURE__ */ new Map();
1601
+ for (const row of table) {
1602
+ byPid.set(row.pid, row);
1603
+ const siblings = byParent.get(row.ppid);
1604
+ if (siblings) siblings.push(row);
1605
+ else byParent.set(row.ppid, [row]);
1606
+ }
1607
+ let grew = false;
1608
+ if (!scanned) {
1609
+ scanned = true;
1610
+ selfPgid = byPid.get(self)?.pgid;
1611
+ for (const root of live) {
1612
+ const row = byPid.get(root);
1613
+ if (row === void 0) continue;
1614
+ if (row.ppid !== self) {
1615
+ foreign.push(root);
1616
+ live.delete(root);
1617
+ continue;
1618
+ }
1619
+ rootRows.set(root, row);
1620
+ signal(-root, "SIGSTOP");
1621
+ grew = true;
1622
+ }
1623
+ }
1624
+ for (const root of rootRows.keys()) {
1625
+ const queue = [root];
1626
+ const seen = /* @__PURE__ */ new Set([root]);
1627
+ while (queue.length > 0) {
1628
+ const parent = queue.shift();
1629
+ for (const child of byParent.get(parent) ?? []) {
1630
+ if (!owned(child.pid) || seen.has(child.pid)) continue;
1631
+ seen.add(child.pid);
1632
+ queue.push(child.pid);
1633
+ if (!closure.has(child.pid)) {
1634
+ closure.set(child.pid, child);
1635
+ signal(child.pid, "SIGSTOP");
1636
+ grew = true;
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1641
+ if (!grew) break;
1642
+ }
1643
+ const unreached = [];
1644
+ const groups = /* @__PURE__ */ new Set();
1645
+ for (const root of live) {
1646
+ groups.add(root);
1647
+ if (!signal(-root, "SIGKILL")) unreached.push(root);
1648
+ }
1649
+ for (const pid of closure.keys()) signal(pid, "SIGKILL");
1650
+ if (selfPgid !== void 0) for (const row of [...rootRows.values(), ...closure.values()]) {
1651
+ const group = row.pgid;
1652
+ if (group <= 1 || group === self || group === selfPgid || groups.has(group)) continue;
1653
+ groups.add(group);
1654
+ signal(-group, "SIGKILL");
1655
+ }
1656
+ return {
1657
+ scanned,
1658
+ descendants: closure.size,
1659
+ foreign,
1660
+ unreached
1661
+ };
1662
+ }
1663
+ function readTable(table) {
1664
+ try {
1665
+ return table();
1666
+ } catch {
1667
+ return null;
1668
+ }
1669
+ }
1670
+ //#endregion
1671
+ //#region src/services/shells.ts
1672
+ const NOTIFY_MS = 250;
1673
+ const SWEEP_INTERVAL_MS = 36e5;
1674
+ const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
1675
+ const ID_CHARS = 12;
1676
+ const TERM = "xterm-256color";
1677
+ const INDEX_SUFFIX = ".json";
1678
+ const INDEX_VERSION = 1;
1679
+ const CTTY_WRAPPER = "true <>/dev/tty 2>/dev/null; exec \"$0\" -c \"$1\"";
1680
+ let ptyModule;
1681
+ async function loadPty() {
1682
+ if (ptyModule !== void 0) return ptyModule;
1683
+ try {
1684
+ ptyModule = await import("@lydell/node-pty");
1685
+ } catch {
1686
+ ptyModule = null;
1687
+ }
1688
+ return ptyModule;
1689
+ }
1690
+ function createShellDirectory(registry) {
1691
+ return {
1692
+ list: async (from) => registry.list(from).map(shellSummary),
1693
+ read: async (from, shellId, options) => {
1694
+ const info = registry.get(from, shellId);
1695
+ if (!info) return;
1696
+ const text = await registry.output(from, shellId, { view: "text" });
1697
+ if (text === void 0) return;
1698
+ return {
1699
+ shell: shellSummary(info),
1700
+ ...shellTail(text, clampShellTail(options?.tail))
1701
+ };
1702
+ }
1703
+ };
1704
+ }
1705
+ function shellPermitted(shells, runner, operator) {
1706
+ if (shells === null || !operator) return false;
1707
+ const info = runner.info();
1708
+ return (info.capabilities ?? (info.engine ? ENGINE_CAPABILITIES[info.engine] : void 0))?.hostCwd === true;
1709
+ }
1710
+ function shellChildEnv(base) {
1711
+ const env = {};
1712
+ for (const [key, value] of Object.entries(base)) if (value !== void 0) env[key] = value;
1713
+ return env;
1714
+ }
1715
+ function clampSize(size) {
1716
+ return {
1717
+ cols: clamp(size.cols, SHELL_MIN_COLS, SHELL_MAX_COLS),
1718
+ rows: clamp(size.rows, SHELL_MIN_ROWS, SHELL_MAX_ROWS)
1719
+ };
1720
+ }
1721
+ function loginShell(env) {
1722
+ const shell = env.SHELL;
1723
+ return shell && shell.startsWith("/") ? shell : "/bin/sh";
1724
+ }
1725
+ function createShellRegistry(options) {
1726
+ const { generation } = options;
1727
+ const dir = options.artifactDir;
1728
+ const base = dir ?? join(tmpdir(), `workerdeck-shells-${process.pid}`);
1729
+ const limits = {
1730
+ spill: options.spillBytes ?? SHELL_SPILL_BYTES,
1731
+ cap: options.artifactMaxBytes ?? SHELL_ARTIFACT_MAX_BYTES,
1732
+ ring: options.tailRingBytes ?? SHELL_TAIL_RING_BYTES
1733
+ };
1734
+ const tailFlushMs = options.tailFlushMs ?? SHELL_TAIL_FLUSH_MS;
1735
+ const ttlMs = options.artifactTtlMs ?? SHELL_ARTIFACT_TTL_MS;
1736
+ const perSession = options.maxRunningPerSession ?? SHELL_MAX_RUNNING_PER_SESSION;
1737
+ const total = options.maxRunningTotal ?? SHELL_MAX_RUNNING_TOTAL;
1738
+ const sweepIntervalMs = options.sweepIntervalMs ?? SWEEP_INTERVAL_MS;
1739
+ const sessions = /* @__PURE__ */ new Map();
1740
+ const loading = /* @__PURE__ */ new Map();
1741
+ let sweeper;
1742
+ let stopped = false;
1743
+ const report = (error, context) => options.onError?.(error, context);
1744
+ const indexPath = (sessionId) => join(base, `${encodeURIComponent(sessionId)}${INDEX_SUFFIX}`);
1745
+ const pathsFor = (sessionId, shellId) => {
1746
+ const encoded = encodeURIComponent(sessionId);
1747
+ const sessionDir = join(base, encoded);
1748
+ return {
1749
+ dir: sessionDir,
1750
+ raw: join(sessionDir, `${shellId}.raw`),
1751
+ tail: join(sessionDir, `${shellId}.tail.raw`),
1752
+ rel: `${encoded}/${shellId}.raw`
1753
+ };
1754
+ };
1755
+ const find = (sessionId, id) => sessions.get(sessionId)?.entries.get(id);
1756
+ const runningAll = () => [...sessions.values()].flatMap(runningIn);
1757
+ const persist = (state) => {
1758
+ if (!dir || state.pending) return;
1759
+ state.pending = true;
1760
+ state.chain = state.chain.then(async () => {
1761
+ state.pending = false;
1762
+ const payload = JSON.stringify(serialize(state));
1763
+ await mkdir(dir, {
1764
+ recursive: true,
1765
+ mode: 448
1766
+ });
1767
+ const path = indexPath(state.sessionId);
1768
+ const temp = `${path}.${process.pid}.tmp`;
1769
+ await writeFile(temp, payload, { mode: 384 });
1770
+ await rename(temp, path);
1771
+ }).catch((error) => report(error, {
1772
+ op: "index",
1773
+ sessionId: state.sessionId
1774
+ }));
1775
+ };
1776
+ const readIndex = async (sessionId) => {
1777
+ const state = freshSession(sessionId);
1778
+ if (!dir) return state;
1779
+ let raw;
1780
+ try {
1781
+ raw = await readFile(indexPath(sessionId), "utf8");
1782
+ } catch (error) {
1783
+ if (!isMissing(error)) report(error, {
1784
+ op: "index",
1785
+ sessionId
1786
+ });
1787
+ return state;
1788
+ }
1789
+ let index;
1790
+ try {
1791
+ index = parseIndex(JSON.parse(raw));
1792
+ } catch (error) {
1793
+ report(error, {
1794
+ op: "index",
1795
+ sessionId
1796
+ });
1797
+ return state;
1798
+ }
1799
+ state.nextOrdinal = index.nextOrdinal;
1800
+ const now = Date.now();
1801
+ let dirty = false;
1802
+ for (const record of index.shells) {
1803
+ const { generation: recordGeneration, output, artifact, ...info } = record;
1804
+ if (info.status === "running" && recordGeneration !== generation) {
1805
+ info.status = "exited";
1806
+ info.endReason = "server_restarted";
1807
+ info.endedAt = now;
1808
+ delete info.exitCode;
1809
+ delete info.signal;
1810
+ dirty = true;
1811
+ }
1812
+ state.entries.set(info.id, {
1813
+ info,
1814
+ generation: recordGeneration,
1815
+ artifact: new StoredArtifact(pathsFor(sessionId, info.id), info, {
1816
+ output,
1817
+ artifact
1818
+ }),
1819
+ sinks: /* @__PURE__ */ new Set(),
1820
+ listeners: /* @__PURE__ */ new Set()
1821
+ });
1822
+ }
1823
+ if (dirty) persist(state);
1824
+ return state;
1825
+ };
1826
+ const load = (sessionId) => {
1827
+ const held = sessions.get(sessionId);
1828
+ if (held) return Promise.resolve(held);
1829
+ let inflight = loading.get(sessionId);
1830
+ if (!inflight) {
1831
+ inflight = readIndex(sessionId).then((state) => {
1832
+ sessions.set(sessionId, state);
1833
+ loading.delete(sessionId);
1834
+ return state;
1835
+ });
1836
+ loading.set(sessionId, inflight);
1837
+ }
1838
+ return inflight;
1839
+ };
1840
+ const fire = (entry) => {
1841
+ for (const listener of entry.listeners) try {
1842
+ listener();
1843
+ } catch (error) {
1844
+ report(error, {
1845
+ op: "listener",
1846
+ sessionId: entry.info.sessionId,
1847
+ shellId: entry.info.id
1848
+ });
1849
+ }
1850
+ };
1851
+ const scheduleNotify = (entry) => {
1852
+ if (entry.notify) return;
1853
+ entry.notify = setTimeout(() => {
1854
+ entry.notify = void 0;
1855
+ if (entry.info.status === "running") fire(entry);
1856
+ }, NOTIFY_MS);
1857
+ entry.notify.unref();
1858
+ };
1859
+ const settle = (state, entry, reason) => {
1860
+ if (entry.info.status !== "running") return;
1861
+ entry.info.status = "exited";
1862
+ entry.info.endedAt = Date.now();
1863
+ entry.info.endReason = reason;
1864
+ clearTimeout(entry.clock);
1865
+ clearTimeout(entry.notify);
1866
+ clearInterval(entry.tailTimer);
1867
+ entry.clock = entry.notify = entry.tailTimer = void 0;
1868
+ entry.child = void 0;
1869
+ entry.pid = void 0;
1870
+ if (entry.artifact instanceof LiveArtifact) entry.artifact.close();
1871
+ for (const sink of entry.sinks) try {
1872
+ sink.end(reason);
1873
+ } catch {}
1874
+ entry.sinks.clear();
1875
+ persist(state);
1876
+ fire(entry);
1877
+ };
1878
+ const treeDeps = {
1879
+ table: options.processTable ?? readProcessTable,
1880
+ signal: (pid, name) => void process.kill(pid, name),
1881
+ self: process.pid
1882
+ };
1883
+ const killTrees = (entries) => {
1884
+ const live = entries.flatMap((entry) => entry.pid === void 0 ? [] : [{
1885
+ entry,
1886
+ pid: entry.pid
1887
+ }]);
1888
+ if (live.length === 0) return;
1889
+ const result = killProcessTrees(live.map(({ pid }) => pid), treeDeps);
1890
+ for (const { entry, pid } of live) {
1891
+ if (result.unreached.includes(pid)) try {
1892
+ entry.child?.kill("SIGKILL");
1893
+ } catch {}
1894
+ const context = {
1895
+ op: "kill",
1896
+ sessionId: entry.info.sessionId,
1897
+ shellId: entry.info.id
1898
+ };
1899
+ if (!result.scanned) report(/* @__PURE__ */ new Error("no process table (ps), only the process group was signalled"), context);
1900
+ else if (result.foreign.includes(pid)) report(/* @__PURE__ */ new Error(`pid ${pid} is no longer this shell's child and was left alone`), context);
1901
+ }
1902
+ };
1903
+ const killEntries = (targets, reason) => {
1904
+ const running = targets.filter(({ entry }) => entry.info.status === "running");
1905
+ killTrees(running.map(({ entry }) => entry));
1906
+ for (const { state, entry } of running) settle(state, entry, reason);
1907
+ return running.length;
1908
+ };
1909
+ const kill = (state, entry, reason) => {
1910
+ killEntries([{
1911
+ state,
1912
+ entry
1913
+ }], reason);
1914
+ };
1915
+ const killSession = (sessionId, reason) => {
1916
+ const state = sessions.get(sessionId);
1917
+ if (!state) return;
1918
+ killEntries(runningIn(state).map((entry) => ({
1919
+ state,
1920
+ entry
1921
+ })), reason);
1922
+ };
1923
+ const spawn = async ({ runner, command, owner }) => {
1924
+ if (stopped) throw new Error("the gateway is shutting down");
1925
+ const sessionId = runner.id;
1926
+ const state = await load(sessionId);
1927
+ const pty = await loadPty();
1928
+ if (!pty) throw new Error(SHELL_REFUSAL);
1929
+ const cwd = runner.info().cwd;
1930
+ if (!(await stat(cwd).catch(() => void 0))?.isDirectory()) throw new Error(`the session's cwd is not a directory: ${cwd}`);
1931
+ const inSession = runningIn(state).length;
1932
+ if (inSession >= perSession) throw new Error(`this session already has ${inSession} shells running (the limit is ${perSession})`);
1933
+ const overall = runningAll().length;
1934
+ if (overall >= total) throw new Error(`the gateway already has ${overall} shells running (the limit is ${total})`);
1935
+ const id = mintShellId();
1936
+ const info = {
1937
+ id,
1938
+ sessionId,
1939
+ ordinal: state.nextOrdinal++,
1940
+ command,
1941
+ label: shellLabel(command),
1942
+ cwd,
1943
+ owner,
1944
+ status: "running",
1945
+ startedAt: Date.now(),
1946
+ bytes: 0,
1947
+ cols: SHELL_COLS,
1948
+ rows: SHELL_ROWS
1949
+ };
1950
+ const artifact = new LiveArtifact(pathsFor(sessionId, id), limits, (error) => report(error, {
1951
+ op: "artifact",
1952
+ sessionId,
1953
+ shellId: id
1954
+ }));
1955
+ const entry = {
1956
+ info,
1957
+ generation,
1958
+ artifact,
1959
+ sinks: /* @__PURE__ */ new Set(),
1960
+ listeners: /* @__PURE__ */ new Set()
1961
+ };
1962
+ state.entries.set(id, entry);
1963
+ const env = shellChildEnv(process.env);
1964
+ env.TERM = TERM;
1965
+ env.COLORTERM = "truecolor";
1966
+ env.PWD = cwd;
1967
+ let child;
1968
+ try {
1969
+ child = pty.spawn("/bin/sh", [
1970
+ "-c",
1971
+ CTTY_WRAPPER,
1972
+ loginShell(process.env),
1973
+ command
1974
+ ], {
1975
+ name: TERM,
1976
+ cols: SHELL_COLS,
1977
+ rows: SHELL_ROWS,
1978
+ cwd,
1979
+ env
1980
+ });
1981
+ } catch (error) {
1982
+ settle(state, entry, "spawn_failed");
1983
+ throw error instanceof Error ? error : /* @__PURE__ */ new Error("failed to start the shell");
1984
+ }
1985
+ entry.child = child;
1986
+ entry.pid = child.pid;
1987
+ child.onData((data) => {
1988
+ if (entry.info.status !== "running") return;
1989
+ const changed = artifact.append(data);
1990
+ entry.info.bytes = artifact.bytes;
1991
+ if (changed) {
1992
+ if (artifact.capped && !entry.info.capped) {
1993
+ entry.info.capped = true;
1994
+ entry.tailTimer = setInterval(() => artifact.writeTail(), tailFlushMs);
1995
+ entry.tailTimer.unref();
1996
+ }
1997
+ persist(state);
1998
+ }
1999
+ for (const sink of entry.sinks) try {
2000
+ sink.write(data);
2001
+ } catch {
2002
+ entry.sinks.delete(sink);
2003
+ }
2004
+ scheduleNotify(entry);
2005
+ });
2006
+ child.onExit(({ exitCode, signal }) => {
2007
+ if (entry.info.status !== "running") return;
2008
+ if (signal) entry.info.signal = signal;
2009
+ else entry.info.exitCode = exitCode;
2010
+ settle(state, entry, "exit");
2011
+ });
2012
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
2013
+ entry.clock = setTimeout(() => kill(state, entry, "timeout"), options.timeoutMs);
2014
+ entry.clock.unref();
2015
+ }
2016
+ persist(state);
2017
+ return {
2018
+ shell: { ...info },
2019
+ source: sourceFor(entry, artifact)
2020
+ };
2021
+ };
2022
+ const sweep = async () => {
2023
+ const now = Date.now();
2024
+ for (const state of sessions.values()) {
2025
+ let dirty = false;
2026
+ for (const entry of state.entries.values()) {
2027
+ const { status, endedAt } = entry.info;
2028
+ if (status !== "exited" || endedAt === void 0 || now - endedAt < ttlMs) continue;
2029
+ state.entries.delete(entry.info.id);
2030
+ dirty = true;
2031
+ try {
2032
+ await entry.artifact.remove();
2033
+ } catch (error) {
2034
+ report(error, {
2035
+ op: "sweep",
2036
+ sessionId: state.sessionId,
2037
+ shellId: entry.info.id
2038
+ });
2039
+ }
2040
+ }
2041
+ if (dirty) persist(state);
2042
+ }
2043
+ };
2044
+ return {
2045
+ spawn,
2046
+ get: (sessionId, shellId) => {
2047
+ const entry = find(sessionId, shellId);
2048
+ return entry ? { ...entry.info } : void 0;
2049
+ },
2050
+ list: (sessionId) => {
2051
+ const state = sessions.get(sessionId);
2052
+ if (!state) return [];
2053
+ return [...state.entries.values()].map((entry) => ({ ...entry.info })).sort((a, b) => b.ordinal - a.ordinal);
2054
+ },
2055
+ running: () => runningAll().map((entry) => ({ ...entry.info })),
2056
+ kill: (sessionId, shellId, reason = "killed") => {
2057
+ const state = sessions.get(sessionId);
2058
+ const entry = state?.entries.get(shellId);
2059
+ if (!state || !entry) return;
2060
+ kill(state, entry, reason);
2061
+ return { ...entry.info };
2062
+ },
2063
+ killAll: (reason) => {
2064
+ stopped = true;
2065
+ if (sweeper) {
2066
+ clearInterval(sweeper);
2067
+ sweeper = void 0;
2068
+ }
2069
+ return killEntries([...sessions.values()].flatMap((state) => runningIn(state).map((entry) => ({
2070
+ state,
2071
+ entry
2072
+ }))), reason);
2073
+ },
2074
+ killAllSync: () => {
2075
+ killTrees(runningAll());
2076
+ },
2077
+ attach: async (sessionId, shellId, sink) => {
2078
+ const entry = find(sessionId, shellId);
2079
+ if (!entry) throw new Error("unknown shell");
2080
+ const pending = [];
2081
+ let live = false;
2082
+ let ended;
2083
+ const proxy = {
2084
+ write: (data) => {
2085
+ if (live) sink.write(data);
2086
+ else pending.push(data);
2087
+ },
2088
+ end: (reason) => {
2089
+ if (live) sink.end(reason);
2090
+ else ended = reason;
2091
+ }
2092
+ };
2093
+ if (entry.info.status === "running") entry.sinks.add(proxy);
2094
+ let replay;
2095
+ try {
2096
+ replay = await entry.artifact.replay(SHELL_ATTACH_REPLAY_BYTES);
2097
+ } catch (error) {
2098
+ entry.sinks.delete(proxy);
2099
+ throw error;
2100
+ }
2101
+ live = true;
2102
+ if (ended !== void 0) {
2103
+ const reason = ended;
2104
+ queueMicrotask(() => sink.end(reason));
2105
+ }
2106
+ return {
2107
+ shell: { ...entry.info },
2108
+ scrollback: replay.toString("utf8") + pending.join(""),
2109
+ detach: () => {
2110
+ entry.sinks.delete(proxy);
2111
+ }
2112
+ };
2113
+ },
2114
+ write: (sessionId, shellId, data) => {
2115
+ const entry = find(sessionId, shellId);
2116
+ if (!entry) throw new Error("unknown shell");
2117
+ if (typeof data !== "string" || data.length > SHELL_INPUT_MAX) throw new Error(`shell input must be a string of at most ${SHELL_INPUT_MAX} characters`);
2118
+ if (!entry.child) throw new Error("the shell has exited");
2119
+ entry.child.write(data);
2120
+ },
2121
+ resize: (sessionId, shellId, size) => {
2122
+ const entry = find(sessionId, shellId);
2123
+ if (!entry) throw new Error("unknown shell");
2124
+ const next = clampSize(size);
2125
+ if (entry.child) {
2126
+ try {
2127
+ entry.child.resize(next.cols, next.rows);
2128
+ } catch {}
2129
+ entry.info.cols = next.cols;
2130
+ entry.info.rows = next.rows;
2131
+ }
2132
+ return next;
2133
+ },
2134
+ output: async (sessionId, shellId, query) => {
2135
+ const entry = find(sessionId, shellId);
2136
+ if (!entry) return;
2137
+ if (query.tail === void 0 && query.view === "text") return entry.artifact.textView();
2138
+ const text = (await entry.artifact.read(query.tail)).toString("utf8");
2139
+ return query.view === "raw" ? text : ttyText(text);
2140
+ },
2141
+ hydrate: async () => {
2142
+ if (dir) {
2143
+ let names = [];
2144
+ try {
2145
+ names = await readdir(dir);
2146
+ } catch (error) {
2147
+ if (!isMissing(error)) report(error, { op: "index" });
2148
+ }
2149
+ for (const name of names) {
2150
+ if (!name.endsWith(INDEX_SUFFIX)) continue;
2151
+ let sessionId;
2152
+ try {
2153
+ sessionId = decodeURIComponent(name.slice(0, -5));
2154
+ } catch {
2155
+ continue;
2156
+ }
2157
+ await load(sessionId);
2158
+ }
2159
+ }
2160
+ await sweep();
2161
+ if (!sweeper && !stopped) {
2162
+ sweeper = setInterval(() => void sweep(), sweepIntervalMs);
2163
+ sweeper.unref();
2164
+ }
2165
+ },
2166
+ sweep,
2167
+ flush: async () => {
2168
+ const states = [...sessions.values()];
2169
+ await Promise.all(states.flatMap((state) => [...state.entries.values()].map((entry) => entry.artifact.idle())));
2170
+ await Promise.all(states.map((state) => state.chain));
2171
+ if (!dir && stopped) await rm(base, {
2172
+ recursive: true,
2173
+ force: true
2174
+ }).catch(() => {});
2175
+ },
2176
+ decorate: (info) => {
2177
+ const state = sessions.get(info.id);
2178
+ if (!state) return info;
2179
+ const now = Date.now();
2180
+ const shells = [...state.entries.values()].filter((entry) => {
2181
+ const { status, exitCode, endedAt } = entry.info;
2182
+ return status === "running" || typeof exitCode === "number" && exitCode !== 0 && endedAt !== void 0 && now - endedAt < SHELL_LINGER_MS;
2183
+ }).map((entry) => ({ ...entry.info }));
2184
+ return shells.length > 0 ? {
2185
+ ...info,
2186
+ shells
2187
+ } : info;
2188
+ },
2189
+ watch: (runner) => runner.subscribe((event) => {
2190
+ if (event.type === "session_closed" || event.type === "status_changed" && event.status === "parked") killSession(runner.id, "killed");
2191
+ }, runner.info().lastSeq)
2192
+ };
2193
+ }
2194
+ var LiveArtifact = class {
2195
+ #paths;
2196
+ #limits;
2197
+ #onError;
2198
+ #ring;
2199
+ #ringAt = 0;
2200
+ #ringLen = 0;
2201
+ #chunks = [];
2202
+ #chunkBytes = 0;
2203
+ #bytes = 0;
2204
+ #spilled = false;
2205
+ #capped = false;
2206
+ #closed = false;
2207
+ #broken = false;
2208
+ #io = Promise.resolve();
2209
+ #handle;
2210
+ #done = "";
2211
+ #pending = "";
2212
+ #textCache;
2213
+ constructor(paths, limits, onError) {
2214
+ this.#paths = paths;
2215
+ this.#limits = limits;
2216
+ this.#onError = onError;
2217
+ this.#ring = Buffer.alloc(Math.max(0, limits.ring));
2218
+ }
2219
+ get bytes() {
2220
+ return this.#bytes;
2221
+ }
2222
+ get capped() {
2223
+ return this.#capped;
2224
+ }
2225
+ append(data) {
2226
+ const chunk = Buffer.from(data, "utf8");
2227
+ const before = this.#bytes;
2228
+ this.#bytes += chunk.length;
2229
+ this.#ringPush(chunk);
2230
+ this.#textCache = void 0;
2231
+ if (this.#capped) return false;
2232
+ const room = Math.max(0, this.#limits.cap - before);
2233
+ const kept = chunk.length <= room ? chunk : chunk.subarray(0, room);
2234
+ this.#textAppend(kept === chunk ? data : kept.toString("utf8"));
2235
+ let changed = false;
2236
+ if (this.#spilled) this.#write(kept);
2237
+ else {
2238
+ this.#chunks.push(kept);
2239
+ this.#chunkBytes += kept.length;
2240
+ if (this.#chunkBytes > this.#limits.spill) {
2241
+ this.#spill();
2242
+ changed = true;
2243
+ }
2244
+ }
2245
+ if (this.#bytes > this.#limits.cap) {
2246
+ this.#capped = true;
2247
+ changed = true;
2248
+ }
2249
+ return changed;
2250
+ }
2251
+ text() {
2252
+ if (this.#textCache === void 0) {
2253
+ let text = this.#done + ttyText(this.#pending);
2254
+ if (this.#capped) {
2255
+ const after = this.#bytes - this.#limits.cap;
2256
+ const kept = Math.min(after, this.#ringLen);
2257
+ if (kept > 0) text += (after > kept ? "\n" : "") + ttyText(this.tail(kept).toString("utf8"));
2258
+ }
2259
+ this.#textCache = text;
2260
+ }
2261
+ return this.#textCache;
2262
+ }
2263
+ textView() {
2264
+ return Promise.resolve(this.text());
2265
+ }
2266
+ tail(n = this.#ringLen) {
2267
+ const size = this.#ring.length;
2268
+ const take = Math.max(0, Math.min(n, this.#ringLen));
2269
+ const out = Buffer.allocUnsafe(take);
2270
+ if (take === 0) return out;
2271
+ const start = (this.#ringAt - take + size) % size;
2272
+ const first = Math.min(take, size - start);
2273
+ this.#ring.copy(out, 0, start, start + first);
2274
+ if (first < take) this.#ring.copy(out, first, 0, take - first);
2275
+ return out;
2276
+ }
2277
+ replay(max) {
2278
+ const want = Math.max(0, Math.floor(max));
2279
+ if (!this.#spilled) {
2280
+ const all = Buffer.concat(this.#chunks, this.#chunkBytes);
2281
+ return Promise.resolve(all.subarray(Math.max(0, all.length - want)));
2282
+ }
2283
+ if (this.#capped || want <= this.#ringLen) return Promise.resolve(this.tail(want));
2284
+ const end = this.#bytes;
2285
+ return this.#io.then(() => readRange(this.#paths.raw, Math.max(0, end - want), end));
2286
+ }
2287
+ async read(tail) {
2288
+ if (tail !== void 0) return this.replay(tail);
2289
+ if (!this.#spilled) return Buffer.concat(this.#chunks, this.#chunkBytes);
2290
+ await this.#io;
2291
+ return readWhole(this.#paths.raw);
2292
+ }
2293
+ stored() {
2294
+ return this.#spilled ? { artifact: this.#paths.rel } : { output: Buffer.concat(this.#chunks, this.#chunkBytes).toString("utf8") };
2295
+ }
2296
+ writeTail() {
2297
+ const tail = this.tail();
2298
+ this.#queue(async () => {
2299
+ await mkdir(this.#paths.dir, {
2300
+ recursive: true,
2301
+ mode: 448
2302
+ });
2303
+ const temp = `${this.#paths.tail}.${process.pid}.tmp`;
2304
+ await writeFile(temp, tail, { mode: 384 });
2305
+ await rename(temp, this.#paths.tail);
2306
+ });
2307
+ }
2308
+ close() {
2309
+ if (this.#closed) return;
2310
+ this.#closed = true;
2311
+ if (this.#capped) this.writeTail();
2312
+ this.#io = this.#io.then(async () => {
2313
+ const handle = this.#handle;
2314
+ this.#handle = void 0;
2315
+ await handle?.close().catch(() => {});
2316
+ });
2317
+ }
2318
+ idle() {
2319
+ return this.#io;
2320
+ }
2321
+ async remove() {
2322
+ this.close();
2323
+ await this.#io;
2324
+ await rm(this.#paths.raw, { force: true });
2325
+ await rm(this.#paths.tail, { force: true });
2326
+ }
2327
+ #textAppend(data) {
2328
+ const newline = data.lastIndexOf("\n");
2329
+ if (newline === -1) {
2330
+ this.#pending += data;
2331
+ return;
2332
+ }
2333
+ this.#done += ttyText(this.#pending + data.slice(0, newline + 1));
2334
+ this.#pending = data.slice(newline + 1);
2335
+ }
2336
+ #ringPush(chunk) {
2337
+ const size = this.#ring.length;
2338
+ if (size === 0 || chunk.length === 0) return;
2339
+ if (chunk.length >= size) {
2340
+ chunk.copy(this.#ring, 0, chunk.length - size);
2341
+ this.#ringAt = 0;
2342
+ this.#ringLen = size;
2343
+ return;
2344
+ }
2345
+ const first = Math.min(chunk.length, size - this.#ringAt);
2346
+ chunk.copy(this.#ring, this.#ringAt, 0, first);
2347
+ if (first < chunk.length) chunk.copy(this.#ring, 0, first);
2348
+ this.#ringAt = (this.#ringAt + chunk.length) % size;
2349
+ this.#ringLen = Math.min(size, this.#ringLen + chunk.length);
2350
+ }
2351
+ #spill() {
2352
+ this.#spilled = true;
2353
+ const buffered = Buffer.concat(this.#chunks, this.#chunkBytes);
2354
+ this.#chunks = [];
2355
+ this.#chunkBytes = 0;
2356
+ this.#queue(async () => {
2357
+ await mkdir(this.#paths.dir, {
2358
+ recursive: true,
2359
+ mode: 448
2360
+ });
2361
+ this.#handle = await open(this.#paths.raw, "w", 384);
2362
+ await this.#handle.write(buffered);
2363
+ });
2364
+ }
2365
+ #write(chunk) {
2366
+ if (chunk.length === 0) return;
2367
+ this.#queue(async () => {
2368
+ await this.#handle?.write(chunk);
2369
+ });
2370
+ }
2371
+ #queue(task) {
2372
+ this.#io = this.#io.then(async () => {
2373
+ if (this.#broken) return;
2374
+ try {
2375
+ await task();
2376
+ } catch (error) {
2377
+ this.#broken = true;
2378
+ this.#onError(error);
2379
+ }
2380
+ });
2381
+ }
2382
+ };
2383
+ var StoredArtifact = class {
2384
+ #paths;
2385
+ #bytes;
2386
+ #capped;
2387
+ #stored;
2388
+ constructor(paths, info, stored) {
2389
+ this.#paths = paths;
2390
+ this.#bytes = info.bytes;
2391
+ this.#capped = info.capped === true;
2392
+ this.#stored = stored.artifact !== void 0 ? { artifact: stored.artifact } : { output: stored.output ?? "" };
2393
+ }
2394
+ async read(tail) {
2395
+ if (tail !== void 0) return this.replay(tail);
2396
+ return this.#head();
2397
+ }
2398
+ async replay(max) {
2399
+ const want = Math.max(0, Math.floor(max));
2400
+ if (this.#capped) {
2401
+ const ring = await readWhole(this.#paths.tail);
2402
+ return ring.subarray(Math.max(0, ring.length - want));
2403
+ }
2404
+ if (this.#stored.output !== void 0) {
2405
+ const all = Buffer.from(this.#stored.output, "utf8");
2406
+ return all.subarray(Math.max(0, all.length - want));
2407
+ }
2408
+ const size = await stat(this.#paths.raw).then((s) => s.size, () => 0);
2409
+ return readRange(this.#paths.raw, Math.max(0, size - want), size);
2410
+ }
2411
+ async textView() {
2412
+ const head = await this.#head();
2413
+ let text = ttyText(head.toString("utf8"));
2414
+ if (this.#capped) {
2415
+ const ring = await readWhole(this.#paths.tail);
2416
+ const after = this.#bytes - head.length;
2417
+ const kept = Math.min(after, ring.length);
2418
+ if (kept > 0) text += (after > kept ? "\n" : "") + ttyText(ring.subarray(ring.length - kept).toString("utf8"));
2419
+ }
2420
+ return text;
2421
+ }
2422
+ stored() {
2423
+ return this.#stored;
2424
+ }
2425
+ idle() {
2426
+ return Promise.resolve();
2427
+ }
2428
+ async remove() {
2429
+ await rm(this.#paths.raw, { force: true });
2430
+ await rm(this.#paths.tail, { force: true });
2431
+ }
2432
+ #head() {
2433
+ return this.#stored.output !== void 0 ? Promise.resolve(Buffer.from(this.#stored.output, "utf8")) : readWhole(this.#paths.raw);
2434
+ }
2435
+ };
2436
+ function serialize(state) {
2437
+ return {
2438
+ version: INDEX_VERSION,
2439
+ sessionId: state.sessionId,
2440
+ nextOrdinal: state.nextOrdinal,
2441
+ shells: [...state.entries.values()].map((entry) => ({
2442
+ ...entry.info,
2443
+ generation: entry.generation,
2444
+ ...entry.artifact.stored()
2445
+ }))
2446
+ };
2447
+ }
2448
+ function parseIndex(value) {
2449
+ const index = value;
2450
+ if (!index || index.version !== INDEX_VERSION || typeof index.sessionId !== "string" || !Array.isArray(index.shells)) throw new Error("unrecognised shell index");
2451
+ const shells = index.shells.filter((record) => typeof record === "object" && record !== null && typeof record.id === "string");
2452
+ const nextOrdinal = typeof index.nextOrdinal === "number" && index.nextOrdinal > 0 ? index.nextOrdinal : 1;
2453
+ return {
2454
+ version: INDEX_VERSION,
2455
+ sessionId: index.sessionId,
2456
+ nextOrdinal,
2457
+ shells
2458
+ };
2459
+ }
2460
+ function freshSession(sessionId) {
2461
+ return {
2462
+ sessionId,
2463
+ nextOrdinal: 1,
2464
+ entries: /* @__PURE__ */ new Map(),
2465
+ chain: Promise.resolve(),
2466
+ pending: false
2467
+ };
2468
+ }
2469
+ function runningIn(state) {
2470
+ return [...state.entries.values()].filter((entry) => entry.info.status === "running");
2471
+ }
2472
+ function sourceFor(entry, artifact) {
2473
+ return {
2474
+ info: () => entry.info,
2475
+ text: () => artifact.text(),
2476
+ subscribe: (listener) => {
2477
+ entry.listeners.add(listener);
2478
+ return () => {
2479
+ entry.listeners.delete(listener);
2480
+ };
2481
+ }
2482
+ };
2483
+ }
2484
+ function mintShellId() {
2485
+ const bytes = randomBytes(ID_CHARS);
2486
+ let id = "sh_";
2487
+ for (let i = 0; i < ID_CHARS; i++) id += ID_ALPHABET[bytes[i] & 31];
2488
+ return id;
2489
+ }
2490
+ function shellLabel(command) {
2491
+ return (command.split("\n")[0] ?? "").trim().slice(0, SHELL_LABEL_MAX);
2492
+ }
2493
+ function clamp(value, min, max) {
2494
+ return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.trunc(value))) : min;
2495
+ }
2496
+ function isMissing(error) {
2497
+ return error?.code === "ENOENT";
2498
+ }
2499
+ async function readWhole(path) {
2500
+ try {
2501
+ return await readFile(path);
2502
+ } catch (error) {
2503
+ if (isMissing(error)) return Buffer.alloc(0);
2504
+ throw error;
2505
+ }
2506
+ }
2507
+ async function readRange(path, start, end) {
2508
+ const length = Math.max(0, end - start);
2509
+ if (length === 0) return Buffer.alloc(0);
2510
+ let handle;
2511
+ try {
2512
+ handle = await open(path, "r");
2513
+ } catch (error) {
2514
+ if (isMissing(error)) return Buffer.alloc(0);
2515
+ throw error;
2516
+ }
2517
+ try {
2518
+ const out = Buffer.allocUnsafe(length);
2519
+ let got = 0;
2520
+ while (got < length) {
2521
+ const { bytesRead } = await handle.read(out, got, length - got, start + got);
2522
+ if (bytesRead === 0) break;
2523
+ got += bytesRead;
2524
+ }
2525
+ return got === length ? out : out.subarray(0, got);
2526
+ } finally {
2527
+ await handle.close();
2528
+ }
2529
+ }
2530
+ //#endregion
2531
+ //#region src/routes/shells.ts
2532
+ async function handleShells(ctx, req, res, route, runner, operator) {
2533
+ const { shells } = ctx;
2534
+ const sessionId = route.id;
2535
+ if (!shells) {
2536
+ json(res, 404, { error: SHELL_REFUSAL });
2537
+ return;
2538
+ }
2539
+ if (!operator) {
2540
+ json(res, 403, { error: SHELL_REFUSAL });
2541
+ return;
2542
+ }
2543
+ if (route.shellId === void 0) {
2544
+ if (req.method !== "GET") {
2545
+ json(res, 405, { error: "method not allowed" });
2546
+ return;
2547
+ }
2548
+ json(res, 200, { shells: shells.list(sessionId) });
2549
+ return;
2550
+ }
2551
+ if (route.shellAction === "kill") {
2552
+ if (req.method !== "POST") {
2553
+ json(res, 405, { error: "method not allowed" });
2554
+ return;
2555
+ }
2556
+ if (!runner || !shellPermitted(shells, runner, operator)) {
2557
+ json(res, 403, { error: SHELL_REFUSAL });
2558
+ return;
2559
+ }
2560
+ const shell = shells.kill(sessionId, route.shellId);
2561
+ if (!shell) {
2562
+ json(res, 404, { error: "shell not found" });
2563
+ return;
2564
+ }
2565
+ json(res, 200, { shell });
2566
+ return;
2567
+ }
2568
+ if (req.method !== "GET") {
2569
+ json(res, 405, { error: "method not allowed" });
2570
+ return;
2571
+ }
2572
+ if (route.shellAction === "output") {
2573
+ const url = new URL(req.url ?? "/", "http://internal");
2574
+ const view = url.searchParams.get("view") === "raw" ? "raw" : "text";
2575
+ const tailParam = url.searchParams.get("tail");
2576
+ const tail = tailParam === null ? void 0 : Number(tailParam);
2577
+ if (tail !== void 0 && (!Number.isInteger(tail) || tail <= 0)) {
2578
+ json(res, 400, { error: "tail must be a positive integer" });
2579
+ return;
2580
+ }
2581
+ const output = await shells.output(sessionId, route.shellId, {
2582
+ view,
2583
+ tail
2584
+ });
2585
+ if (output === void 0) {
2586
+ json(res, 404, { error: "shell not found" });
2587
+ return;
2588
+ }
2589
+ const body = Buffer.from(output, "utf8");
2590
+ res.writeHead(200, untrustedDownloadHeaders(`${route.shellId}.txt`, "text/plain; charset=utf-8", body.length));
2591
+ res.end(body);
2592
+ return;
2593
+ }
2594
+ const shell = shells.get(sessionId, route.shellId);
2595
+ if (!shell) {
2596
+ json(res, 404, { error: "shell not found" });
2597
+ return;
2598
+ }
2599
+ json(res, 200, { shell });
2600
+ }
2601
+ //#endregion
1521
2602
  //#region src/routes/tool-results.ts
1522
2603
  function handleToolResult(req, res, lookup, seq) {
1523
2604
  if (req.method !== "GET") {
@@ -1650,6 +2731,10 @@ async function handleSessions(ctx, req, res, route, auth) {
1650
2731
  await handleProducedFiles(ctx, req, res, route.id, route.producedFileId);
1651
2732
  return;
1652
2733
  }
2734
+ if (route.shells) {
2735
+ await handleShells(ctx, req, res, route, runner ?? null, authSvc.isOperator(auth));
2736
+ return;
2737
+ }
1653
2738
  if (route.projectIcon) {
1654
2739
  handleProjectIcon(projects, req, res, (runner?.info() ?? parked.info).cwd);
1655
2740
  return;
@@ -1724,151 +2809,7 @@ async function handleSessions(ctx, req, res, route, auth) {
1724
2809
  }
1725
2810
  json(res, 405, { error: "method not allowed" });
1726
2811
  }
1727
- const TIMEOUT_EXIT_CODE = 124;
1728
- const SPAWN_FAILURE_EXIT_CODE = 127;
1729
- const KILLED_EXIT_CODE = 128 + constants$1.signals.SIGKILL;
1730
- function shellPermitted(shell, runner, operator) {
1731
- if (shell === null || !operator) return false;
1732
- const info = runner.info();
1733
- return (info.capabilities ?? (info.engine ? ENGINE_CAPABILITIES[info.engine] : void 0))?.hostCwd === true;
1734
- }
1735
- function shellChildEnv(base) {
1736
- const env = {};
1737
- for (const [key, value] of Object.entries(base)) if (value !== void 0) env[key] = value;
1738
- return env;
1739
- }
1740
- function createShellService(options = {}) {
1741
- const timeoutMs = options.timeoutMs ?? 12e4;
1742
- const maxOutputBytes = options.maxOutputBytes ?? 32768;
1743
- const active = /* @__PURE__ */ new Map();
1744
- const run = async (sessionId, cwd, command) => {
1745
- if (active.has(sessionId)) throw new Error("a shell command is still running on this session");
1746
- const child = spawnShell({
1747
- command,
1748
- cwd,
1749
- env: shellChildEnv(process.env),
1750
- timeoutMs,
1751
- maxOutputBytes
1752
- });
1753
- active.set(sessionId, child);
1754
- try {
1755
- return await child.result;
1756
- } finally {
1757
- if (active.get(sessionId) === child) active.delete(sessionId);
1758
- }
1759
- };
1760
- const kill = (sessionId, reason = "session closed") => {
1761
- active.get(sessionId)?.kill(reason);
1762
- };
1763
- const watch = (runner) => runner.subscribe((event) => {
1764
- if (event.type === "session_closed") kill(runner.id, "session closed");
1765
- else if (event.type === "status_changed" && event.status === "parked") kill(runner.id, "session parked");
1766
- }, runner.info().lastSeq);
1767
- const killAll = () => {
1768
- for (const sessionId of active.keys()) kill(sessionId, "server shutting down");
1769
- };
1770
- const running = (sessionId) => active.has(sessionId);
1771
- return {
1772
- run,
1773
- kill,
1774
- watch,
1775
- killAll,
1776
- running,
1777
- timeoutMs,
1778
- maxOutputBytes
1779
- };
1780
- }
1781
- var BoundedOutput = class {
1782
- #chunks = [];
1783
- #kept = 0;
1784
- #dropped = 0;
1785
- #budget;
1786
- constructor(budget) {
1787
- this.#budget = budget;
1788
- }
1789
- push(chunk) {
1790
- const take = Math.min(chunk.length, this.#budget.remaining);
1791
- if (take > 0) {
1792
- this.#chunks.push(take === chunk.length ? chunk : chunk.subarray(0, take));
1793
- this.#kept += take;
1794
- this.#budget.remaining -= take;
1795
- }
1796
- this.#dropped += chunk.length - take;
1797
- }
1798
- text() {
1799
- const head = Buffer.concat(this.#chunks, this.#kept).toString("utf8");
1800
- return this.#dropped > 0 ? `${head}\n[output truncated: ${this.#dropped} more bytes dropped]` : head;
1801
- }
1802
- };
1803
- function spawnShell(input) {
1804
- const startedAt = Date.now();
1805
- const budget = { remaining: input.maxOutputBytes };
1806
- const stdout = new BoundedOutput(budget);
1807
- const stderr = new BoundedOutput(budget);
1808
- let settle;
1809
- const result = new Promise((resolve) => {
1810
- settle = resolve;
1811
- });
1812
- let done = false;
1813
- const finish = (exitCode, note) => {
1814
- if (done) return;
1815
- done = true;
1816
- clearTimeout(timer);
1817
- const captured = stderr.text();
1818
- const stderrText = note ? captured ? `${captured}\n[${note}]` : `[${note}]` : captured;
1819
- settle?.({
1820
- command: input.command,
1821
- stdout: stdout.text(),
1822
- stderr: stderrText,
1823
- exitCode,
1824
- durationMs: Date.now() - startedAt
1825
- });
1826
- };
1827
- const child = spawn("/bin/sh", ["-c", input.command], {
1828
- cwd: input.cwd,
1829
- env: {
1830
- ...input.env,
1831
- PWD: input.cwd
1832
- },
1833
- stdio: [
1834
- "ignore",
1835
- "pipe",
1836
- "pipe"
1837
- ],
1838
- detached: true
1839
- });
1840
- child.stdout.on("data", (chunk) => stdout.push(chunk));
1841
- child.stderr.on("data", (chunk) => stderr.push(chunk));
1842
- const killGroup = () => {
1843
- if (child.pid === void 0) return;
1844
- try {
1845
- process.kill(-child.pid, "SIGKILL");
1846
- } catch {
1847
- try {
1848
- child.kill("SIGKILL");
1849
- } catch {}
1850
- }
1851
- child.stdout.destroy();
1852
- child.stderr.destroy();
1853
- };
1854
- const kill = (reason, exitCode = KILLED_EXIT_CODE) => {
1855
- if (done) return;
1856
- killGroup();
1857
- finish(exitCode, `killed: ${reason}`);
1858
- };
1859
- const timer = setTimeout(() => kill(`timed out after ${Math.round(input.timeoutMs / 1e3)}s`, TIMEOUT_EXIT_CODE), input.timeoutMs);
1860
- child.on("error", (error) => finish(SPAWN_FAILURE_EXIT_CODE, error.message));
1861
- child.on("close", (code, signal) => {
1862
- const signalCode = signal ? constants$1.signals[signal] ?? 0 : 0;
1863
- finish(code ?? (signalCode ? 128 + signalCode : 1), signal ? `killed by ${signal}` : void 0);
1864
- });
1865
- return {
1866
- result,
1867
- kill: (reason) => kill(reason)
1868
- };
1869
- }
1870
- //#endregion
1871
- //#region src/routes/ws.ts
2812
+ const SHELL_DETACHED_BACKPRESSURE = "backpressure";
1872
2813
  function attachClient(ctx, ws, runner, req, access) {
1873
2814
  const { bridge, parking } = ctx;
1874
2815
  const url = new URL(req.url ?? "/", "http://internal");
@@ -1878,12 +2819,18 @@ function attachClient(ctx, ws, runner, req, access) {
1878
2819
  const send = (frame) => {
1879
2820
  if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(frame));
1880
2821
  };
2822
+ const client = {
2823
+ send,
2824
+ open: () => ws.readyState === ws.OPEN,
2825
+ buffered: () => ws.bufferedAmount,
2826
+ shells: /* @__PURE__ */ new Map()
2827
+ };
1881
2828
  send({
1882
2829
  type: "attached",
1883
2830
  protocolVersion: PROTOCOL_VERSION,
1884
2831
  session: ctx.projects.withProject(runner.info()),
1885
2832
  replayingFrom: afterSeq,
1886
- ...shellPermitted(ctx.shell, runner, access.operator) ? { shell: true } : {},
2833
+ ...shellPermitted(ctx.shells, runner, access.operator) ? { shell: true } : {},
1887
2834
  ...ctx.pricingOverrides ? { pricingOverrides: ctx.pricingOverrides } : {}
1888
2835
  });
1889
2836
  const unsubscribe = runner.subscribe((event) => send({
@@ -1907,7 +2854,7 @@ function attachClient(ctx, ws, runner, req, access) {
1907
2854
  });
1908
2855
  return;
1909
2856
  }
1910
- handleCommand(ctx, frame, runner, access).catch((error) => {
2857
+ handleCommand(ctx, frame, runner, access, client).catch((error) => {
1911
2858
  send({
1912
2859
  type: "protocol_error",
1913
2860
  message: error instanceof Error ? error.message : "command failed"
@@ -1915,12 +2862,13 @@ function attachClient(ctx, ws, runner, req, access) {
1915
2862
  });
1916
2863
  });
1917
2864
  ws.on("close", () => {
2865
+ detachShells(client);
1918
2866
  unsubscribe();
1919
2867
  detachBridge();
1920
2868
  parking.onDetach(runner.id);
1921
2869
  });
1922
2870
  }
1923
- async function handleCommand(ctx, frame, runner, access) {
2871
+ async function handleCommand(ctx, frame, runner, access, client) {
1924
2872
  const { attachmentStore, bridge } = ctx;
1925
2873
  switch (frame.type) {
1926
2874
  case "user_message": {
@@ -1974,21 +2922,107 @@ async function handleCommand(ctx, frame, runner, access) {
1974
2922
  });
1975
2923
  return;
1976
2924
  case "shell_command": {
1977
- if (!shellPermitted(ctx.shell, runner, access.operator) || ctx.shell === null) throw new Error("shell commands are not available on this session");
2925
+ const shells = permittedShells(ctx, runner, access);
1978
2926
  if (typeof frame.command !== "string" || frame.command.includes("\0")) throw new Error("shell command must be a string");
1979
2927
  if (frame.command.length > SHELL_COMMAND_MAX) throw new Error(`shell command exceeds ${SHELL_COMMAND_MAX} characters`);
1980
2928
  if (frame.command.trim() === "") throw new Error("shell command is empty");
1981
2929
  if (!runner.queueLocalCommand) throw new Error(`the ${runner.info().engine ?? "claude"} engine cannot take shell output`);
1982
- const result = await ctx.shell.run(runner.id, runner.info().cwd, frame.command);
1983
- runner.queueLocalCommand(result);
2930
+ const { shell, source } = await shells.spawn({
2931
+ runner,
2932
+ command: frame.command,
2933
+ owner: "user"
2934
+ });
2935
+ try {
2936
+ runner.queueLocalCommand(source);
2937
+ } catch (error) {
2938
+ shells.kill(runner.id, shell.id);
2939
+ throw error;
2940
+ }
1984
2941
  return;
1985
2942
  }
2943
+ case "shell_attach":
2944
+ await attachShell(permittedShells(ctx, runner, access), client, runner.id, frame.shellId, {
2945
+ cols: frame.cols,
2946
+ rows: frame.rows
2947
+ });
2948
+ return;
2949
+ case "shell_input":
2950
+ permittedShells(ctx, runner, access).write(runner.id, frame.shellId, frame.data);
2951
+ return;
2952
+ case "shell_resize":
2953
+ permittedShells(ctx, runner, access).resize(runner.id, frame.shellId, {
2954
+ cols: frame.cols,
2955
+ rows: frame.rows
2956
+ });
2957
+ return;
2958
+ case "shell_detach":
2959
+ detachShell(client, frame.shellId);
2960
+ return;
1986
2961
  case "close":
1987
2962
  runner.close("client");
1988
2963
  return;
1989
2964
  default: throw new Error(`unknown command: ${frame.type}`);
1990
2965
  }
1991
2966
  }
2967
+ function permittedShells(ctx, runner, access) {
2968
+ if (ctx.shells === null || !shellPermitted(ctx.shells, runner, access.operator)) throw new Error(SHELL_REFUSAL);
2969
+ return ctx.shells;
2970
+ }
2971
+ async function attachShell(shells, client, sessionId, shellId, size) {
2972
+ shells.resize(sessionId, shellId, size);
2973
+ let detach;
2974
+ const drop = (reason) => {
2975
+ if (detach === void 0 || client.shells.get(shellId) !== detach) return;
2976
+ client.shells.delete(shellId);
2977
+ detach();
2978
+ client.send({
2979
+ type: "shell_detached",
2980
+ shellId,
2981
+ reason
2982
+ });
2983
+ };
2984
+ const sink = {
2985
+ write: (data) => {
2986
+ if (client.buffered() > 4194304) {
2987
+ drop(SHELL_DETACHED_BACKPRESSURE);
2988
+ return;
2989
+ }
2990
+ client.send({
2991
+ type: "shell_output",
2992
+ shellId,
2993
+ data
2994
+ });
2995
+ },
2996
+ end: drop
2997
+ };
2998
+ const attached = await shells.attach(sessionId, shellId, sink);
2999
+ if (!client.open()) {
3000
+ attached.detach();
3001
+ return;
3002
+ }
3003
+ detachShell(client, shellId);
3004
+ if (attached.shell.status === "running") {
3005
+ detach = attached.detach;
3006
+ client.shells.set(shellId, detach);
3007
+ }
3008
+ const { shell, scrollback } = attached;
3009
+ client.send({
3010
+ type: "shell_attached",
3011
+ shellId,
3012
+ shell,
3013
+ cols: shell.cols,
3014
+ rows: shell.rows,
3015
+ scrollback
3016
+ });
3017
+ }
3018
+ function detachShell(client, shellId) {
3019
+ client.shells.get(shellId)?.();
3020
+ client.shells.delete(shellId);
3021
+ }
3022
+ function detachShells(client) {
3023
+ for (const detach of client.shells.values()) detach();
3024
+ client.shells.clear();
3025
+ }
1992
3026
  //#endregion
1993
3027
  //#region src/services/attachments.ts
1994
3028
  const DEFAULT_MAX_FILE_BYTES = 10485760;
@@ -3465,14 +4499,15 @@ function createSessionFactory(deps) {
3465
4499
  }
3466
4500
  };
3467
4501
  const buildRunner = async (built, restore, id) => {
3468
- const config = deps.peers ? {
3469
- ...built,
3470
- peers: deps.peers
3471
- } : built;
3472
- const name = config.profile;
4502
+ const name = built.profile;
3473
4503
  const profile = name !== void 0 ? profiles.get(name) : void 0;
3474
4504
  if (name !== void 0 && !profile) throw new Error(`unknown profile: ${name}`);
3475
4505
  const capabilities = profile?.capabilities ?? adapterFor(profile?.engine).capabilities;
4506
+ const config = {
4507
+ ...built,
4508
+ ...deps.peers ? { peers: deps.peers } : {},
4509
+ ...deps.shells && capabilities.hostCwd === true ? { shells: deps.shells } : {}
4510
+ };
3476
4511
  if (config.instructions !== void 0 && capabilities.systemInstructions === false) throw new Error(`the ${engineOf(profile)} engine cannot deliver system instructions`);
3477
4512
  const runner = profile && isProviderProfile(profile) ? await deps.createEngineRunner({
3478
4513
  config,
@@ -3567,7 +4602,7 @@ function createSessionFactory(deps) {
3567
4602
  const SOCKET_CLOSE_GRACE_MS = 250;
3568
4603
  const CLOSE_DEADLINE_MS = 1e3;
3569
4604
  const USAGE_STALE_MS = 3e5;
3570
- function surveyDrain(registry) {
4605
+ function surveyDrain(registry, shells) {
3571
4606
  const working = [];
3572
4607
  const awaitingHuman = [];
3573
4608
  for (const info of registry.list()) {
@@ -3578,7 +4613,12 @@ function surveyDrain(registry) {
3578
4613
  return {
3579
4614
  working,
3580
4615
  awaitingHuman,
3581
- timedOut: false
4616
+ timedOut: false,
4617
+ shells: (shells?.running() ?? []).map(({ sessionId, id, label }) => ({
4618
+ sessionId,
4619
+ id,
4620
+ label
4621
+ }))
3582
4622
  };
3583
4623
  }
3584
4624
  function sameDrain(a, b) {
@@ -3638,13 +4678,24 @@ function createWorkerServer(options = {}) {
3638
4678
  if (invalid) throw new Error(`createWorkerServer: ${invalid}`);
3639
4679
  }
3640
4680
  const refs = {};
3641
- const projects = new ProjectInfoService();
4681
+ const generation = randomUUID();
4682
+ const shells = options.shell?.enabled === true ? createShellRegistry({
4683
+ generation,
4684
+ artifactDir: options.shell.artifactDir ?? null,
4685
+ timeoutMs: options.shell.timeoutMs,
4686
+ artifactMaxBytes: options.shell.artifactMaxBytes,
4687
+ artifactTtlMs: options.shell.artifactTtlMs,
4688
+ maxRunningPerSession: options.shell.maxRunningPerSession,
4689
+ onError: (error, context) => console.warn(`[workerdeck] shell ${context.op} error (${context.shellId ?? context.sessionId ?? "-"}): ${String(error)}`)
4690
+ }) : null;
4691
+ const projects = new ProjectInfoService({ decorate: (info) => shells ? shells.decorate(info) : info });
3642
4692
  const peers = options.peers?.enabled === false ? void 0 : createPeerService({
3643
4693
  refs,
3644
4694
  projects,
3645
4695
  options: options.peers
3646
4696
  });
3647
4697
  if (peers) installPeerDirectory(peers);
4698
+ installShellDirectory(shells ? createShellDirectory(shells) : void 0);
3648
4699
  const factory = createSessionFactory({
3649
4700
  adapterFor,
3650
4701
  profiles,
@@ -3655,6 +4706,7 @@ function createWorkerServer(options = {}) {
3655
4706
  approvalTimeoutMs: options.approvalTimeoutMs,
3656
4707
  requireApiKey: options.requireApiKey,
3657
4708
  peers: peers ? peerDirectoryHandle() : void 0,
4709
+ shells: shells ? shellDirectoryHandle() : void 0,
3658
4710
  refs
3659
4711
  });
3660
4712
  const availability = new AvailabilityTracker({
@@ -3668,14 +4720,13 @@ function createWorkerServer(options = {}) {
3668
4720
  decorateInfo: (info) => projects.withProject(info)
3669
4721
  });
3670
4722
  const producedFiles = new ProducedFileStore();
3671
- const shell = options.shell?.enabled === true ? createShellService(options.shell) : null;
3672
4723
  const registry = new SessionRegistry({ onRegister: (runner) => {
3673
4724
  const detachers = [
3674
4725
  notifier.watch(runner),
3675
4726
  producedFiles.watch(runner),
3676
4727
  profileUsage.watch(runner),
3677
4728
  spendLedger.watch(runner),
3678
- shell?.watch(runner),
4729
+ shells?.watch(runner),
3679
4730
  peers?.watch(runner)
3680
4731
  ];
3681
4732
  const profile = runner.info().profile;
@@ -3786,7 +4837,8 @@ function createWorkerServer(options = {}) {
3786
4837
  hostFilesWritable: options.hostFiles?.write === true,
3787
4838
  maxHostFileBytes: options.hostFiles?.maxFileBytes ?? 1048576,
3788
4839
  maxHostDirEntries: options.hostFiles?.maxEntries ?? 5e3,
3789
- shell,
4840
+ shells,
4841
+ generation,
3790
4842
  pricingOverrides: Object.keys(pricing.overrides).length > 0 ? pricing.overrides : void 0
3791
4843
  };
3792
4844
  const handleRequest = async (req, res) => {
@@ -3855,6 +4907,18 @@ function createWorkerServer(options = {}) {
3855
4907
  await handleSdkSessions(ctx, req, res, authCtx);
3856
4908
  return;
3857
4909
  }
4910
+ if (pathname === basePath + "/meta") {
4911
+ const authCtx = await auth.authenticate(req);
4912
+ if (!authCtx.ok) {
4913
+ json(res, 401, { error: "unauthorized" });
4914
+ return;
4915
+ }
4916
+ json(res, 200, {
4917
+ protocolVersion: PROTOCOL_VERSION,
4918
+ ...auth.isOperator(authCtx) ? { machineId: machineId() } : {}
4919
+ });
4920
+ return;
4921
+ }
3858
4922
  if (pathname.startsWith(basePath + "/fs/")) {
3859
4923
  const authCtx = await auth.authenticate(req);
3860
4924
  if (!authCtx.ok) {
@@ -3955,10 +5019,12 @@ function createWorkerServer(options = {}) {
3955
5019
  queue,
3956
5020
  bridge,
3957
5021
  parking,
5022
+ shells,
3958
5023
  listen: async (port, host) => {
3959
5024
  await profiles.refreshStored();
3960
5025
  await profiles.seedStore();
3961
5026
  await parking.hydrate();
5027
+ await shells?.hydrate();
3962
5028
  return new Promise((resolve, reject) => {
3963
5029
  server.once("error", reject);
3964
5030
  server.listen(port, host, () => {
@@ -3990,16 +5056,16 @@ function createWorkerServer(options = {}) {
3990
5056
  const { timeoutMs = 3e4, pollMs = 250, onProgress } = drainOptions;
3991
5057
  draining = true;
3992
5058
  const deadline = Date.now() + timeoutMs;
3993
- let report = surveyDrain(registry);
5059
+ let report = surveyDrain(registry, shells);
3994
5060
  onProgress?.(report);
3995
5061
  while (report.working.length > 0 && Date.now() < deadline) {
3996
5062
  await new Promise((resolve) => setTimeout(resolve, pollMs));
3997
- const next = surveyDrain(registry);
5063
+ const next = surveyDrain(registry, shells);
3998
5064
  if (!sameDrain(next, report)) onProgress?.(next);
3999
5065
  report = next;
4000
5066
  }
4001
5067
  report = {
4002
- ...surveyDrain(registry),
5068
+ ...surveyDrain(registry, shells),
4003
5069
  timedOut: false
4004
5070
  };
4005
5071
  report.timedOut = report.working.length > 0;
@@ -4009,23 +5075,25 @@ function createWorkerServer(options = {}) {
4009
5075
  close: () => {
4010
5076
  closing ??= new Promise((resolve) => {
4011
5077
  queue?.close();
5078
+ shells?.killAll("server_stopped");
5079
+ const flushed = shells?.flush().catch(() => {}) ?? Promise.resolve();
4012
5080
  parking.close();
4013
5081
  registry.closeAll();
4014
- shell?.killAll();
4015
5082
  for (const ws of wss.clients) ws.close(1001, "server shutting down");
4016
5083
  queueSockets.clear();
4017
5084
  const force = setTimeout(() => {
4018
5085
  for (const ws of wss.clients) ws.terminate();
4019
5086
  }, SOCKET_CLOSE_GRACE_MS);
4020
5087
  force.unref();
4021
- const deadline = setTimeout(() => resolve(), CLOSE_DEADLINE_MS);
5088
+ const finish = () => void flushed.then(() => resolve());
5089
+ const deadline = setTimeout(finish, CLOSE_DEADLINE_MS);
4022
5090
  deadline.unref();
4023
5091
  const drained = Promise.all([new Promise((done) => wss.close(() => done())), new Promise((done) => server.close(() => done()))]);
4024
5092
  server.closeAllConnections();
4025
5093
  drained.then(() => {
4026
5094
  clearTimeout(force);
4027
5095
  clearTimeout(deadline);
4028
- resolve();
5096
+ finish();
4029
5097
  });
4030
5098
  });
4031
5099
  return closing;
@@ -4123,6 +5191,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
4123
5191
  };
4124
5192
  }
4125
5193
  //#endregion
4126
- export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, toDurableRecord };
5194
+ export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellRegistry, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord };
4127
5195
 
4128
5196
  //# sourceMappingURL=index.mjs.map