@cecwxf/dsh-room 0.3.3 → 0.3.4
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/README.md +10 -0
- package/dist/client.js +64 -42
- package/dist/index.js +421 -160
- package/dist/types/adapter.d.ts +1 -0
- package/dist/types/adapter.d.ts.map +1 -1
- package/dist/types/approval-queue.d.ts +1 -0
- package/dist/types/approval-queue.d.ts.map +1 -1
- package/dist/types/http.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/mcp-workspace.d.ts.map +1 -1
- package/dist/types/models.d.ts +14 -0
- package/dist/types/models.d.ts.map +1 -0
- package/dist/types/orchestrator.d.ts +4 -1
- package/dist/types/orchestrator.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +16 -1
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/state.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -416,8 +416,8 @@ __export(mcp_workspace_exports, {
|
|
|
416
416
|
MCP_TOOL_DEFS: () => MCP_TOOL_DEFS,
|
|
417
417
|
WorkspaceMcp: () => WorkspaceMcp
|
|
418
418
|
});
|
|
419
|
-
import { mkdir as mkdir3, readFile as readFile3, readdir, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
420
|
-
import { dirname as dirname2, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
419
|
+
import { lstat, mkdir as mkdir3, readFile as readFile3, readdir, realpath, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
420
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
421
421
|
import { spawn } from "node:child_process";
|
|
422
422
|
function actionFor(tool) {
|
|
423
423
|
if (tool === "workspace_write" || tool === "workspace_mkdir") return "fs.write";
|
|
@@ -482,16 +482,27 @@ var init_mcp_workspace = __esm({
|
|
|
482
482
|
const root = resolve2(this.cfg.workspacePath);
|
|
483
483
|
const target = resolve2(root, rel || ".");
|
|
484
484
|
const relToRoot = relative2(root, target);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
485
|
+
const outside = (p) => isAbsolute2(p) || p === ".." || p.startsWith(`..${sep}`);
|
|
486
|
+
if (outside(relToRoot)) throw new Error("path escapes workspace");
|
|
487
|
+
const realRoot = await realpath(root);
|
|
488
|
+
let ancestor = resolve2(realRoot, relToRoot);
|
|
489
|
+
const suffix = [];
|
|
490
|
+
while (true) {
|
|
491
|
+
let real;
|
|
492
|
+
try {
|
|
493
|
+
real = await realpath(ancestor);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
if (e.code !== "ENOENT") throw e;
|
|
496
|
+
if ((await lstat(ancestor).catch(() => null))?.isSymbolicLink()) throw new Error("path escapes workspace (dangling symlink)");
|
|
497
|
+
const parent = dirname2(ancestor);
|
|
498
|
+
if (parent === ancestor) throw e;
|
|
499
|
+
suffix.unshift(relative2(parent, ancestor));
|
|
500
|
+
ancestor = parent;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (outside(relative2(realRoot, real))) throw new Error("path escapes workspace (symlink)");
|
|
504
|
+
return resolve2(real, ...suffix);
|
|
493
505
|
}
|
|
494
|
-
return target;
|
|
495
506
|
}
|
|
496
507
|
async maybeApprove(tool, summary, detail, from) {
|
|
497
508
|
const action = actionFor(tool);
|
|
@@ -503,7 +514,7 @@ var init_mcp_workspace = __esm({
|
|
|
503
514
|
if (cap) {
|
|
504
515
|
const grant = resolvePeerGrant(this.cfg, from, cap);
|
|
505
516
|
if (grant === "deny") throw new Error(`peer grant deny: ${cap} for ${from}`);
|
|
506
|
-
if (grant === "auto") return;
|
|
517
|
+
if (grant === "auto" && (cap !== "read" || this.cfg.permissions.autoRead)) return;
|
|
507
518
|
}
|
|
508
519
|
if (!action && cap === "read") {
|
|
509
520
|
const decision2 = await this.approvals.request({
|
|
@@ -541,6 +552,7 @@ var init_mcp_workspace = __esm({
|
|
|
541
552
|
await mkdir3(this.cfg.workspacePath, { recursive: true });
|
|
542
553
|
switch (tool) {
|
|
543
554
|
case "workspace_list": {
|
|
555
|
+
await this.maybeApprove(tool, `list ${args.path ?? "."}`, "", from);
|
|
544
556
|
const dir = await this.resolveSafe(String(args.path ?? "."));
|
|
545
557
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
546
558
|
return {
|
|
@@ -549,18 +561,20 @@ var init_mcp_workspace = __esm({
|
|
|
549
561
|
};
|
|
550
562
|
}
|
|
551
563
|
case "workspace_read": {
|
|
552
|
-
|
|
564
|
+
await this.maybeApprove(tool, `read ${args.path}`, "", from);
|
|
553
565
|
const file = await this.resolveSafe(String(args.path ?? ""));
|
|
554
566
|
const buf = await readFile3(file);
|
|
555
567
|
if (buf.length > 256 * 1024) throw new Error("file too large (>256KiB)");
|
|
556
568
|
return { path: String(args.path), content: buf.toString("utf8") };
|
|
557
569
|
}
|
|
558
570
|
case "workspace_stat": {
|
|
571
|
+
await this.maybeApprove(tool, `stat ${args.path}`, "", from);
|
|
559
572
|
const file = await this.resolveSafe(String(args.path ?? ""));
|
|
560
573
|
const st = await stat2(file);
|
|
561
574
|
return { path: String(args.path), size: st.size, mtimeMs: st.mtimeMs, isDirectory: st.isDirectory(), isFile: st.isFile() };
|
|
562
575
|
}
|
|
563
576
|
case "workspace_search": {
|
|
577
|
+
await this.maybeApprove(tool, `search ${args.path ?? "."}`, String(args.query ?? ""), from);
|
|
564
578
|
const q = String(args.query ?? "");
|
|
565
579
|
if (!q) throw new Error("query required");
|
|
566
580
|
const start = await this.resolveSafe(String(args.path ?? "."));
|
|
@@ -630,9 +644,22 @@ var init_mcp_workspace = __esm({
|
|
|
630
644
|
}
|
|
631
645
|
});
|
|
632
646
|
|
|
647
|
+
// src/models.ts
|
|
648
|
+
function resolveSelection(defaults, room = {}, agent = {}) {
|
|
649
|
+
return {
|
|
650
|
+
provider: agent.provider || room.provider || defaults.provider,
|
|
651
|
+
model: agent.model || room.model || defaults.model,
|
|
652
|
+
reasoningEffort: agent.reasoningEffort || room.reasoningEffort || defaults.reasoningEffort
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function describeModel(selection, catalog) {
|
|
656
|
+
if (!selection.provider || !selection.model) return null;
|
|
657
|
+
return catalog.find((m) => m.provider === selection.provider && m.model === selection.model) ?? { provider: selection.provider, model: selection.model, name: selection.model };
|
|
658
|
+
}
|
|
659
|
+
|
|
633
660
|
// src/plugin.ts
|
|
634
|
-
import { randomUUID as
|
|
635
|
-
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4, appendFile as appendFile2 } from "node:fs/promises";
|
|
661
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
662
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4, appendFile as appendFile2, rename as rename2, rm as rm3 } from "node:fs/promises";
|
|
636
663
|
import { dirname as dirname4, join as join3 } from "node:path";
|
|
637
664
|
|
|
638
665
|
// ../protocol/dist/types.js
|
|
@@ -1229,7 +1256,7 @@ var WsTransport = class {
|
|
|
1229
1256
|
return;
|
|
1230
1257
|
this.teardownSocket();
|
|
1231
1258
|
const gen = ++this.generation;
|
|
1232
|
-
await new Promise((
|
|
1259
|
+
await new Promise((resolve4, reject) => {
|
|
1233
1260
|
const ws = new WebSocket(this.url);
|
|
1234
1261
|
this.ws = ws;
|
|
1235
1262
|
let settled = false;
|
|
@@ -1250,7 +1277,7 @@ var WsTransport = class {
|
|
|
1250
1277
|
reject(err);
|
|
1251
1278
|
} else {
|
|
1252
1279
|
this._connected = true;
|
|
1253
|
-
|
|
1280
|
+
resolve4();
|
|
1254
1281
|
}
|
|
1255
1282
|
};
|
|
1256
1283
|
const timer = setTimeout(() => finish(new Error(`WebSocket connect timeout (${timeoutMs}ms) to ${this.url}`)), timeoutMs);
|
|
@@ -1340,6 +1367,8 @@ var CollabClient = class _CollabClient {
|
|
|
1340
1367
|
statusHandlers = [];
|
|
1341
1368
|
waiters = [];
|
|
1342
1369
|
joined = false;
|
|
1370
|
+
hasJoined = false;
|
|
1371
|
+
reconnecting;
|
|
1343
1372
|
disposed = false;
|
|
1344
1373
|
capabilities;
|
|
1345
1374
|
reconnect;
|
|
@@ -1361,9 +1390,11 @@ var CollabClient = class _CollabClient {
|
|
|
1361
1390
|
this.transport.onClose(() => {
|
|
1362
1391
|
this.joined = false;
|
|
1363
1392
|
this.setConnStatus("disconnected", "ws_closed");
|
|
1364
|
-
if (this.reconnect && !this.disposed) {
|
|
1393
|
+
if (this.reconnect && !this.disposed && !this.reconnecting) {
|
|
1365
1394
|
this.setConnStatus("connecting", "reconnect");
|
|
1366
|
-
|
|
1395
|
+
this.reconnecting = this.reconnectLoop().finally(() => {
|
|
1396
|
+
this.reconnecting = void 0;
|
|
1397
|
+
});
|
|
1367
1398
|
}
|
|
1368
1399
|
});
|
|
1369
1400
|
}
|
|
@@ -1441,7 +1472,15 @@ var CollabClient = class _CollabClient {
|
|
|
1441
1472
|
await this.transport.connect();
|
|
1442
1473
|
}
|
|
1443
1474
|
async join(timeoutMs = 1e4) {
|
|
1475
|
+
if (this.disposed)
|
|
1476
|
+
throw new Error("disposed");
|
|
1477
|
+
if (this.joined)
|
|
1478
|
+
return;
|
|
1479
|
+
const catchUp = this.hasJoined || !!this.cursor;
|
|
1480
|
+
const since = this.cursor?.get() ?? this.seq;
|
|
1444
1481
|
await this.connect();
|
|
1482
|
+
if (this.disposed)
|
|
1483
|
+
throw new Error("disposed");
|
|
1445
1484
|
const challengeP = this.waitFor((f) => f.type === "ctrl" && (f.method === "challenge" || f.method === "error"), timeoutMs);
|
|
1446
1485
|
this.transport.send({
|
|
1447
1486
|
v: 1,
|
|
@@ -1481,14 +1520,16 @@ var CollabClient = class _CollabClient {
|
|
|
1481
1520
|
throw new Error("expected joined");
|
|
1482
1521
|
}
|
|
1483
1522
|
this.joined = true;
|
|
1523
|
+
this.hasJoined = true;
|
|
1484
1524
|
this.peers = joined.peers;
|
|
1485
1525
|
this.seq = joined.seq;
|
|
1486
1526
|
this.setConnStatus("connected");
|
|
1487
1527
|
for (const h of this.peerHandlers)
|
|
1488
1528
|
h(this.peers);
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1529
|
+
if (catchUp && since < joined.seq) {
|
|
1530
|
+
const missed = await this.pollFrames(since);
|
|
1531
|
+
for (const frame of missed.frames)
|
|
1532
|
+
await this.handleFwd(frame);
|
|
1492
1533
|
}
|
|
1493
1534
|
this.startPing();
|
|
1494
1535
|
}
|
|
@@ -1583,6 +1624,18 @@ var CollabClient = class _CollabClient {
|
|
|
1583
1624
|
}
|
|
1584
1625
|
async poll(sinceSeq) {
|
|
1585
1626
|
const since = sinceSeq ?? this.cursor?.get() ?? 0;
|
|
1627
|
+
const res = await this.pollFrames(since);
|
|
1628
|
+
const out = [];
|
|
1629
|
+
for (const fwd of res.frames) {
|
|
1630
|
+
const p = await this.decryptFwd(fwd);
|
|
1631
|
+
if (p)
|
|
1632
|
+
out.push(p);
|
|
1633
|
+
}
|
|
1634
|
+
this.seq = Math.max(this.seq, res.seq);
|
|
1635
|
+
this.cursor?.set(this.seq);
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1638
|
+
async pollFrames(since) {
|
|
1586
1639
|
const resP = this.waitFor((f) => f.type === "ctrl" && f.method === "poll_result", 5e3);
|
|
1587
1640
|
this.transport.send({
|
|
1588
1641
|
v: 1,
|
|
@@ -1594,16 +1647,8 @@ var CollabClient = class _CollabClient {
|
|
|
1594
1647
|
});
|
|
1595
1648
|
const res = await resP;
|
|
1596
1649
|
if (res.type !== "ctrl" || res.method !== "poll_result")
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
for (const fwd of res.frames) {
|
|
1600
|
-
const p = await this.decryptFwd(fwd);
|
|
1601
|
-
if (p)
|
|
1602
|
-
out.push(p);
|
|
1603
|
-
}
|
|
1604
|
-
this.seq = Math.max(this.seq, res.seq);
|
|
1605
|
-
this.cursor?.set(this.seq);
|
|
1606
|
-
return out;
|
|
1650
|
+
throw new Error("expected poll_result");
|
|
1651
|
+
return res;
|
|
1607
1652
|
}
|
|
1608
1653
|
async history(sinceSeq = 0) {
|
|
1609
1654
|
return this.poll(sinceSeq);
|
|
@@ -1651,20 +1696,23 @@ var CollabClient = class _CollabClient {
|
|
|
1651
1696
|
}
|
|
1652
1697
|
async decryptFwd(frame) {
|
|
1653
1698
|
try {
|
|
1699
|
+
if (frame.roomId !== this.roomId)
|
|
1700
|
+
return null;
|
|
1654
1701
|
const aad = new TextEncoder().encode(`${frame.roomId}:${frame.from}`);
|
|
1655
1702
|
const plain = await decrypt(this.roomKey, frame.nonce, frame.ciphertext, aad);
|
|
1656
|
-
|
|
1703
|
+
const payload = decodePayload(plain);
|
|
1704
|
+
return payload.sender === frame.from ? payload : null;
|
|
1657
1705
|
} catch {
|
|
1658
1706
|
return null;
|
|
1659
1707
|
}
|
|
1660
1708
|
}
|
|
1661
1709
|
waitFor(pred, timeoutMs) {
|
|
1662
|
-
const p = new Promise((
|
|
1710
|
+
const p = new Promise((resolve4, reject) => {
|
|
1663
1711
|
const timer = setTimeout(() => {
|
|
1664
1712
|
this.waiters = this.waiters.filter((w) => w.timer !== timer);
|
|
1665
1713
|
reject(new Error("timeout waiting for frame"));
|
|
1666
1714
|
}, timeoutMs);
|
|
1667
|
-
this.waiters.push({ pred, resolve:
|
|
1715
|
+
this.waiters.push({ pred, resolve: resolve4, reject, timer });
|
|
1668
1716
|
});
|
|
1669
1717
|
p.catch(() => {
|
|
1670
1718
|
});
|
|
@@ -1818,6 +1866,7 @@ var RunScheduler = class {
|
|
|
1818
1866
|
runs = [];
|
|
1819
1867
|
reqs = /* @__PURE__ */ new Map();
|
|
1820
1868
|
aborts = /* @__PURE__ */ new Map();
|
|
1869
|
+
gates = /* @__PURE__ */ new Map();
|
|
1821
1870
|
idleWaiters = [];
|
|
1822
1871
|
enqueue(req) {
|
|
1823
1872
|
const run = {
|
|
@@ -1835,7 +1884,10 @@ var RunScheduler = class {
|
|
|
1835
1884
|
this.reqs.set(run.id, req);
|
|
1836
1885
|
this.changed();
|
|
1837
1886
|
if (req.gate) {
|
|
1838
|
-
req.gate
|
|
1887
|
+
const gate = req.gate;
|
|
1888
|
+
const ac = new AbortController();
|
|
1889
|
+
this.gates.set(run.id, ac);
|
|
1890
|
+
Promise.resolve().then(() => ac.signal.aborted ? false : gate(ac.signal)).then(
|
|
1839
1891
|
(ok2) => {
|
|
1840
1892
|
if (run.state !== "approval") return;
|
|
1841
1893
|
if (ok2) run.state = "queued";
|
|
@@ -1844,8 +1896,12 @@ var RunScheduler = class {
|
|
|
1844
1896
|
},
|
|
1845
1897
|
(e) => {
|
|
1846
1898
|
if (run.state === "approval") this.finish(run, "failed", e.message);
|
|
1899
|
+
this.pump();
|
|
1847
1900
|
}
|
|
1848
|
-
)
|
|
1901
|
+
).finally(() => {
|
|
1902
|
+
this.gates.delete(run.id);
|
|
1903
|
+
this.pump();
|
|
1904
|
+
});
|
|
1849
1905
|
} else this.pump();
|
|
1850
1906
|
return run;
|
|
1851
1907
|
}
|
|
@@ -1853,11 +1909,14 @@ var RunScheduler = class {
|
|
|
1853
1909
|
list(roomId, finished = 8) {
|
|
1854
1910
|
const mine = this.runs.filter((r) => !roomId || r.roomId === roomId);
|
|
1855
1911
|
const active = mine.filter((r) => r.state === "approval" || r.state === "queued" || r.state === "running");
|
|
1856
|
-
const done = mine.filter((r) => !active.includes(r)).slice(-finished);
|
|
1912
|
+
const done = finished > 0 ? mine.filter((r) => !active.includes(r)).slice(-finished) : [];
|
|
1857
1913
|
return [...active, ...done].map((r) => ({ ...r }));
|
|
1858
1914
|
}
|
|
1859
1915
|
running(roomId) {
|
|
1860
|
-
return this.runs.filter((r) => r.roomId === roomId && r.
|
|
1916
|
+
return this.runs.filter((r) => r.roomId === roomId && this.aborts.has(r.id)).length;
|
|
1917
|
+
}
|
|
1918
|
+
isExecuting(roomId, agentId) {
|
|
1919
|
+
return this.runs.some((r) => r.roomId === roomId && r.agentId === agentId && this.aborts.has(r.id));
|
|
1861
1920
|
}
|
|
1862
1921
|
/** Stop every queued/running run in a room (optionally one agent). Returns how many were stopped. */
|
|
1863
1922
|
stop(roomId, agentId) {
|
|
@@ -1865,6 +1924,7 @@ var RunScheduler = class {
|
|
|
1865
1924
|
for (const r of this.runs) {
|
|
1866
1925
|
if (r.roomId !== roomId || agentId && r.agentId !== agentId) continue;
|
|
1867
1926
|
if (r.state === "approval" || r.state === "queued") {
|
|
1927
|
+
this.gates.get(r.id)?.abort();
|
|
1868
1928
|
this.finish(r, "stopped", "stopped by user");
|
|
1869
1929
|
n++;
|
|
1870
1930
|
} else if (r.state === "running") {
|
|
@@ -1878,18 +1938,18 @@ var RunScheduler = class {
|
|
|
1878
1938
|
}
|
|
1879
1939
|
/** Resolves when no run is approval/queued/running (tests, e2e). */
|
|
1880
1940
|
whenIdle() {
|
|
1881
|
-
if (!this.
|
|
1941
|
+
if (!this.busy()) return Promise.resolve();
|
|
1882
1942
|
return new Promise((r) => this.idleWaiters.push(r));
|
|
1883
1943
|
}
|
|
1884
1944
|
pump() {
|
|
1885
1945
|
for (const run of this.runs) {
|
|
1886
1946
|
if (run.state !== "queued") continue;
|
|
1887
1947
|
if (this.running(run.roomId) >= Math.max(1, this.limitFor(run.roomId))) continue;
|
|
1888
|
-
if (this.
|
|
1948
|
+
if (this.isExecuting(run.roomId, run.agentId)) continue;
|
|
1889
1949
|
this.start(run);
|
|
1890
1950
|
}
|
|
1891
1951
|
this.changed();
|
|
1892
|
-
if (!this.
|
|
1952
|
+
if (!this.busy()) {
|
|
1893
1953
|
const w = this.idleWaiters;
|
|
1894
1954
|
this.idleWaiters = [];
|
|
1895
1955
|
for (const fn of w) fn();
|
|
@@ -1901,7 +1961,9 @@ var RunScheduler = class {
|
|
|
1901
1961
|
run.state = "running";
|
|
1902
1962
|
run.startedAt = Date.now();
|
|
1903
1963
|
const req = this.reqs.get(run.id);
|
|
1904
|
-
Promise.resolve().then(() =>
|
|
1964
|
+
Promise.resolve().then(() => {
|
|
1965
|
+
if (!ac.signal.aborted) return this.exec(run, req, ac.signal);
|
|
1966
|
+
}).then(
|
|
1905
1967
|
() => run.state === "running" && this.finish(run, "done"),
|
|
1906
1968
|
(e) => run.state === "running" && this.finish(run, ac.signal.aborted ? "stopped" : "failed", e?.message ?? String(e))
|
|
1907
1969
|
).finally(() => {
|
|
@@ -1914,9 +1976,17 @@ var RunScheduler = class {
|
|
|
1914
1976
|
run.endedAt = Date.now();
|
|
1915
1977
|
if (error) run.error = error;
|
|
1916
1978
|
this.reqs.delete(run.id);
|
|
1917
|
-
if (this.runs.length > 400)
|
|
1979
|
+
if (this.runs.length > 400) {
|
|
1980
|
+
const active = this.runs.filter((r) => r.state === "approval" || r.state === "queued" || this.aborts.has(r.id) || this.gates.has(r.id));
|
|
1981
|
+
const finished = this.runs.filter((r) => !active.includes(r)).slice(-300);
|
|
1982
|
+
const keep = /* @__PURE__ */ new Set([...active, ...finished]);
|
|
1983
|
+
this.runs = this.runs.filter((r) => keep.has(r));
|
|
1984
|
+
}
|
|
1918
1985
|
this.changed();
|
|
1919
1986
|
}
|
|
1987
|
+
busy() {
|
|
1988
|
+
return this.aborts.size > 0 || this.gates.size > 0 || this.runs.some((r) => r.state === "approval" || r.state === "queued");
|
|
1989
|
+
}
|
|
1920
1990
|
changed() {
|
|
1921
1991
|
try {
|
|
1922
1992
|
this.onChange();
|
|
@@ -1936,19 +2006,23 @@ import { WebSocketServer } from "ws";
|
|
|
1936
2006
|
import { WebSocket as WebSocket2 } from "ws";
|
|
1937
2007
|
var RING = 1e3;
|
|
1938
2008
|
var RATE = 40;
|
|
2009
|
+
var MAX_CIPHERTEXT = 256 * 1024;
|
|
1939
2010
|
var LanRoom = class {
|
|
1940
2011
|
roomId;
|
|
1941
2012
|
socks = /* @__PURE__ */ new Set();
|
|
1942
2013
|
seq = 0;
|
|
1943
2014
|
ring = [];
|
|
1944
2015
|
roomRate = [];
|
|
2016
|
+
identities = /* @__PURE__ */ new Map();
|
|
1945
2017
|
constructor(roomId) {
|
|
1946
2018
|
this.roomId = roomId;
|
|
1947
2019
|
}
|
|
1948
2020
|
attach(ws) {
|
|
1949
|
-
const s = { ws, joined: false, stamps: [] };
|
|
2021
|
+
const s = { ws, joined: false, stamps: [], processing: Promise.resolve() };
|
|
1950
2022
|
this.socks.add(s);
|
|
1951
|
-
ws.on("message", (data) =>
|
|
2023
|
+
ws.on("message", (data) => {
|
|
2024
|
+
s.processing = s.processing.then(() => this.onMessage(s, data.toString())).catch(() => this.error(s, "bad_frame", "invalid frame"));
|
|
2025
|
+
});
|
|
1952
2026
|
ws.on("close", () => this.onClose(s));
|
|
1953
2027
|
ws.on("error", () => this.onClose(s));
|
|
1954
2028
|
}
|
|
@@ -1965,6 +2039,9 @@ var LanRoom = class {
|
|
|
1965
2039
|
this.send(s.ws, frame);
|
|
1966
2040
|
}
|
|
1967
2041
|
}
|
|
2042
|
+
error(s, code, message) {
|
|
2043
|
+
this.send(s.ws, { v: 1, type: "ctrl", method: "error", code, message, roomId: this.roomId });
|
|
2044
|
+
}
|
|
1968
2045
|
rateOk(stamps, now, limit) {
|
|
1969
2046
|
const recent = stamps.filter((t) => now - t < 1e3);
|
|
1970
2047
|
if (recent.length >= limit)
|
|
@@ -1973,22 +2050,26 @@ var LanRoom = class {
|
|
|
1973
2050
|
return { ok: true, next: recent };
|
|
1974
2051
|
}
|
|
1975
2052
|
async onMessage(s, text2) {
|
|
1976
|
-
const now = Date.now();
|
|
1977
|
-
const rr = this.rateOk(this.roomRate, now, 200);
|
|
1978
|
-
this.roomRate = rr.next;
|
|
1979
|
-
if (!rr.ok)
|
|
1980
|
-
return;
|
|
1981
|
-
const sr = this.rateOk(s.stamps, now, RATE);
|
|
1982
|
-
s.stamps = sr.next;
|
|
1983
|
-
if (!sr.ok)
|
|
1984
|
-
return;
|
|
1985
2053
|
for (const line of text2.split("\n")) {
|
|
2054
|
+
if (!this.socks.has(s))
|
|
2055
|
+
return;
|
|
1986
2056
|
if (!line.trim())
|
|
1987
2057
|
continue;
|
|
2058
|
+
const now = Date.now();
|
|
2059
|
+
const rr = this.rateOk(this.roomRate, now, 200);
|
|
2060
|
+
this.roomRate = rr.next;
|
|
2061
|
+
const sr = this.rateOk(s.stamps, now, RATE);
|
|
2062
|
+
s.stamps = sr.next;
|
|
2063
|
+
if (!rr.ok || !sr.ok)
|
|
2064
|
+
return this.error(s, "rate_limit", "too many frames");
|
|
1988
2065
|
let frame;
|
|
1989
2066
|
try {
|
|
1990
|
-
|
|
2067
|
+
const parsed = JSON.parse(line);
|
|
2068
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2069
|
+
throw new Error("not an object");
|
|
2070
|
+
frame = parsed;
|
|
1991
2071
|
} catch {
|
|
2072
|
+
this.error(s, "bad_json", "frame must be a JSON object");
|
|
1992
2073
|
continue;
|
|
1993
2074
|
}
|
|
1994
2075
|
if (frame.v !== 1)
|
|
@@ -2009,11 +2090,19 @@ var LanRoom = class {
|
|
|
2009
2090
|
}
|
|
2010
2091
|
if (frame.type === "ctrl" && frame.method === "poll") {
|
|
2011
2092
|
const since = frame.sinceSeq ?? 0;
|
|
2093
|
+
if (!Number.isSafeInteger(since) || since < 0) {
|
|
2094
|
+
this.error(s, "bad_poll", "invalid sinceSeq");
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2012
2097
|
const frames = this.ring.filter((f) => f.seq > since);
|
|
2013
2098
|
this.send(s.ws, { v: 1, type: "ctrl", method: "poll_result", roomId: this.roomId, frames, seq: this.seq });
|
|
2014
2099
|
continue;
|
|
2015
2100
|
}
|
|
2016
2101
|
if (frame.type === "fwd") {
|
|
2102
|
+
if (frame.roomId !== this.roomId || frame.from !== s.agentId || typeof frame.ciphertext !== "string" || !frame.ciphertext || frame.ciphertext.length > MAX_CIPHERTEXT || typeof frame.nonce !== "string" || !/^[A-Za-z0-9_-]{16}$/.test(frame.nonce)) {
|
|
2103
|
+
this.error(s, "bad_frame", "invalid encrypted frame");
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2017
2106
|
this.seq += 1;
|
|
2018
2107
|
const fwd = { ...frame, seq: this.seq, roomId: this.roomId, from: s.agentId };
|
|
2019
2108
|
this.ring.push(fwd);
|
|
@@ -2028,23 +2117,44 @@ var LanRoom = class {
|
|
|
2028
2117
|
this.send(s.ws, { v: 1, type: "ctrl", method: "error", code: "bad_room", message: "room mismatch", roomId: this.roomId });
|
|
2029
2118
|
return;
|
|
2030
2119
|
}
|
|
2031
|
-
if (!
|
|
2032
|
-
s
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
s
|
|
2120
|
+
if (typeof frame.agentId !== "string" || !frame.agentId || frame.agentId.length > 128 || typeof frame.publicKey !== "string" || !frame.publicKey || frame.publicKey.length > 256 || typeof frame.challengeResp !== "string" || frame.challengeResp.length > 256 || frame.capabilities !== void 0 && (!Array.isArray(frame.capabilities) || frame.capabilities.length > 32 || frame.capabilities.some((c) => typeof c !== "string" || c.length > 128))) {
|
|
2121
|
+
return this.error(s, "bad_join", "invalid identity or capabilities");
|
|
2122
|
+
}
|
|
2123
|
+
const knownKey = this.identities.get(frame.agentId);
|
|
2124
|
+
if (knownKey && knownKey !== frame.publicKey)
|
|
2125
|
+
return this.error(s, "identity_conflict", "agentId belongs to another public key in this room");
|
|
2126
|
+
if (s.joined) {
|
|
2127
|
+
if (frame.agentId !== s.agentId || frame.publicKey !== s.publicKey)
|
|
2128
|
+
return this.error(s, "identity_locked", "joined socket identity cannot change");
|
|
2129
|
+
this.send(s.ws, { v: 1, type: "ctrl", method: "joined", roomId: this.roomId, agentId: s.agentId, peers: this.members(), seq: this.seq });
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (!s.pending || !frame.challengeResp || s.pending.agentId !== frame.agentId || s.pending.publicKey !== frame.publicKey) {
|
|
2133
|
+
s.pending = { challenge: b64uEncode(randomBytes(32)), agentId: frame.agentId, publicKey: frame.publicKey };
|
|
2134
|
+
this.send(s.ws, { v: 1, type: "ctrl", method: "challenge", roomId: this.roomId, challenge: s.pending.challenge });
|
|
2037
2135
|
return;
|
|
2038
2136
|
}
|
|
2039
|
-
const
|
|
2137
|
+
const pending = s.pending;
|
|
2138
|
+
s.pending = void 0;
|
|
2139
|
+
const ok2 = await verifyChallenge(frame.publicKey, pending.challenge, frame.challengeResp).catch(() => false);
|
|
2140
|
+
if (!this.socks.has(s))
|
|
2141
|
+
return;
|
|
2040
2142
|
if (!ok2) {
|
|
2041
2143
|
this.send(s.ws, { v: 1, type: "ctrl", method: "error", code: "bad_sig", message: "challenge failed", roomId: this.roomId });
|
|
2042
2144
|
return;
|
|
2043
2145
|
}
|
|
2146
|
+
const verifiedKey = this.identities.get(frame.agentId);
|
|
2147
|
+
if (verifiedKey && verifiedKey !== frame.publicKey)
|
|
2148
|
+
return this.error(s, "identity_conflict", "agentId belongs to another public key in this room");
|
|
2149
|
+
this.identities.set(frame.agentId, frame.publicKey);
|
|
2044
2150
|
s.agentId = frame.agentId;
|
|
2045
2151
|
s.publicKey = frame.publicKey;
|
|
2046
2152
|
s.capabilities = frame.capabilities;
|
|
2047
2153
|
s.joined = true;
|
|
2154
|
+
for (const old of [...this.socks]) {
|
|
2155
|
+
if (old !== s && old.joined && old.agentId === s.agentId)
|
|
2156
|
+
this.onClose(old);
|
|
2157
|
+
}
|
|
2048
2158
|
const peers = this.members();
|
|
2049
2159
|
this.send(s.ws, { v: 1, type: "ctrl", method: "joined", roomId: this.roomId, agentId: s.agentId, peers, seq: this.seq });
|
|
2050
2160
|
this.broadcast({ v: 1, type: "ctrl", method: "peer_update", roomId: this.roomId, peers, event: "joined", agentId: s.agentId }, s.ws);
|
|
@@ -2158,10 +2268,10 @@ function browseLanRelays(opts) {
|
|
|
2158
2268
|
}
|
|
2159
2269
|
});
|
|
2160
2270
|
dns.query({ questions: [{ name: MDNS_SERVICE, type: "PTR" }] });
|
|
2161
|
-
return new Promise((
|
|
2271
|
+
return new Promise((resolve4) => {
|
|
2162
2272
|
setTimeout(() => {
|
|
2163
2273
|
dns.destroy();
|
|
2164
|
-
|
|
2274
|
+
resolve4([...found.values()]);
|
|
2165
2275
|
}, timeoutMs);
|
|
2166
2276
|
});
|
|
2167
2277
|
}
|
|
@@ -2472,19 +2582,33 @@ var LanRelayServer = class {
|
|
|
2472
2582
|
res.writeHead(200, { "content-type": "text/plain" });
|
|
2473
2583
|
res.end("Collab LAN / VPS Relay\nWS: /ws/:roomId\nDirectory: GET|POST /directory/rooms, DELETE /directory/rooms/:roomId\nHealth: /health\nmDNS: _collab._tcp.local\n");
|
|
2474
2584
|
});
|
|
2475
|
-
this.wss = new WebSocketServer({ noServer: true });
|
|
2585
|
+
this.wss = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024 });
|
|
2476
2586
|
this.http.on("upgrade", (req, socket, head) => {
|
|
2477
|
-
const m = /^\/ws\/([^/?#]+)
|
|
2587
|
+
const m = /^\/ws\/([^/?#]+)\/?(?:\?[^#]*)?$/.exec(req.url ?? "");
|
|
2478
2588
|
if (!m) {
|
|
2479
2589
|
socket.destroy();
|
|
2480
2590
|
return;
|
|
2481
2591
|
}
|
|
2482
|
-
|
|
2592
|
+
let roomId;
|
|
2593
|
+
try {
|
|
2594
|
+
roomId = decodeURIComponent(m[1]);
|
|
2595
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(roomId))
|
|
2596
|
+
throw new Error("invalid roomId");
|
|
2597
|
+
} catch {
|
|
2598
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2483
2601
|
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2484
2602
|
this.room(roomId).attach(ws);
|
|
2485
2603
|
});
|
|
2486
2604
|
});
|
|
2487
|
-
await new Promise((
|
|
2605
|
+
await new Promise((resolve4, reject) => {
|
|
2606
|
+
this.http.once("error", reject);
|
|
2607
|
+
this.http.listen(port, host, () => {
|
|
2608
|
+
this.http.off("error", reject);
|
|
2609
|
+
resolve4();
|
|
2610
|
+
});
|
|
2611
|
+
});
|
|
2488
2612
|
const addr = this.http.address();
|
|
2489
2613
|
this.port = typeof addr === "object" && addr ? addr.port : port;
|
|
2490
2614
|
if (this.opts.mdns !== false) {
|
|
@@ -2494,8 +2618,12 @@ var LanRelayServer = class {
|
|
|
2494
2618
|
}
|
|
2495
2619
|
async close() {
|
|
2496
2620
|
this.stopMdns?.();
|
|
2497
|
-
|
|
2498
|
-
|
|
2621
|
+
for (const ws of this.wss?.clients ?? [])
|
|
2622
|
+
ws.terminate();
|
|
2623
|
+
if (this.wss)
|
|
2624
|
+
await new Promise((resolve4) => this.wss.close(() => resolve4()));
|
|
2625
|
+
if (this.http)
|
|
2626
|
+
await new Promise((resolve4) => this.http.close(() => resolve4()));
|
|
2499
2627
|
}
|
|
2500
2628
|
};
|
|
2501
2629
|
|
|
@@ -2521,8 +2649,9 @@ var ApprovalQueue = class {
|
|
|
2521
2649
|
}
|
|
2522
2650
|
/** Ask for approval; resolves with allow|deny. Auto-denies after timeoutMs. */
|
|
2523
2651
|
request(opts) {
|
|
2652
|
+
if (opts.signal?.aborted) return Promise.resolve("deny");
|
|
2524
2653
|
const requestId = randomUUID();
|
|
2525
|
-
return new Promise((
|
|
2654
|
+
return new Promise((resolve4) => {
|
|
2526
2655
|
const entry = {
|
|
2527
2656
|
requestId,
|
|
2528
2657
|
roomId: opts.roomId,
|
|
@@ -2532,22 +2661,22 @@ var ApprovalQueue = class {
|
|
|
2532
2661
|
from: opts.from,
|
|
2533
2662
|
createdAt: Date.now(),
|
|
2534
2663
|
resolve: (d) => {
|
|
2664
|
+
if (!this.pending.has(requestId)) return;
|
|
2535
2665
|
clearTimeout(timer);
|
|
2666
|
+
opts.signal?.removeEventListener("abort", abort);
|
|
2536
2667
|
this.pending.delete(requestId);
|
|
2537
2668
|
this.emit();
|
|
2538
|
-
|
|
2669
|
+
resolve4(d);
|
|
2539
2670
|
}
|
|
2540
2671
|
};
|
|
2541
|
-
|
|
2542
|
-
this.emit();
|
|
2672
|
+
const abort = () => entry.resolve("deny");
|
|
2543
2673
|
const timer = setTimeout(() => {
|
|
2544
|
-
|
|
2545
|
-
this.pending.delete(requestId);
|
|
2546
|
-
this.emit();
|
|
2547
|
-
resolve3("deny");
|
|
2548
|
-
}
|
|
2674
|
+
entry.resolve("deny");
|
|
2549
2675
|
}, opts.timeoutMs ?? 12e4);
|
|
2550
2676
|
timer.unref?.();
|
|
2677
|
+
this.pending.set(requestId, entry);
|
|
2678
|
+
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
2679
|
+
this.emit();
|
|
2551
2680
|
});
|
|
2552
2681
|
}
|
|
2553
2682
|
decide(requestId, decision) {
|
|
@@ -2564,7 +2693,8 @@ init_member_config();
|
|
|
2564
2693
|
|
|
2565
2694
|
// src/state.ts
|
|
2566
2695
|
import { chmod, mkdir as mkdir4, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
2567
|
-
import { dirname as dirname3 } from "node:path";
|
|
2696
|
+
import { dirname as dirname3, resolve as resolve3 } from "node:path";
|
|
2697
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2568
2698
|
function newId(prefix) {
|
|
2569
2699
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2570
2700
|
}
|
|
@@ -2608,12 +2738,25 @@ async function loadState(path) {
|
|
|
2608
2738
|
return null;
|
|
2609
2739
|
}
|
|
2610
2740
|
}
|
|
2741
|
+
var pendingWrites = /* @__PURE__ */ new Map();
|
|
2611
2742
|
async function saveState(path, state) {
|
|
2743
|
+
path = resolve3(path);
|
|
2744
|
+
const snapshot = structuredClone(state);
|
|
2745
|
+
const pending = (pendingWrites.get(path) ?? Promise.resolve()).catch(() => {
|
|
2746
|
+
}).then(() => writeState(path, snapshot));
|
|
2747
|
+
pendingWrites.set(path, pending);
|
|
2748
|
+
try {
|
|
2749
|
+
await pending;
|
|
2750
|
+
} finally {
|
|
2751
|
+
if (pendingWrites.get(path) === pending) pendingWrites.delete(path);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
async function writeState(path, state) {
|
|
2612
2755
|
const dir = dirname3(path);
|
|
2613
2756
|
await mkdir4(dir, { recursive: true, mode: 448 });
|
|
2614
2757
|
await chmod(dir, 448).catch(() => {
|
|
2615
2758
|
});
|
|
2616
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
2759
|
+
const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
2617
2760
|
const body = {
|
|
2618
2761
|
version: 3,
|
|
2619
2762
|
activeWorkspaceId: state.activeWorkspaceId,
|
|
@@ -2622,9 +2765,13 @@ async function saveState(path, state) {
|
|
|
2622
2765
|
rooms: state.rooms,
|
|
2623
2766
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2624
2767
|
};
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2768
|
+
try {
|
|
2769
|
+
await writeFile3(tmp, JSON.stringify(body, null, 2), { mode: 384, flag: "wx" });
|
|
2770
|
+
await chmod(tmp, 384);
|
|
2771
|
+
await rename(tmp, path);
|
|
2772
|
+
} finally {
|
|
2773
|
+
await rm2(tmp, { force: true });
|
|
2774
|
+
}
|
|
2628
2775
|
}
|
|
2629
2776
|
|
|
2630
2777
|
// src/commands.ts
|
|
@@ -2774,7 +2921,7 @@ var PLUGIN_VERSION = (() => {
|
|
|
2774
2921
|
try {
|
|
2775
2922
|
return requirePkg("../package.json").version;
|
|
2776
2923
|
} catch {
|
|
2777
|
-
return "0.3.
|
|
2924
|
+
return "0.3.4";
|
|
2778
2925
|
}
|
|
2779
2926
|
})();
|
|
2780
2927
|
var SUMMARY_MAX = 120;
|
|
@@ -2782,14 +2929,21 @@ var MESSAGES_MAX = 300;
|
|
|
2782
2929
|
var AGENT_SERVICES = ["agents", "agentDefaultModel", "agentPresets", "permissionPresets", "sessionTitle", "workspaceRegistry"];
|
|
2783
2930
|
var text = (s) => [{ type: "text", text: s }];
|
|
2784
2931
|
var jsonRender = (_args, value) => text(JSON.stringify(value));
|
|
2785
|
-
function withTimeout(p, ms, label) {
|
|
2932
|
+
function withTimeout(p, ms, label, signal) {
|
|
2786
2933
|
let t;
|
|
2934
|
+
let onAbort;
|
|
2787
2935
|
const timeout = new Promise((_, rej) => {
|
|
2788
2936
|
t = setTimeout(() => rej(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
2789
2937
|
t.unref?.();
|
|
2790
2938
|
});
|
|
2791
|
-
|
|
2939
|
+
const aborted = new Promise((_, reject) => {
|
|
2940
|
+
onAbort = () => reject(signal?.reason ?? new Error("stopped by user"));
|
|
2941
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2942
|
+
if (signal?.aborted) onAbort();
|
|
2943
|
+
});
|
|
2944
|
+
return Promise.race([p, timeout, aborted]).finally(() => {
|
|
2792
2945
|
if (t) clearTimeout(t);
|
|
2946
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
2793
2947
|
});
|
|
2794
2948
|
}
|
|
2795
2949
|
async function loadIdentity(path, agentName) {
|
|
@@ -2859,6 +3013,7 @@ var RoomConn = class {
|
|
|
2859
3013
|
locks;
|
|
2860
3014
|
git;
|
|
2861
3015
|
historyTimer;
|
|
3016
|
+
historySave;
|
|
2862
3017
|
announceTimer;
|
|
2863
3018
|
normalized;
|
|
2864
3019
|
/** Normalized once; the same object afterwards so `conn.local.x = …` always sticks. */
|
|
@@ -2920,6 +3075,36 @@ var CollabBridge = class {
|
|
|
2920
3075
|
sessionAgents = /* @__PURE__ */ new Map();
|
|
2921
3076
|
/** Override how a local agent run executes (tests / mock provider). */
|
|
2922
3077
|
localRunner = null;
|
|
3078
|
+
models = [];
|
|
3079
|
+
async refreshModels() {
|
|
3080
|
+
const llm = this.ctx.get("llm", false);
|
|
3081
|
+
if (!llm?.listProviders || !llm?.listModels) return;
|
|
3082
|
+
const results = await Promise.allSettled(llm.listProviders().map(async (p) => {
|
|
3083
|
+
const entries = await withTimeout(llm.listModels(p.id), 5e3, `models ${p.id}`);
|
|
3084
|
+
return entries.map((m) => ({ provider: p.id, model: m.id, name: m.name || m.id }));
|
|
3085
|
+
}));
|
|
3086
|
+
this.models = results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
|
|
3087
|
+
this.emit({ type: "rooms" });
|
|
3088
|
+
}
|
|
3089
|
+
selectedModel(conn, agent) {
|
|
3090
|
+
const defaults = this.ctx.get("agentDefaultModel", false)?.currentSelection?.() ?? {};
|
|
3091
|
+
return resolveSelection(defaults, conn?.meta.memberConfig, agent);
|
|
3092
|
+
}
|
|
3093
|
+
resolvedModel(conn, agent) {
|
|
3094
|
+
return describeModel(this.selectedModel(conn, agent), this.models);
|
|
3095
|
+
}
|
|
3096
|
+
async resetAgentSession(conn, agent) {
|
|
3097
|
+
const key = `${conn.meta.roomId}:${agent.agentId}`;
|
|
3098
|
+
const pending = this.agentHandles.get(key);
|
|
3099
|
+
this.agentHandles.delete(key);
|
|
3100
|
+
const sid = conn.meta.agentSessions?.[agent.agentId];
|
|
3101
|
+
if (sid) this.sessionAgents.delete(sid);
|
|
3102
|
+
if (conn.meta.agentSessions) delete conn.meta.agentSessions[agent.agentId];
|
|
3103
|
+
conn.meta.agentNoEffort = conn.meta.agentNoEffort?.filter((id) => id !== agent.agentId);
|
|
3104
|
+
this.noEffort.delete(key);
|
|
3105
|
+
if (pending) await pending.then((h) => h.dispose()).catch(() => {
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
2923
3108
|
log(level, ...a) {
|
|
2924
3109
|
this.ctx.logger[level]("[collab]", ...a);
|
|
2925
3110
|
const dbg = process.env.DSH_ROOM_DEBUG_LOG;
|
|
@@ -2950,6 +3135,7 @@ var CollabBridge = class {
|
|
|
2950
3135
|
start() {
|
|
2951
3136
|
this.ready ??= (async () => {
|
|
2952
3137
|
await this.self();
|
|
3138
|
+
await this.refreshModels();
|
|
2953
3139
|
const st = await loadState(this.config.statePath);
|
|
2954
3140
|
for (const w of st?.workspaces ?? []) this.workspaces.set(w.id, w);
|
|
2955
3141
|
if (!this.workspaces.size) {
|
|
@@ -2958,12 +3144,17 @@ var CollabBridge = class {
|
|
|
2958
3144
|
}
|
|
2959
3145
|
this.activeWorkspaceId = st?.activeWorkspaceId ?? [...this.workspaces.keys()][0] ?? null;
|
|
2960
3146
|
const metas = st?.rooms ?? [];
|
|
2961
|
-
if (this.config.invite && !metas.some((m) =>
|
|
3147
|
+
if (this.config.invite && !metas.some((m) => m.roomId === CollabClient.parseInvite(this.config.invite).roomId)) {
|
|
2962
3148
|
const { roomId } = CollabClient.parseInvite(this.config.invite);
|
|
2963
3149
|
const wsId = this.activeWorkspaceId;
|
|
2964
3150
|
metas.push({ roomId, invite: this.config.invite, name: roomId, description: "", tags: [], public: false, owner: false, joinedAt: Date.now(), workspaceId: wsId });
|
|
2965
3151
|
}
|
|
2966
3152
|
this.activeRoomId = st?.activeRoomId ?? metas[0]?.roomId ?? null;
|
|
3153
|
+
for (const meta of metas) {
|
|
3154
|
+
const conn = new RoomConn(meta);
|
|
3155
|
+
conn.onAdd = () => this.scheduleHistorySave(conn);
|
|
3156
|
+
this.rooms.set(meta.roomId, conn);
|
|
3157
|
+
}
|
|
2967
3158
|
await this.persist();
|
|
2968
3159
|
this.emit({ type: "rooms" });
|
|
2969
3160
|
void Promise.all(
|
|
@@ -3006,7 +3197,7 @@ var CollabBridge = class {
|
|
|
3006
3197
|
const roomId = parsed.roomId;
|
|
3007
3198
|
const roomKeyB64 = parsed.roomKeyB64;
|
|
3008
3199
|
conn.meta.transport = parsed.mode;
|
|
3009
|
-
const wsUrl =
|
|
3200
|
+
const wsUrl = this.inviteUrl(parsed);
|
|
3010
3201
|
if (!conn.meta.memberConfig) {
|
|
3011
3202
|
conn.meta.memberConfig = defaultMemberConfig(this.config.workspacePath);
|
|
3012
3203
|
}
|
|
@@ -3356,6 +3547,7 @@ var CollabBridge = class {
|
|
|
3356
3547
|
this.emit({ type: "message", roomId: conn.meta.roomId, message: m });
|
|
3357
3548
|
this.routeLocal(conn, full);
|
|
3358
3549
|
}
|
|
3550
|
+
if (conn.local.loopback || conn.local.agents.length) await this.saveLocalHistory(conn);
|
|
3359
3551
|
return { roomId: conn.meta.roomId, ...r };
|
|
3360
3552
|
}
|
|
3361
3553
|
send(roomId, textIn, mentions, replyTo) {
|
|
@@ -3447,7 +3639,7 @@ var CollabBridge = class {
|
|
|
3447
3639
|
bound: !!sessionId,
|
|
3448
3640
|
sessionId,
|
|
3449
3641
|
transport: conn.local.loopback ? "local" : conn.meta.transport ?? (conn.meta.invite.startsWith("collab://lan/") ? "lan" : "relay"),
|
|
3450
|
-
memberConfig: conn.meta.memberConfig ? { ...publicMemberConfig(conn.meta.memberConfig), workspacePath: conn.meta.memberConfig.workspacePath, peerGrants: conn.meta.memberConfig.peerGrants, permissions: conn.meta.memberConfig.permissions } : null,
|
|
3642
|
+
memberConfig: conn.meta.memberConfig ? { ...publicMemberConfig(conn.meta.memberConfig), workspacePath: conn.meta.memberConfig.workspacePath, resolvedModel: this.resolvedModel(conn), peerGrants: conn.meta.memberConfig.peerGrants, permissions: conn.meta.memberConfig.permissions } : null,
|
|
3451
3643
|
peerConfigs: Object.fromEntries(this.peerConfigs.get(conn.meta.roomId) ?? []),
|
|
3452
3644
|
local: this.localView(conn),
|
|
3453
3645
|
runs: this.scheduler.list(conn.meta.roomId)
|
|
@@ -3456,6 +3648,8 @@ var CollabBridge = class {
|
|
|
3456
3648
|
state() {
|
|
3457
3649
|
return {
|
|
3458
3650
|
version: PLUGIN_VERSION,
|
|
3651
|
+
models: this.models,
|
|
3652
|
+
defaultModel: this.resolvedModel(),
|
|
3459
3653
|
self: this.identity?.agentId ?? null,
|
|
3460
3654
|
relayUrl: this.config.relayUrl,
|
|
3461
3655
|
lanRelayPort: this.lanRelay?.port ?? null,
|
|
@@ -3486,7 +3680,20 @@ var CollabBridge = class {
|
|
|
3486
3680
|
workspacePath: patch.workspacePath ?? cur.workspacePath,
|
|
3487
3681
|
workspaceLabel: patch.workspaceLabel ?? cur.workspaceLabel
|
|
3488
3682
|
};
|
|
3683
|
+
const modelChanged = ["provider", "model", "reasoningEffort"].some((k) => cur[k] !== next[k]);
|
|
3684
|
+
if (modelChanged && conn.local.agents.some((a) => this.scheduler.isExecuting(roomId, a.agentId))) throw new Error("Stop running agents before changing the room model");
|
|
3685
|
+
const before = conn.local.agents.map((a) => this.selectedModel(conn, a));
|
|
3489
3686
|
conn.meta.memberConfig = next;
|
|
3687
|
+
if (modelChanged) {
|
|
3688
|
+
const host = this.sessions.get(roomId);
|
|
3689
|
+
this.sessions.delete(roomId);
|
|
3690
|
+
delete conn.meta.sessionId;
|
|
3691
|
+
if (host) await host.then((h) => h.dispose()).catch(() => {
|
|
3692
|
+
});
|
|
3693
|
+
for (const [i, a] of conn.local.agents.entries()) {
|
|
3694
|
+
if (JSON.stringify(before[i]) !== JSON.stringify(this.selectedModel(conn, a))) await this.resetAgentSession(conn, a);
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3490
3697
|
this.mcpByRoom.get(roomId)?.updateConfig(next);
|
|
3491
3698
|
await this.persist();
|
|
3492
3699
|
if (conn.client) await this.announceMemberConfig(conn);
|
|
@@ -3664,7 +3871,7 @@ var CollabBridge = class {
|
|
|
3664
3871
|
const summary = `collab ${kind} from ${from} in ${conn.meta.name}`.slice(0, SUMMARY_MAX);
|
|
3665
3872
|
return Object.freeze({
|
|
3666
3873
|
role: "user",
|
|
3667
|
-
id:
|
|
3874
|
+
id: randomUUID3(),
|
|
3668
3875
|
content: Object.freeze([Object.freeze({ type: "text", text: body })]),
|
|
3669
3876
|
source: Object.freeze({ kind: "collab", roomId: conn.meta.roomId, from, messageId, form: "notice", summary })
|
|
3670
3877
|
});
|
|
@@ -3728,13 +3935,13 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3728
3935
|
const wsPath = roomPath && roomPath.trim() ? roomPath : cfg.workspacePath;
|
|
3729
3936
|
await mkdir5(wsPath, { recursive: true });
|
|
3730
3937
|
const workspace = await svc.workspaceRegistry.create(wsPath, wsPath === cfg.workspacePath ? "collab" : void 0);
|
|
3731
|
-
const selected =
|
|
3732
|
-
const sessionId = `collab-${roomId}-${
|
|
3938
|
+
const selected = this.selectedModel(conn);
|
|
3939
|
+
const sessionId = `collab-${roomId}-${randomUUID3().slice(0, 8)}`;
|
|
3733
3940
|
const handle = await svc.agents.create({
|
|
3734
3941
|
sessionId,
|
|
3735
3942
|
signal: this.abort.signal,
|
|
3736
3943
|
meta: { cwd: workspace.path, agentPreset: preset.id },
|
|
3737
|
-
agentOptions: { provider: selected.provider, model: selected.model },
|
|
3944
|
+
agentOptions: { provider: selected.provider, model: selected.model, ...selected.reasoningEffort ? { reasoningEffort: selected.reasoningEffort } : {} },
|
|
3738
3945
|
setup: async (agentCtx) => {
|
|
3739
3946
|
await svc.agentPresets.mount(agentCtx, preset.id);
|
|
3740
3947
|
}
|
|
@@ -3755,6 +3962,9 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3755
3962
|
return pending;
|
|
3756
3963
|
}
|
|
3757
3964
|
// ── local multi-agent: members hosted by this dsh ────────────────────────
|
|
3965
|
+
inviteUrl(parsed) {
|
|
3966
|
+
return inviteWsUrl(parsed.uri.startsWith("collab://join/") ? { ...parsed, wsBase: this.config.relayUrl } : parsed);
|
|
3967
|
+
}
|
|
3758
3968
|
transportFor(conn, wsUrl) {
|
|
3759
3969
|
return conn.local.loopback ? new MockTransport(this.hub, `loopback:${conn.meta.roomId}`) : new WsTransport(wsUrl);
|
|
3760
3970
|
}
|
|
@@ -3781,7 +3991,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3781
3991
|
identity,
|
|
3782
3992
|
roomId: parsed.roomId,
|
|
3783
3993
|
roomKey: parsed.roomKeyB64,
|
|
3784
|
-
transport: this.transportFor(conn,
|
|
3994
|
+
transport: this.transportFor(conn, this.inviteUrl(parsed)),
|
|
3785
3995
|
autoReply: "off",
|
|
3786
3996
|
reconnect: !conn.local.loopback,
|
|
3787
3997
|
capabilities: ["collab.v1", "dsh", "member.config", "local-agent"]
|
|
@@ -3880,13 +4090,11 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3880
4090
|
const clean = sanitizeAgentInput(input);
|
|
3881
4091
|
const sessionKeys = ["model", "provider", "reasoningEffort", "scope"];
|
|
3882
4092
|
const resetSession = sessionKeys.some((k) => k in clean && clean[k] !== a[k]);
|
|
4093
|
+
if (resetSession && this.scheduler.isExecuting(roomId, a.agentId)) throw new Error("Stop this agent before changing its model or scope");
|
|
3883
4094
|
if (clean.moderator) for (const x of conn.local.agents) x.moderator = false;
|
|
3884
4095
|
Object.assign(a, clean, { id: a.id, agentId: a.agentId });
|
|
3885
4096
|
if (resetSession) {
|
|
3886
|
-
this.
|
|
3887
|
-
if (conn.meta.agentSessions) delete conn.meta.agentSessions[a.agentId];
|
|
3888
|
-
conn.meta.agentNoEffort = conn.meta.agentNoEffort?.filter((x) => x !== a.agentId);
|
|
3889
|
-
this.noEffort.delete(`${conn.meta.roomId}:${a.agentId}`);
|
|
4097
|
+
await this.resetAgentSession(conn, a);
|
|
3890
4098
|
}
|
|
3891
4099
|
await this.persist();
|
|
3892
4100
|
await this.announceAgentConfig(conn, a).catch(() => {
|
|
@@ -3944,6 +4152,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3944
4152
|
await conn.client?.dispose().catch(() => {
|
|
3945
4153
|
});
|
|
3946
4154
|
conn.client = null;
|
|
4155
|
+
conn.lastSeenPeers.clear();
|
|
3947
4156
|
conn.local.loopback = wantLoopback;
|
|
3948
4157
|
conn.status = "connecting";
|
|
3949
4158
|
this.emit({ type: "rooms" });
|
|
@@ -3957,19 +4166,31 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
3957
4166
|
return join3(dirname4(this.config.statePath), "local-history", `${roomId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
|
|
3958
4167
|
}
|
|
3959
4168
|
scheduleHistorySave(conn) {
|
|
3960
|
-
if (!conn.local.loopback || conn.historyTimer) return;
|
|
4169
|
+
if (!conn.local.loopback && !conn.local.agents.length || conn.historyTimer) return;
|
|
3961
4170
|
conn.historyTimer = setTimeout(() => {
|
|
3962
4171
|
conn.historyTimer = void 0;
|
|
3963
|
-
void this.saveLocalHistory(conn);
|
|
4172
|
+
void this.saveLocalHistory(conn).catch((e) => this.log("error", `save history ${conn.meta.roomId}: ${e.message}`));
|
|
3964
4173
|
}, 500);
|
|
3965
4174
|
conn.historyTimer.unref?.();
|
|
3966
4175
|
}
|
|
3967
|
-
|
|
4176
|
+
saveLocalHistory(conn) {
|
|
4177
|
+
clearTimeout(conn.historyTimer);
|
|
4178
|
+
conn.historyTimer = void 0;
|
|
3968
4179
|
const f = this.historyPath(conn.meta.roomId);
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
4180
|
+
const body = JSON.stringify(conn.messages.slice(-MESSAGES_MAX));
|
|
4181
|
+
const save = (conn.historySave ?? Promise.resolve()).catch(() => {
|
|
4182
|
+
}).then(async () => {
|
|
4183
|
+
await mkdir5(dirname4(f), { recursive: true, mode: 448 });
|
|
4184
|
+
const tmp = `${f}.${randomUUID3()}.tmp`;
|
|
4185
|
+
try {
|
|
4186
|
+
await writeFile4(tmp, body, { mode: 384, flag: "wx" });
|
|
4187
|
+
await rename2(tmp, f);
|
|
4188
|
+
} finally {
|
|
4189
|
+
await rm3(tmp, { force: true });
|
|
4190
|
+
}
|
|
3972
4191
|
});
|
|
4192
|
+
conn.historySave = save;
|
|
4193
|
+
return save;
|
|
3973
4194
|
}
|
|
3974
4195
|
async loadLocalHistory(conn) {
|
|
3975
4196
|
try {
|
|
@@ -4044,7 +4265,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4044
4265
|
tasks: opts.tasks
|
|
4045
4266
|
};
|
|
4046
4267
|
if (a.approvalRequired) {
|
|
4047
|
-
req.gate = async () => await this.approvals.request({ roomId: conn.meta.roomId, action: "shell.exec", summary: `Run local agent ${a.name} (${a.agentId})`, detail: summary, from: a.agentId, timeoutMs: 10 * 6e4 }) === "allow";
|
|
4268
|
+
req.gate = async (signal) => await this.approvals.request({ roomId: conn.meta.roomId, action: "shell.exec", summary: `Run local agent ${a.name} (${a.agentId})`, detail: summary, from: a.agentId, timeoutMs: 10 * 6e4, signal }) === "allow";
|
|
4048
4269
|
}
|
|
4049
4270
|
this.scheduler.enqueue(req);
|
|
4050
4271
|
}
|
|
@@ -4073,13 +4294,14 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4073
4294
|
conn.runThread.set(a.agentId, run.threadId);
|
|
4074
4295
|
try {
|
|
4075
4296
|
const cwd = await this.agentCwd(conn, a);
|
|
4297
|
+
signal.throwIfAborted();
|
|
4076
4298
|
const prompt = this.buildLocalPrompt(conn, a, mode, req.trigger, run.threadId, cwd, req.tasks);
|
|
4077
4299
|
const ctx = { bridge: this, roomId: conn.meta.roomId, agent: a, prompt, mode, trigger: req.trigger, threadId: run.threadId, cwd, signal };
|
|
4078
4300
|
const wtBefore = conn.worktrees.get(a.agentId);
|
|
4079
4301
|
const before = wtBefore ? await mainStatus(wtBefore.repo).catch(() => null) : null;
|
|
4080
4302
|
try {
|
|
4081
4303
|
if (this.localRunner) await this.localRunner(ctx);
|
|
4082
|
-
else if (this.config.localAgentRunner === "mock"
|
|
4304
|
+
else if (this.config.localAgentRunner === "mock") await mockLocalRunner(ctx);
|
|
4083
4305
|
else await this.runInSession(conn, a, ctx);
|
|
4084
4306
|
} finally {
|
|
4085
4307
|
if (wtBefore && before) {
|
|
@@ -4123,49 +4345,59 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4123
4345
|
return root;
|
|
4124
4346
|
}
|
|
4125
4347
|
async runInSession(conn, a, ctx, retried = false) {
|
|
4348
|
+
ctx.signal.throwIfAborted();
|
|
4126
4349
|
const handle = await this.agentSessionFor(conn, a, ctx.cwd);
|
|
4350
|
+
ctx.signal.throwIfAborted();
|
|
4127
4351
|
const agent = handle.agent;
|
|
4128
|
-
|
|
4129
|
-
const seq0 = agent.session?.seq;
|
|
4130
|
-
agent.followup(this.notice(conn, ctx.prompt, ctx.trigger?.sender ?? "room", ctx.trigger?.messageId ?? randomUUID2(), `${ctx.mode} \u2192 ${a.name}`));
|
|
4131
|
-
if (!agent.whenIdle) return;
|
|
4132
|
-
const aborted = new Promise((resolve3) => ctx.signal.addEventListener("abort", () => resolve3(), { once: true }));
|
|
4133
|
-
const timeout = new Promise((resolve3) => {
|
|
4134
|
-
const t = setTimeout(resolve3, 20 * 6e4);
|
|
4135
|
-
t.unref?.();
|
|
4136
|
-
});
|
|
4137
|
-
const settled = (async () => {
|
|
4138
|
-
for (let i = 0; i < 40; i++) {
|
|
4139
|
-
await agent.whenIdle();
|
|
4140
|
-
if (seq0 === void 0 || agent.session?.seq !== seq0) return;
|
|
4141
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
4142
|
-
}
|
|
4143
|
-
})();
|
|
4144
|
-
await Promise.race([settled, aborted, timeout]);
|
|
4145
|
-
this.log("info", `run ${a.agentId}: seq ${seq0} -> ${agent.session?.seq}`);
|
|
4146
|
-
if (process.env.DSH_ROOM_DEBUG_LOG && seq0 !== void 0) {
|
|
4147
|
-
const sess = agent.session;
|
|
4148
|
-
for (let i = seq0; i < (sess.seq ?? seq0); i++) this.log("info", ` ev ${i}: ${JSON.stringify(sess.eventAt?.(i)).slice(0, 700)}`);
|
|
4149
|
-
}
|
|
4150
|
-
if (!ctx.signal.aborted && seq0 !== void 0 && agent.session?.seq === seq0) throw new Error("agent session did not start a turn");
|
|
4151
|
-
const failure = !ctx.signal.aborted && seq0 !== void 0 ? this.turnError(agent.session, seq0) : void 0;
|
|
4152
|
-
if (failure) {
|
|
4153
|
-
const key = `${conn.meta.roomId}:${a.agentId}`;
|
|
4154
|
-
if (!retried && failure.code === "UNSUPPORTED_REASONING_EFFORT" && a.reasoningEffort) {
|
|
4155
|
-
this.log("warn", `${a.agentId}: ${failure.message}; recreating its session with the model's default effort`);
|
|
4156
|
-
this.noEffort.add(key);
|
|
4157
|
-
conn.meta.agentNoEffort = [.../* @__PURE__ */ new Set([...conn.meta.agentNoEffort ?? [], a.agentId])];
|
|
4158
|
-
this.agentHandles.delete(key);
|
|
4159
|
-
if (conn.meta.agentSessions) delete conn.meta.agentSessions[a.agentId];
|
|
4160
|
-
return this.runInSession(conn, a, ctx, true);
|
|
4161
|
-
}
|
|
4162
|
-
throw new Error(failure.message);
|
|
4163
|
-
}
|
|
4164
|
-
if (ctx.signal.aborted) {
|
|
4352
|
+
const cancel = () => {
|
|
4165
4353
|
try {
|
|
4166
4354
|
agent.cancel?.({ kind: "user" });
|
|
4167
4355
|
} catch {
|
|
4168
4356
|
}
|
|
4357
|
+
};
|
|
4358
|
+
ctx.signal.addEventListener("abort", cancel, { once: true });
|
|
4359
|
+
try {
|
|
4360
|
+
if (agent.whenIdle) await withTimeout(agent.whenIdle(), 3e4, `session idle ${a.agentId}`, ctx.signal);
|
|
4361
|
+
ctx.signal.throwIfAborted();
|
|
4362
|
+
const seq0 = agent.session?.seq;
|
|
4363
|
+
agent.followup(this.notice(conn, ctx.prompt, ctx.trigger?.sender ?? "room", ctx.trigger?.messageId ?? randomUUID3(), `${ctx.mode} \u2192 ${a.name}`));
|
|
4364
|
+
if (!agent.whenIdle) return;
|
|
4365
|
+
const settled = (async () => {
|
|
4366
|
+
for (let i = 0; i < 40; i++) {
|
|
4367
|
+
ctx.signal.throwIfAborted();
|
|
4368
|
+
await agent.whenIdle();
|
|
4369
|
+
ctx.signal.throwIfAborted();
|
|
4370
|
+
if (seq0 === void 0 || agent.session?.seq !== seq0) return;
|
|
4371
|
+
await sleep2(250, ctx.signal);
|
|
4372
|
+
}
|
|
4373
|
+
})();
|
|
4374
|
+
await withTimeout(settled, 20 * 6e4, `agent turn ${a.agentId}`, ctx.signal);
|
|
4375
|
+
this.log("info", `run ${a.agentId}: seq ${seq0} -> ${agent.session?.seq}`);
|
|
4376
|
+
if (process.env.DSH_ROOM_DEBUG_LOG && seq0 !== void 0) {
|
|
4377
|
+
const sess = agent.session;
|
|
4378
|
+
for (let i = seq0; i < (sess.seq ?? seq0); i++) this.log("info", ` ev ${i}: ${JSON.stringify(sess.eventAt?.(i)).slice(0, 700)}`);
|
|
4379
|
+
}
|
|
4380
|
+
if (!ctx.signal.aborted && seq0 !== void 0 && agent.session?.seq === seq0) throw new Error("agent session did not start a turn");
|
|
4381
|
+
const failure = !ctx.signal.aborted && seq0 !== void 0 ? this.turnError(agent.session, seq0) : void 0;
|
|
4382
|
+
if (failure) {
|
|
4383
|
+
const key = `${conn.meta.roomId}:${a.agentId}`;
|
|
4384
|
+
if (!retried && failure.code === "UNSUPPORTED_REASONING_EFFORT" && this.selectedModel(conn, a).reasoningEffort) {
|
|
4385
|
+
this.log("warn", `${a.agentId}: ${failure.message}; recreating its session with the model's default effort`);
|
|
4386
|
+
this.noEffort.add(key);
|
|
4387
|
+
conn.meta.agentNoEffort = [.../* @__PURE__ */ new Set([...conn.meta.agentNoEffort ?? [], a.agentId])];
|
|
4388
|
+
this.agentHandles.delete(key);
|
|
4389
|
+
if (conn.meta.agentSessions) delete conn.meta.agentSessions[a.agentId];
|
|
4390
|
+
return this.runInSession(conn, a, ctx, true);
|
|
4391
|
+
}
|
|
4392
|
+
throw new Error(failure.message);
|
|
4393
|
+
}
|
|
4394
|
+
} catch (e) {
|
|
4395
|
+
cancel();
|
|
4396
|
+
if (agent.whenIdle) await withTimeout(agent.whenIdle(), 3e4, `agent stop ${a.agentId}`).catch(() => {
|
|
4397
|
+
});
|
|
4398
|
+
throw e;
|
|
4399
|
+
} finally {
|
|
4400
|
+
ctx.signal.removeEventListener("abort", cancel);
|
|
4169
4401
|
}
|
|
4170
4402
|
}
|
|
4171
4403
|
/** Last turn/end error since `from`, if the agent's turn failed (provider/model errors). */
|
|
@@ -4203,15 +4435,15 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4203
4435
|
try {
|
|
4204
4436
|
const root = conn.meta.memberConfig?.workspacePath?.trim() || this.config.workspacePath;
|
|
4205
4437
|
const workspace = await svc.workspaceRegistry.create(root, root === this.config.workspacePath ? "collab" : void 0);
|
|
4206
|
-
const
|
|
4207
|
-
const base = { provider:
|
|
4208
|
-
const withEffort =
|
|
4438
|
+
const effective = this.selectedModel(conn, a);
|
|
4439
|
+
const base = { provider: effective.provider, model: effective.model };
|
|
4440
|
+
const withEffort = effective.reasoningEffort && !this.noEffort.has(key) && !conn.meta.agentNoEffort?.includes(a.agentId);
|
|
4209
4441
|
const resume = svc.agents.resume;
|
|
4210
4442
|
if (prevSid && resume) {
|
|
4211
4443
|
try {
|
|
4212
4444
|
const h = await resume.call(svc.agents, {
|
|
4213
4445
|
resumeSessionId: prevSid,
|
|
4214
|
-
agentOptions: withEffort ? { ...base, reasoningEffort:
|
|
4446
|
+
agentOptions: withEffort ? { ...base, reasoningEffort: effective.reasoningEffort } : base,
|
|
4215
4447
|
setup: async (agentCtx) => {
|
|
4216
4448
|
await svc.agentPresets.mount(agentCtx, preset.id);
|
|
4217
4449
|
}
|
|
@@ -4229,7 +4461,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4229
4461
|
this.log("warn", `resume ${prevSid} for ${a.agentId} failed (${e.message}); creating a new session`);
|
|
4230
4462
|
}
|
|
4231
4463
|
}
|
|
4232
|
-
const sessionId = `room-${conn.meta.roomId.slice(-8)}-${a.id}-${
|
|
4464
|
+
const sessionId = `room-${conn.meta.roomId.slice(-8)}-${a.id}-${randomUUID3().slice(0, 6)}`;
|
|
4233
4465
|
const create = (agentOptions, cwdPath) => svc.agents.create({
|
|
4234
4466
|
sessionId,
|
|
4235
4467
|
signal: this.abort.signal,
|
|
@@ -4241,7 +4473,7 @@ Do what is asked using your tools. Answer in the room by calling collab_send wit
|
|
|
4241
4473
|
});
|
|
4242
4474
|
let handle;
|
|
4243
4475
|
try {
|
|
4244
|
-
handle = await create(withEffort ? { ...base, reasoningEffort:
|
|
4476
|
+
handle = await create(withEffort ? { ...base, reasoningEffort: effective.reasoningEffort } : base, root);
|
|
4245
4477
|
} catch (e) {
|
|
4246
4478
|
this.log("warn", `session for ${a.agentId} with reasoning effort failed (${e.message}); retrying with defaults`);
|
|
4247
4479
|
handle = await create(base, root);
|
|
@@ -4360,6 +4592,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4360
4592
|
this.emit({ type: "message", roomId: conn.meta.roomId, message: m });
|
|
4361
4593
|
this.routeLocal(conn, full);
|
|
4362
4594
|
}
|
|
4595
|
+
if (conn.local.loopback || conn.local.agents.length) await this.saveLocalHistory(conn);
|
|
4363
4596
|
return { roomId: conn.meta.roomId, ...r };
|
|
4364
4597
|
}
|
|
4365
4598
|
// ── merge view: per-agent worktree diffs ─────────────────────────────────
|
|
@@ -4396,7 +4629,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4396
4629
|
if (!approved) throw new Error("merge needs explicit approval");
|
|
4397
4630
|
const conn = this.room(roomId);
|
|
4398
4631
|
const a = this.localAgent(conn, agentId);
|
|
4399
|
-
if (this.scheduler.
|
|
4632
|
+
if (this.scheduler.isExecuting(conn.meta.roomId, a.agentId)) throw new Error(`${a.name} is still running \u2014 wait for it to finish stopping`);
|
|
4400
4633
|
const wt = conn.worktrees.get(a.agentId) ?? await this.findWorktree(conn, a);
|
|
4401
4634
|
if (!wt) throw new Error(`${a.name} has no worktree yet`);
|
|
4402
4635
|
const r = await mergeWorktree(wt, a.agentId);
|
|
@@ -4410,7 +4643,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4410
4643
|
if (!approved) throw new Error("discard needs explicit approval");
|
|
4411
4644
|
const conn = this.room(roomId);
|
|
4412
4645
|
const a = this.localAgent(conn, agentId);
|
|
4413
|
-
if (this.scheduler.
|
|
4646
|
+
if (this.scheduler.isExecuting(conn.meta.roomId, a.agentId)) throw new Error(`${a.name} is still running \u2014 wait for it to finish stopping`);
|
|
4414
4647
|
const wt = conn.worktrees.get(a.agentId) ?? await this.findWorktree(conn, a);
|
|
4415
4648
|
if (!wt) return { ok: true, discarded: false };
|
|
4416
4649
|
await discardWorktree(wt);
|
|
@@ -4444,6 +4677,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4444
4677
|
const wt = conn.worktrees.get(a.agentId);
|
|
4445
4678
|
return {
|
|
4446
4679
|
...a,
|
|
4680
|
+
resolvedModel: this.resolvedModel(conn, a),
|
|
4447
4681
|
sessionId: conn.meta.agentSessions?.[a.agentId],
|
|
4448
4682
|
online: online.has(a.agentId) || !!conn.agents.get(a.agentId)?.isJoined(),
|
|
4449
4683
|
state: r?.state ?? "idle",
|
|
@@ -4469,8 +4703,10 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4469
4703
|
for (const conn of this.rooms.values()) {
|
|
4470
4704
|
if (conn.heartbeat) clearInterval(conn.heartbeat);
|
|
4471
4705
|
if (conn.presenceTimer) clearInterval(conn.presenceTimer);
|
|
4706
|
+
clearTimeout(conn.announceTimer);
|
|
4707
|
+
clearTimeout(conn.historyTimer);
|
|
4472
4708
|
this.scheduler.stop(conn.meta.roomId);
|
|
4473
|
-
if (conn.local.loopback) await this.saveLocalHistory(conn);
|
|
4709
|
+
if (conn.local.loopback || conn.local.agents.length) await this.saveLocalHistory(conn);
|
|
4474
4710
|
await this.disconnectAgents(conn);
|
|
4475
4711
|
await conn.client?.dispose().catch(() => {
|
|
4476
4712
|
});
|
|
@@ -4497,7 +4733,7 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4497
4733
|
}
|
|
4498
4734
|
const who = bridge.callerAgent(exec);
|
|
4499
4735
|
bridge.log("info", `tool ${name2} by ${who?.agent.agentId ?? "host"}`);
|
|
4500
|
-
return await run(a, who) ?? { ok: true };
|
|
4736
|
+
return JSON.parse(JSON.stringify(await run(a, who) ?? { ok: true }));
|
|
4501
4737
|
}
|
|
4502
4738
|
});
|
|
4503
4739
|
const str2 = (v) => typeof v === "string" && v ? v : void 0;
|
|
@@ -4559,8 +4795,8 @@ Do what is asked, then answer with collab_send replyTo "${p.messageId}".`;
|
|
|
4559
4795
|
];
|
|
4560
4796
|
}
|
|
4561
4797
|
};
|
|
4562
|
-
var sleep2 = (ms, signal) => new Promise((
|
|
4563
|
-
const t = setTimeout(
|
|
4798
|
+
var sleep2 = (ms, signal) => new Promise((resolve4, reject) => {
|
|
4799
|
+
const t = setTimeout(resolve4, ms);
|
|
4564
4800
|
signal?.addEventListener("abort", () => {
|
|
4565
4801
|
clearTimeout(t);
|
|
4566
4802
|
reject(new Error("stopped"));
|
|
@@ -4608,7 +4844,7 @@ Written by ${a.agentId} (${a.name}) at ${(/* @__PURE__ */ new Date()).toISOStrin
|
|
|
4608
4844
|
|
|
4609
4845
|
// src/config.ts
|
|
4610
4846
|
import { homedir } from "node:os";
|
|
4611
|
-
import { join as join4, isAbsolute as
|
|
4847
|
+
import { join as join4, isAbsolute as isAbsolute3 } from "node:path";
|
|
4612
4848
|
var AUTO = /* @__PURE__ */ new Set(["off", "mention", "mention-or-task"]);
|
|
4613
4849
|
function expand(p) {
|
|
4614
4850
|
return p.startsWith("~/") ? join4(homedir(), p.slice(2)) : p;
|
|
@@ -4621,7 +4857,7 @@ function normalizeConfig(raw) {
|
|
|
4621
4857
|
const relayUrl = str2("relayUrl") ?? DEFAULT_RELAY_URL;
|
|
4622
4858
|
if (!/^wss?:\/\//.test(relayUrl)) throw new Error(`dsh-room: relayUrl must be ws:// or wss://, got ${relayUrl}`);
|
|
4623
4859
|
const workspacePath = expand(str2("workspacePath") ?? "~/.collab/workspace");
|
|
4624
|
-
if (!
|
|
4860
|
+
if (!isAbsolute3(workspacePath)) throw new Error("dsh-room: workspacePath must be absolute");
|
|
4625
4861
|
return {
|
|
4626
4862
|
relayUrl,
|
|
4627
4863
|
invite: str2("invite"),
|
|
@@ -4837,6 +5073,27 @@ async function readJson2(req) {
|
|
|
4837
5073
|
}
|
|
4838
5074
|
var str = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
4839
5075
|
var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : typeof v === "string" ? v.split(/[\s,]+/).filter(Boolean) : void 0;
|
|
5076
|
+
function peerGrants(value) {
|
|
5077
|
+
const object = (v) => {
|
|
5078
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new HttpError(400, "peerGrants must contain objects");
|
|
5079
|
+
return v;
|
|
5080
|
+
};
|
|
5081
|
+
const grants = (v) => {
|
|
5082
|
+
const input2 = object(v), out = {};
|
|
5083
|
+
for (const cap of ["read", "write", "exec", "delete"]) {
|
|
5084
|
+
if (!(cap in input2)) continue;
|
|
5085
|
+
const grant = input2[cap];
|
|
5086
|
+
if (grant !== "auto" && grant !== "ask" && grant !== "deny") throw new HttpError(400, `invalid peer grant for ${cap}`);
|
|
5087
|
+
out[cap] = grant;
|
|
5088
|
+
}
|
|
5089
|
+
return out;
|
|
5090
|
+
};
|
|
5091
|
+
const input = object(value);
|
|
5092
|
+
return {
|
|
5093
|
+
default: "default" in input ? grants(input.default) : {},
|
|
5094
|
+
byAgent: "byAgent" in input ? Object.fromEntries(Object.entries(object(input.byAgent)).map(([id, v]) => [id, grants(v)])) : {}
|
|
5095
|
+
};
|
|
5096
|
+
}
|
|
4840
5097
|
function createApiHandler(bridge, connection) {
|
|
4841
5098
|
return async (req, res) => {
|
|
4842
5099
|
const conn = connection();
|
|
@@ -4960,6 +5217,7 @@ function createApiHandler(bridge, connection) {
|
|
|
4960
5217
|
if (k in b) patch[k] = b[k];
|
|
4961
5218
|
}
|
|
4962
5219
|
if (b.permissions && typeof b.permissions === "object") patch.permissions = b.permissions;
|
|
5220
|
+
if ("peerGrants" in b) patch.peerGrants = peerGrants(b.peerGrants);
|
|
4963
5221
|
return sendJson2(res, 200, await bridge.updateMemberConfig(roomId, patch));
|
|
4964
5222
|
}
|
|
4965
5223
|
if (method === "GET" && action === "mcp") return sendJson2(res, 200, { tools: (await Promise.resolve().then(() => (init_mcp_workspace(), mcp_workspace_exports))).MCP_TOOL_DEFS });
|
|
@@ -4975,7 +5233,7 @@ function createApiHandler(bridge, connection) {
|
|
|
4975
5233
|
}
|
|
4976
5234
|
throw new HttpError(404, `no route ${method} ${API}${path}`);
|
|
4977
5235
|
} catch (e) {
|
|
4978
|
-
const status = e instanceof HttpError ? e.status : /not in room|not in a collab room|no local agent/.test(e.message) ? 404 : /needs explicit approval
|
|
5236
|
+
const status = e instanceof HttpError ? e.status : /not in room|not in a collab room|no local agent/.test(e.message) ? 404 : /needs explicit approval|peer grant deny|^denied:/.test(e.message) ? 403 : 500;
|
|
4979
5237
|
if (!res.headersSent) sendJson2(res, status, { error: e.message });
|
|
4980
5238
|
}
|
|
4981
5239
|
};
|
|
@@ -5026,6 +5284,9 @@ function apply(ctx, config) {
|
|
|
5026
5284
|
ctx.inject?.(["webServer"], (c) => {
|
|
5027
5285
|
c.effect(() => registerRoutes(bridge, c.webServer, () => c.get("connection")), "dsh-room: /api/collab routes");
|
|
5028
5286
|
});
|
|
5287
|
+
ctx.inject?.(["llm", "agentDefaultModel"], () => {
|
|
5288
|
+
void bridge.refreshModels().catch((e) => bridge.log("warn", `model catalog: ${e.message}`));
|
|
5289
|
+
});
|
|
5029
5290
|
bridge.start().catch((e) => bridge.log("error", `start failed: ${e.message}`));
|
|
5030
5291
|
bridge.log("info", `loaded (relay ${bridge.config.relayUrl}, autoReply ${bridge.config.autoReply}, ${unregister.length} tools)`);
|
|
5031
5292
|
}
|