pinomad 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{chunk-3tf23vjq.js → chunk-1d9sppj0.js} +1 -1
- package/dist/chunk-26d04z02.js +126 -0
- package/dist/chunk-4by4x5c4.js +1376 -0
- package/dist/{chunk-kxws8b83.js → chunk-5ng86fmy.js} +4 -4
- package/dist/chunk-9d59jjmt.js +144 -0
- package/dist/{chunk-t9y2wfk5.js → chunk-aqj4wg33.js} +98 -6
- package/dist/{chunk-n60kkf1m.js → chunk-crv3dqzp.js} +4 -4
- package/dist/{chunk-tfqc1djy.js → chunk-d4wdt5qs.js} +976 -306
- package/dist/chunk-ebfaf3k2.js +190 -0
- package/dist/chunk-f0v3sht0.js +136 -0
- package/dist/{chunk-3qn8gbjb.js → chunk-gqr7ahv4.js} +1 -1
- package/dist/{chunk-c155p0t0.js → chunk-hsp12cjt.js} +111 -8
- package/dist/chunk-kmbh44dt.js +704 -0
- package/dist/{chunk-bggesvnj.js → chunk-m4q9wafw.js} +28 -92
- package/dist/{chunk-syf0z68f.js → chunk-zttf1hvh.js} +2 -2
- package/dist/pinomad.js +16 -11
- package/package.json +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/index-BkF0-im5.css +1 -0
- package/web/assets/index-D4lgm3Y4.js +383 -0
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-4dj2vb8t.js +0 -284
- package/dist/chunk-7fqn69ys.js +0 -33
- package/dist/chunk-fxaewke2.js +0 -113
- package/dist/chunk-z4x88s0q.js +0 -347
- package/web/assets/index-CBXdQLua.js +0 -383
- package/web/assets/index-CG33GBxM.css +0 -1
|
@@ -2,43 +2,44 @@
|
|
|
2
2
|
import {
|
|
3
3
|
serveWebClient
|
|
4
4
|
} from "./chunk-r6w5aqkw.js";
|
|
5
|
-
import {
|
|
6
|
-
packagedVersion,
|
|
7
|
-
webRoot
|
|
8
|
-
} from "./chunk-3tf23vjq.js";
|
|
9
|
-
import {
|
|
10
|
-
HOST_SETTING_KEYS,
|
|
11
|
-
INVALID_FRAME_CLOSE_CODE,
|
|
12
|
-
PROTOCOL,
|
|
13
|
-
THINKING_LEVELS,
|
|
14
|
-
UNAUTHORIZED_CLOSE_CODE,
|
|
15
|
-
isQueueMode,
|
|
16
|
-
isThinkingLevel,
|
|
17
|
-
parseClientFrame
|
|
18
|
-
} from "./chunk-c155p0t0.js";
|
|
19
5
|
import {
|
|
20
6
|
DevicesDoc,
|
|
7
|
+
RevocationsDoc,
|
|
8
|
+
admitDevice,
|
|
21
9
|
ensureDevices,
|
|
22
|
-
isRegistered,
|
|
23
10
|
loadHostKey,
|
|
24
11
|
loadRelayKey,
|
|
12
|
+
normalizeOrigin,
|
|
25
13
|
pairingOffers,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
readServerBinding,
|
|
15
|
+
readServerConfig,
|
|
16
|
+
resolveRelayOrigin
|
|
17
|
+
} from "./chunk-ebfaf3k2.js";
|
|
29
18
|
import {
|
|
30
|
-
RELAY_CLOSE,
|
|
31
19
|
SECURE_PROLOGUE,
|
|
32
20
|
SecureSession,
|
|
33
21
|
decodeClientHello,
|
|
34
|
-
decodeRelayToHost,
|
|
35
22
|
encodeHostHello,
|
|
36
|
-
|
|
37
|
-
relayHostId,
|
|
23
|
+
relayWsBase,
|
|
38
24
|
respondIK,
|
|
39
|
-
|
|
25
|
+
startRelayLink,
|
|
40
26
|
toBase64Url
|
|
41
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-kmbh44dt.js";
|
|
28
|
+
import {
|
|
29
|
+
packagedVersion,
|
|
30
|
+
webRoot
|
|
31
|
+
} from "./chunk-1d9sppj0.js";
|
|
32
|
+
import {
|
|
33
|
+
HOST_SETTING_KEYS,
|
|
34
|
+
INVALID_FRAME_CLOSE_CODE,
|
|
35
|
+
PROTOCOL,
|
|
36
|
+
THINKING_LEVELS,
|
|
37
|
+
UNAUTHORIZED_CLOSE_CODE,
|
|
38
|
+
isQueueMode,
|
|
39
|
+
isThinkingLevel,
|
|
40
|
+
organize,
|
|
41
|
+
parseClientFrame
|
|
42
|
+
} from "./chunk-hsp12cjt.js";
|
|
42
43
|
import {
|
|
43
44
|
DEFAULT_DATA_DIR
|
|
44
45
|
} from "./chunk-cjfmfyjb.js";
|
|
@@ -46,7 +47,7 @@ import"./chunk-df32avw6.js";
|
|
|
46
47
|
|
|
47
48
|
// apps/host/src/main.ts
|
|
48
49
|
import { homedir as homedir3 } from "node:os";
|
|
49
|
-
import { join as
|
|
50
|
+
import { join as join9, resolve as resolve4 } from "node:path";
|
|
50
51
|
import { parseArgs } from "node:util";
|
|
51
52
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
52
53
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
@@ -284,6 +285,7 @@ function createContext(options) {
|
|
|
284
285
|
].join(`
|
|
285
286
|
`);
|
|
286
287
|
}),
|
|
288
|
+
section("file_references", () => "When the user writes @./path in their message, it is a project file the user references; the path is relative to your current working directory. Its contents are not expanded for you — read the file yourself if the task needs them.", { tag: false }),
|
|
287
289
|
section("project_context", async (input, context) => {
|
|
288
290
|
const cwd = input.agent.cwd;
|
|
289
291
|
if (cwd === undefined)
|
|
@@ -521,8 +523,8 @@ var todo = {
|
|
|
521
523
|
// apps/host/src/host.ts
|
|
522
524
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
523
525
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
524
|
-
import { basename as basename2, join as
|
|
525
|
-
import { BACKGROUND_CONTEXT as
|
|
526
|
+
import { basename as basename2, join as join8, sep as sep5 } from "node:path";
|
|
527
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
526
528
|
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
527
529
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
528
530
|
|
|
@@ -681,7 +683,23 @@ class DuplicateRequest extends Error {
|
|
|
681
683
|
this.conversationId = conversationId;
|
|
682
684
|
}
|
|
683
685
|
}
|
|
684
|
-
|
|
686
|
+
function requireRole(agents, name) {
|
|
687
|
+
const found = agents.roles.find((role) => role.name === name);
|
|
688
|
+
if (found !== undefined)
|
|
689
|
+
return found;
|
|
690
|
+
const broken = agents.problems.find((problem) => problem.file === `${name}.md`);
|
|
691
|
+
if (broken !== undefined)
|
|
692
|
+
throw new Error(`Agent "${name}" failed to load: ${broken.reason}`);
|
|
693
|
+
const defined = agents.roles.map((role) => role.name).join(", ");
|
|
694
|
+
throw new Error(defined === "" ? `Unknown agent "${name}"` : `Unknown agent "${name}". Available agents: ${defined}`);
|
|
695
|
+
}
|
|
696
|
+
function roleModelRef(role) {
|
|
697
|
+
if (role.model === undefined)
|
|
698
|
+
return;
|
|
699
|
+
const slash = role.model.indexOf("/");
|
|
700
|
+
return { provider: role.model.slice(0, slash), modelId: role.model.slice(slash + 1) };
|
|
701
|
+
}
|
|
702
|
+
async function createConversation(harness, dataDir, input, defaults, models, modelSummaries, agents, context) {
|
|
685
703
|
const index = await harness.snapshot(IndexDoc, context);
|
|
686
704
|
const prior = index?.conversations.find((entry) => entry.requestId === input.requestId);
|
|
687
705
|
if (prior !== undefined) {
|
|
@@ -691,12 +709,24 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
691
709
|
}
|
|
692
710
|
return prior.id;
|
|
693
711
|
}
|
|
694
|
-
const
|
|
712
|
+
const role = input.agent === undefined ? undefined : requireRole(agents, input.agent);
|
|
713
|
+
const roleModel = role === undefined ? undefined : roleModelRef(role);
|
|
714
|
+
const modelRef = input.model ?? roleModel ?? defaults.model;
|
|
695
715
|
const model = modelRef === undefined ? undefined : models.getModel(modelRef.provider, modelRef.modelId);
|
|
696
716
|
if (input.model !== undefined && model === undefined) {
|
|
697
717
|
throw new Error(`Unknown model: ${input.model.provider}/${input.model.modelId}`);
|
|
698
718
|
}
|
|
699
|
-
|
|
719
|
+
if (role !== undefined) {
|
|
720
|
+
const listed = modelSummaries().map((m) => `${m.provider}/${m.modelId}`).join(", ");
|
|
721
|
+
const suffix2 = listed === "" ? "" : `. Available models: ${listed}`;
|
|
722
|
+
if (modelRef === undefined) {
|
|
723
|
+
throw new Error(`Agent "${role.name}" has no model: the role pins none and the host has no default${suffix2}`);
|
|
724
|
+
}
|
|
725
|
+
if (model === undefined || !modelSummaries().some((m) => m.provider === modelRef.provider && m.modelId === modelRef.modelId)) {
|
|
726
|
+
throw new Error(`Model ${modelRef.provider}/${modelRef.modelId} is not available${suffix2}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const thinkingLevel = model === undefined ? input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel : clampThinkingLevel2(model, input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel ?? "off");
|
|
700
730
|
const home = input.home;
|
|
701
731
|
const projectPath = home.kind === "project" ? await normalizePath(home.path).catch(() => home.path) : undefined;
|
|
702
732
|
const repo = home.kind === "project" && input.checkout !== "project" && projectPath !== undefined ? await gitBase(projectPath) : undefined;
|
|
@@ -707,7 +737,8 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
707
737
|
agent: {
|
|
708
738
|
...projectPath === undefined || repo !== undefined ? {} : { cwd: projectPath },
|
|
709
739
|
...modelRef === undefined ? {} : { model: modelRef },
|
|
710
|
-
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
740
|
+
...thinkingLevel === undefined ? {} : { thinkingLevel },
|
|
741
|
+
...role === undefined || role.prompt === "" ? {} : { instructions: role.prompt }
|
|
711
742
|
},
|
|
712
743
|
init: async (tx, id) => {
|
|
713
744
|
const doc = await tx.doc(IndexDoc);
|
|
@@ -718,7 +749,13 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
718
749
|
throw new Error(`Unknown project: ${home.path}`);
|
|
719
750
|
}
|
|
720
751
|
const stored = home.kind === "chat" ? { kind: "chat" } : { kind: "project", path: projectPath };
|
|
721
|
-
doc.conversations.push({
|
|
752
|
+
doc.conversations.push({
|
|
753
|
+
id,
|
|
754
|
+
home: stored,
|
|
755
|
+
createdAt: Date.now(),
|
|
756
|
+
requestId: input.requestId,
|
|
757
|
+
...role === undefined ? {} : { role: role.name }
|
|
758
|
+
});
|
|
722
759
|
if (repo !== undefined && suffix !== undefined) {
|
|
723
760
|
const path = worktreePath(dataDir, id);
|
|
724
761
|
if (doc.checkouts === undefined)
|
|
@@ -1366,10 +1403,12 @@ var fileValueError = (key, value, models) => {
|
|
|
1366
1403
|
case "steeringMode":
|
|
1367
1404
|
case "followUpMode":
|
|
1368
1405
|
return isQueueMode(value) ? undefined : "malformed";
|
|
1406
|
+
case "acceptAccountDevices":
|
|
1407
|
+
return typeof value === "boolean" ? undefined : "malformed";
|
|
1369
1408
|
}
|
|
1370
1409
|
};
|
|
1371
1410
|
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
1372
|
-
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
1411
|
+
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
1373
1412
|
var parse = (path, text) => {
|
|
1374
1413
|
let parsed;
|
|
1375
1414
|
try {
|
|
@@ -1412,6 +1451,7 @@ async function openHostSettings(options) {
|
|
|
1412
1451
|
defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
|
|
1413
1452
|
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
1414
1453
|
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
1454
|
+
acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
|
|
1415
1455
|
custom
|
|
1416
1456
|
};
|
|
1417
1457
|
};
|
|
@@ -1457,14 +1497,14 @@ import lockfile from "proper-lockfile";
|
|
|
1457
1497
|
// apps/host/src/gateway.ts
|
|
1458
1498
|
import { createServer } from "node:http";
|
|
1459
1499
|
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1460
|
-
import { join as
|
|
1461
|
-
import { BACKGROUND_CONTEXT as
|
|
1500
|
+
import { join as join7, sep as sep4 } from "node:path";
|
|
1501
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1462
1502
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1463
1503
|
import {
|
|
1464
|
-
AgentDoc as
|
|
1504
|
+
AgentDoc as AgentDoc4,
|
|
1465
1505
|
LiveDoc
|
|
1466
1506
|
} from "@earendil-works/pi-durable";
|
|
1467
|
-
import { WebSocket
|
|
1507
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
1468
1508
|
import { Value as Value2 } from "typebox/value";
|
|
1469
1509
|
|
|
1470
1510
|
// apps/host/src/folders.ts
|
|
@@ -1508,6 +1548,273 @@ async function listFolders(path, home) {
|
|
|
1508
1548
|
return { path: dir, home: await realpath3(home).catch(() => home), folders };
|
|
1509
1549
|
}
|
|
1510
1550
|
|
|
1551
|
+
// apps/host/src/files.ts
|
|
1552
|
+
import { spawn } from "node:child_process";
|
|
1553
|
+
import { opendir, realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
1554
|
+
import { join as join6, resolve as resolve3, sep as sep3 } from "node:path";
|
|
1555
|
+
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
1556
|
+
var CACHE_TTL_MS = 3000;
|
|
1557
|
+
var MAX_CACHED_ROOTS = 64;
|
|
1558
|
+
var MAX_CONCURRENT_SCANS = 4;
|
|
1559
|
+
var SCAN_BUDGET_MS = 5000;
|
|
1560
|
+
var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
1561
|
+
var MAX_WALK_ENTRIES = 20000;
|
|
1562
|
+
var MAX_RESULTS = 50;
|
|
1563
|
+
var UNUSABLE_PATH = /[\s`\x00-\x1f\x7f-\x9f]/;
|
|
1564
|
+
function createCandidateLister(enumerate, options = {}) {
|
|
1565
|
+
const now = options.now ?? Date.now;
|
|
1566
|
+
const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
|
|
1567
|
+
const maxConcurrent = options.maxConcurrent ?? MAX_CONCURRENT_SCANS;
|
|
1568
|
+
const maxCached = options.maxCached ?? MAX_CACHED_ROOTS;
|
|
1569
|
+
const cache = new Map;
|
|
1570
|
+
const inflight = new Map;
|
|
1571
|
+
let running = 0;
|
|
1572
|
+
const waiting = [];
|
|
1573
|
+
return (root, mode) => {
|
|
1574
|
+
const key = `${mode}${root}`;
|
|
1575
|
+
const hit = cache.get(key);
|
|
1576
|
+
if (hit !== undefined) {
|
|
1577
|
+
if (hit.expiresAt > now())
|
|
1578
|
+
return Promise.resolve(hit.value);
|
|
1579
|
+
cache.delete(key);
|
|
1580
|
+
}
|
|
1581
|
+
const pending = inflight.get(key);
|
|
1582
|
+
if (pending !== undefined)
|
|
1583
|
+
return pending;
|
|
1584
|
+
const scan = (async () => {
|
|
1585
|
+
if (running >= maxConcurrent)
|
|
1586
|
+
await new Promise((release) => waiting.push(release));
|
|
1587
|
+
running += 1;
|
|
1588
|
+
try {
|
|
1589
|
+
return await enumerate(root, mode);
|
|
1590
|
+
} finally {
|
|
1591
|
+
running -= 1;
|
|
1592
|
+
waiting.shift()?.();
|
|
1593
|
+
}
|
|
1594
|
+
})();
|
|
1595
|
+
inflight.set(key, scan);
|
|
1596
|
+
scan.then((value) => {
|
|
1597
|
+
const at = now();
|
|
1598
|
+
for (const [cached, entry] of cache) {
|
|
1599
|
+
if (entry.expiresAt <= at)
|
|
1600
|
+
cache.delete(cached);
|
|
1601
|
+
}
|
|
1602
|
+
while (cache.size >= maxCached)
|
|
1603
|
+
cache.delete(cache.keys().next().value);
|
|
1604
|
+
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
1605
|
+
}, () => {});
|
|
1606
|
+
return scan.finally(() => inflight.delete(key));
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
function gitLines(cwd, args, limits) {
|
|
1610
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
1611
|
+
const child = spawn("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1612
|
+
const chunks = [];
|
|
1613
|
+
let bytes = 0;
|
|
1614
|
+
let truncated = false;
|
|
1615
|
+
let settled = false;
|
|
1616
|
+
const kill = (markTruncated) => {
|
|
1617
|
+
truncated ||= markTruncated;
|
|
1618
|
+
child.kill("SIGKILL");
|
|
1619
|
+
};
|
|
1620
|
+
const timer = setTimeout(() => kill(true), Math.max(0, limits.deadline - limits.now()));
|
|
1621
|
+
const finish = (error) => {
|
|
1622
|
+
if (settled)
|
|
1623
|
+
return;
|
|
1624
|
+
settled = true;
|
|
1625
|
+
clearTimeout(timer);
|
|
1626
|
+
if (error !== undefined) {
|
|
1627
|
+
rejectPromise(error);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const output = Buffer.concat(chunks).toString("utf8");
|
|
1631
|
+
const parts = output.split("\x00");
|
|
1632
|
+
if (truncated)
|
|
1633
|
+
parts.pop();
|
|
1634
|
+
resolvePromise({ lines: parts.filter((part) => part !== ""), truncated });
|
|
1635
|
+
};
|
|
1636
|
+
child.stdout.on("data", (chunk) => {
|
|
1637
|
+
bytes += chunk.length;
|
|
1638
|
+
if (bytes > limits.maxBytes) {
|
|
1639
|
+
kill(true);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
chunks.push(chunk);
|
|
1643
|
+
if (limits.now() > limits.deadline)
|
|
1644
|
+
kill(true);
|
|
1645
|
+
});
|
|
1646
|
+
child.on("error", (error) => finish(error));
|
|
1647
|
+
child.stderr.resume();
|
|
1648
|
+
child.on("close", (code) => {
|
|
1649
|
+
if (code === 0 || truncated) {
|
|
1650
|
+
finish(undefined);
|
|
1651
|
+
} else {
|
|
1652
|
+
finish(new Error(`git ${args.join(" ")} exited with code ${code}`));
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
async function inWorkTree(root) {
|
|
1658
|
+
return await new Promise((resolvePromise) => {
|
|
1659
|
+
const child = spawn("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1660
|
+
let out = "";
|
|
1661
|
+
child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
|
|
1662
|
+
child.on("error", () => resolvePromise(false));
|
|
1663
|
+
child.on("close", (code) => resolvePromise(code === 0 && out.trim() === "true"));
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
async function hasHead(root) {
|
|
1667
|
+
return await new Promise((resolvePromise) => {
|
|
1668
|
+
const child = spawn("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
1669
|
+
child.on("error", () => resolvePromise(false));
|
|
1670
|
+
child.on("close", (code) => resolvePromise(code === 0));
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
var STAGED_RECORD = /^([0-7]{6}) [0-9a-f]{40,64} [0-7]\t/;
|
|
1674
|
+
async function gitWorkingFiles(root, limits) {
|
|
1675
|
+
const listing = await gitLines(root, ["ls-files", "-z", "-s", "--cached", "--others", "--exclude-standard"], limits);
|
|
1676
|
+
const deleted = await gitLines(root, ["ls-files", "-z", "--deleted"], limits);
|
|
1677
|
+
const gone = new Set(deleted.lines);
|
|
1678
|
+
const files = new Set;
|
|
1679
|
+
for (const line of listing.lines) {
|
|
1680
|
+
const staged = STAGED_RECORD.exec(line);
|
|
1681
|
+
const path = staged === null ? line : line.slice(staged[0].length);
|
|
1682
|
+
if (staged !== null && staged[1] === "160000")
|
|
1683
|
+
continue;
|
|
1684
|
+
if (path === "" || path.endsWith("/") || gone.has(path))
|
|
1685
|
+
continue;
|
|
1686
|
+
files.add(path);
|
|
1687
|
+
}
|
|
1688
|
+
return { files: [...files], truncated: listing.truncated || deleted.truncated };
|
|
1689
|
+
}
|
|
1690
|
+
async function gitHeadFiles(root, limits) {
|
|
1691
|
+
const listing = await gitLines(root, ["ls-tree", "-r", "-z", "HEAD"], limits);
|
|
1692
|
+
const files = [];
|
|
1693
|
+
for (const line of listing.lines) {
|
|
1694
|
+
const tab = line.indexOf("\t");
|
|
1695
|
+
if (tab === -1)
|
|
1696
|
+
continue;
|
|
1697
|
+
const kind = line.slice(0, tab).split(" ")[1];
|
|
1698
|
+
if (kind === "blob")
|
|
1699
|
+
files.push(line.slice(tab + 1));
|
|
1700
|
+
}
|
|
1701
|
+
return { files, truncated: listing.truncated };
|
|
1702
|
+
}
|
|
1703
|
+
async function walkFiles(root, limits) {
|
|
1704
|
+
const files = [];
|
|
1705
|
+
let entries = 0;
|
|
1706
|
+
let truncated = false;
|
|
1707
|
+
const dirs = [{ dir: root, prefix: "" }];
|
|
1708
|
+
while (dirs.length > 0) {
|
|
1709
|
+
if (limits.now() > limits.deadline) {
|
|
1710
|
+
truncated = true;
|
|
1711
|
+
break;
|
|
1712
|
+
}
|
|
1713
|
+
const { dir, prefix } = dirs.pop();
|
|
1714
|
+
let handle;
|
|
1715
|
+
try {
|
|
1716
|
+
handle = await opendir(dir);
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
if (prefix === "")
|
|
1719
|
+
throw error;
|
|
1720
|
+
truncated = true;
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
for await (const entry of handle) {
|
|
1724
|
+
if (entries++ >= limits.maxEntries || limits.now() > limits.deadline) {
|
|
1725
|
+
truncated = true;
|
|
1726
|
+
break;
|
|
1727
|
+
}
|
|
1728
|
+
if (entry.isDirectory()) {
|
|
1729
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
1730
|
+
continue;
|
|
1731
|
+
dirs.push({ dir: join6(dir, entry.name), prefix: `${prefix}${entry.name}/` });
|
|
1732
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
1733
|
+
files.push(`${prefix}${entry.name}`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (truncated)
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
return { files, truncated };
|
|
1740
|
+
}
|
|
1741
|
+
async function enumerateRoot(root, mode, limits = {}) {
|
|
1742
|
+
const now = limits.now ?? Date.now;
|
|
1743
|
+
const bounded = {
|
|
1744
|
+
now,
|
|
1745
|
+
deadline: now() + (limits.budgetMs ?? SCAN_BUDGET_MS),
|
|
1746
|
+
maxBytes: limits.maxBytes ?? MAX_OUTPUT_BYTES,
|
|
1747
|
+
maxEntries: limits.maxEntries ?? MAX_WALK_ENTRIES
|
|
1748
|
+
};
|
|
1749
|
+
if (!await inWorkTree(root)) {
|
|
1750
|
+
return walkFiles(root, bounded);
|
|
1751
|
+
}
|
|
1752
|
+
if (mode === "head" && !await hasHead(root)) {
|
|
1753
|
+
return walkFiles(root, bounded);
|
|
1754
|
+
}
|
|
1755
|
+
return mode === "head" ? gitHeadFiles(root, bounded) : gitWorkingFiles(root, bounded);
|
|
1756
|
+
}
|
|
1757
|
+
function matchFiles(candidates, query, max = MAX_RESULTS) {
|
|
1758
|
+
const usable = candidates.filter((path) => !UNUSABLE_PATH.test(path));
|
|
1759
|
+
if (query === "") {
|
|
1760
|
+
const sorted = [...usable].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
1761
|
+
return { files: sorted.slice(0, max), truncated: sorted.length > max };
|
|
1762
|
+
}
|
|
1763
|
+
const needle = query.toLowerCase();
|
|
1764
|
+
const tiers = [[], [], []];
|
|
1765
|
+
for (const path of usable) {
|
|
1766
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
1767
|
+
const lower = path.toLowerCase();
|
|
1768
|
+
if (base.startsWith(needle))
|
|
1769
|
+
tiers[0].push(path);
|
|
1770
|
+
else if (base.includes(needle))
|
|
1771
|
+
tiers[1].push(path);
|
|
1772
|
+
else if (lower.includes(needle))
|
|
1773
|
+
tiers[2].push(path);
|
|
1774
|
+
}
|
|
1775
|
+
const shorter = (a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0);
|
|
1776
|
+
const matched = [...tiers[0].sort(shorter), ...tiers[1].sort(shorter), ...tiers[2].sort(shorter)];
|
|
1777
|
+
return { files: matched.slice(0, max), truncated: matched.length > max };
|
|
1778
|
+
}
|
|
1779
|
+
async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
1780
|
+
let root;
|
|
1781
|
+
let mode = "live";
|
|
1782
|
+
if (scope.kind === "conversation") {
|
|
1783
|
+
const agent = await harness.snapshot(AgentDoc3, scope.conversationId, context);
|
|
1784
|
+
if (agent === undefined)
|
|
1785
|
+
throw new Error(`Conversation ${scope.conversationId} does not exist`);
|
|
1786
|
+
const cwd = agent.cwd;
|
|
1787
|
+
if (cwd === undefined)
|
|
1788
|
+
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
1789
|
+
if (cwd.startsWith(join6(dataDir, "chats") + sep3))
|
|
1790
|
+
return { files: [], truncated: false };
|
|
1791
|
+
const info = await stat4(cwd).catch(() => {
|
|
1792
|
+
return;
|
|
1793
|
+
});
|
|
1794
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1795
|
+
throw new Error(`Conversation ${scope.conversationId}'s checkout is unavailable: ${cwd}`);
|
|
1796
|
+
}
|
|
1797
|
+
root = cwd;
|
|
1798
|
+
} else {
|
|
1799
|
+
const normalized = await realpath4(resolve3(scope.path)).catch(() => resolve3(scope.path));
|
|
1800
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
1801
|
+
if (!index?.projects.some((project) => project.path === normalized)) {
|
|
1802
|
+
throw new Error(`Unknown project: ${scope.path}`);
|
|
1803
|
+
}
|
|
1804
|
+
const info = await stat4(normalized).catch(() => {
|
|
1805
|
+
return;
|
|
1806
|
+
});
|
|
1807
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1808
|
+
throw new Error(`Not a directory: ${normalized}`);
|
|
1809
|
+
}
|
|
1810
|
+
root = normalized;
|
|
1811
|
+
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
1812
|
+
}
|
|
1813
|
+
const candidates = await enumerate(root, mode);
|
|
1814
|
+
const matched = matchFiles(candidates.files, query);
|
|
1815
|
+
return { files: matched.files, truncated: matched.truncated || candidates.truncated };
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1511
1818
|
// packages/protocol/src/images.ts
|
|
1512
1819
|
var IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
1513
1820
|
var MAX_IMAGES_PER_MESSAGE = 4;
|
|
@@ -1651,6 +1958,378 @@ ${answer}`}`,
|
|
|
1651
1958
|
});
|
|
1652
1959
|
}
|
|
1653
1960
|
|
|
1961
|
+
// apps/host/src/push.ts
|
|
1962
|
+
import { createHash } from "node:crypto";
|
|
1963
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
1964
|
+
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
1965
|
+
|
|
1966
|
+
// packages/client/src/family.ts
|
|
1967
|
+
var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
|
|
1968
|
+
function displayName(summary) {
|
|
1969
|
+
if (summary === undefined)
|
|
1970
|
+
return "Conversation";
|
|
1971
|
+
if (summary.kind !== "subagent")
|
|
1972
|
+
return summary.title ?? "New conversation";
|
|
1973
|
+
if (summary.label !== undefined)
|
|
1974
|
+
return summary.label;
|
|
1975
|
+
const title = summary.title?.replace(SUBAGENT_TASK_PREFIX, "").trim();
|
|
1976
|
+
return title === undefined || title === "" ? "Subagent" : title;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// packages/client/src/run-notifications.ts
|
|
1980
|
+
var everySummary = (organized) => {
|
|
1981
|
+
const summaries = [];
|
|
1982
|
+
const visit = (node) => {
|
|
1983
|
+
summaries.push(node.summary);
|
|
1984
|
+
for (const child of node.children)
|
|
1985
|
+
visit(child);
|
|
1986
|
+
};
|
|
1987
|
+
for (const root of [...organized.chats, ...organized.projects.flatMap((entry) => entry.conversations)])
|
|
1988
|
+
visit(root);
|
|
1989
|
+
return summaries;
|
|
1990
|
+
};
|
|
1991
|
+
function diffRunNotices(previous, organized) {
|
|
1992
|
+
const notices = [];
|
|
1993
|
+
const changed = [];
|
|
1994
|
+
const baseline = new Map;
|
|
1995
|
+
for (const summary of everySummary(organized)) {
|
|
1996
|
+
baseline.set(summary.id, summary.status);
|
|
1997
|
+
if (previous === undefined)
|
|
1998
|
+
continue;
|
|
1999
|
+
const before = previous.get(summary.id);
|
|
2000
|
+
if (before !== summary.status)
|
|
2001
|
+
changed.push(summary.id);
|
|
2002
|
+
if (summary.status === "needs-answer" && before !== "needs-answer") {
|
|
2003
|
+
notices.push({ id: summary.id, title: displayName(summary), body: "Waiting for your answer" });
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const runEnded = before === "running" || before === "needs-answer";
|
|
2007
|
+
if (runEnded && (summary.status === undefined || summary.status === "failed") && summary.kind !== "subagent") {
|
|
2008
|
+
notices.push({ id: summary.id, title: displayName(summary), body: summary.status === "failed" ? "Failed" : "Finished" });
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (previous !== undefined) {
|
|
2012
|
+
for (const gone of previous.keys()) {
|
|
2013
|
+
if (!baseline.has(gone))
|
|
2014
|
+
changed.push(gone);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return { notices, changed, baseline };
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// apps/host/src/push.ts
|
|
2021
|
+
var context2 = BACKGROUND_CONTEXT2;
|
|
2022
|
+
var PushDoc = defineDoc6({
|
|
2023
|
+
kind: "pinomad.push",
|
|
2024
|
+
version: 2,
|
|
2025
|
+
scope: "session",
|
|
2026
|
+
initial: () => ({ registrations: [] }),
|
|
2027
|
+
migrate: () => ({ registrations: [] })
|
|
2028
|
+
});
|
|
2029
|
+
var APNS_TOKEN_PATTERN = /^(?:[0-9a-f]{2})+$/;
|
|
2030
|
+
var APNS_TOKEN_MAX_CHARS = 256;
|
|
2031
|
+
var isApnsEnvironment = (value) => value === "production" || value === "development";
|
|
2032
|
+
function pushTokenProblem(token) {
|
|
2033
|
+
if (token === null)
|
|
2034
|
+
return;
|
|
2035
|
+
if (typeof token === "string") {
|
|
2036
|
+
return "Push notifications no longer go through Expo — register the device's APNs token instead";
|
|
2037
|
+
}
|
|
2038
|
+
if (!isApnsEnvironment(token.environment)) {
|
|
2039
|
+
return 'environment must be "production" or "development"';
|
|
2040
|
+
}
|
|
2041
|
+
const apns = token.apns.toLowerCase();
|
|
2042
|
+
if (apns.length > APNS_TOKEN_MAX_CHARS) {
|
|
2043
|
+
return `APNs token is too long (${APNS_TOKEN_MAX_CHARS} characters at most)`;
|
|
2044
|
+
}
|
|
2045
|
+
if (!APNS_TOKEN_PATTERN.test(apns))
|
|
2046
|
+
return "Not an APNs device token (expected hex)";
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
async function setPushToken(harness, publicKey, token, context3, joined) {
|
|
2050
|
+
const problem = pushTokenProblem(token);
|
|
2051
|
+
if (problem !== undefined)
|
|
2052
|
+
throw new Error(problem);
|
|
2053
|
+
if (token !== null && typeof token !== "string" && !await joined()) {
|
|
2054
|
+
throw new Error("Push notifications need the host joined to an account (`pinomad join`)");
|
|
2055
|
+
}
|
|
2056
|
+
await harness.commit(async (tx) => {
|
|
2057
|
+
const devices = await tx.doc(DevicesDoc);
|
|
2058
|
+
if (!devices.devices.some((entry) => entry.publicKey === publicKey)) {
|
|
2059
|
+
throw new Error("Device is not paired");
|
|
2060
|
+
}
|
|
2061
|
+
const push = await tx.doc(PushDoc);
|
|
2062
|
+
push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
|
|
2063
|
+
if (token !== null && typeof token !== "string") {
|
|
2064
|
+
push.registrations.push({
|
|
2065
|
+
publicKey,
|
|
2066
|
+
token: token.apns.toLowerCase(),
|
|
2067
|
+
environment: token.environment,
|
|
2068
|
+
updatedAt: Date.now()
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
}, context3);
|
|
2072
|
+
}
|
|
2073
|
+
async function revokeDevice(harness, publicKey, context3) {
|
|
2074
|
+
await harness.commit(async (tx) => {
|
|
2075
|
+
const devices = await tx.doc(DevicesDoc);
|
|
2076
|
+
devices.devices = devices.devices.filter((entry) => entry.publicKey !== publicKey);
|
|
2077
|
+
const push = await tx.doc(PushDoc);
|
|
2078
|
+
push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
|
|
2079
|
+
const revocations = await tx.doc(RevocationsDoc);
|
|
2080
|
+
if (!revocations.revoked.includes(publicKey))
|
|
2081
|
+
revocations.revoked.push(publicKey);
|
|
2082
|
+
}, context3);
|
|
2083
|
+
}
|
|
2084
|
+
async function dropStaleRegistration(harness, token, environment, unregisteredAt, context3) {
|
|
2085
|
+
await harness.commit(async (tx) => {
|
|
2086
|
+
const push = await tx.doc(PushDoc);
|
|
2087
|
+
push.registrations = push.registrations.filter((entry) => entry.token !== token || entry.environment !== environment || entry.updatedAt >= unregisteredAt);
|
|
2088
|
+
}, context3);
|
|
2089
|
+
}
|
|
2090
|
+
var FALLBACK_TITLE = "PiNomad conversation";
|
|
2091
|
+
var TITLE_WAIT_MS = 15000;
|
|
2092
|
+
var TITLE_MAX_CHARS = 160;
|
|
2093
|
+
var MAX_ATTEMPTS = 4;
|
|
2094
|
+
var RETRY_BASE_MS = 5000;
|
|
2095
|
+
var MAX_INFLIGHT = 8;
|
|
2096
|
+
var MAX_PENDING = 128;
|
|
2097
|
+
var UNAVAILABLE_TRANSPORT = {
|
|
2098
|
+
send: () => Promise.resolve({ result: "unavailable", reason: "no server connection" })
|
|
2099
|
+
};
|
|
2100
|
+
var realTimers = {
|
|
2101
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
2102
|
+
clearTimeout: (handle) => clearTimeout(handle)
|
|
2103
|
+
};
|
|
2104
|
+
var tokenHint = (token) => `…${token.slice(-6)}`;
|
|
2105
|
+
var collapseId = (hostKey, id) => createHash("sha256").update(`${hostKey}:${id}`).digest("hex").slice(0, 32);
|
|
2106
|
+
async function attachPushSender(options) {
|
|
2107
|
+
const { harness, conversations } = options;
|
|
2108
|
+
const transport = options.transport ?? UNAVAILABLE_TRANSPORT;
|
|
2109
|
+
const timers = options.timers ?? realTimers;
|
|
2110
|
+
const maxPending = options.limits?.maxPending ?? MAX_PENDING;
|
|
2111
|
+
const maxInflight = options.limits?.maxInflight ?? MAX_INFLIGHT;
|
|
2112
|
+
const indexWatch = await harness.watchDoc(IndexDoc, context2);
|
|
2113
|
+
let index = indexWatch?.value ?? undefined;
|
|
2114
|
+
let baseline;
|
|
2115
|
+
let disposed = false;
|
|
2116
|
+
const pending = new Set;
|
|
2117
|
+
const titleWaiters = new Map;
|
|
2118
|
+
let inflight = 0;
|
|
2119
|
+
const readyQueue = [];
|
|
2120
|
+
let unavailableLogged = false;
|
|
2121
|
+
const generation = new Map;
|
|
2122
|
+
const update = () => {
|
|
2123
|
+
if (index === undefined)
|
|
2124
|
+
return;
|
|
2125
|
+
const organized = organize(index, conversations.value);
|
|
2126
|
+
if (baseline === undefined) {
|
|
2127
|
+
baseline = diffRunNotices(undefined, organized).baseline;
|
|
2128
|
+
for (const id of baseline.keys())
|
|
2129
|
+
generation.set(id, 0);
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
const diff = diffRunNotices(baseline, organized);
|
|
2133
|
+
baseline = diff.baseline;
|
|
2134
|
+
for (const id of diff.changed)
|
|
2135
|
+
generation.set(id, (generation.get(id) ?? 0) + 1);
|
|
2136
|
+
for (const notice of diff.notices)
|
|
2137
|
+
enqueue(notice);
|
|
2138
|
+
};
|
|
2139
|
+
const fresh = (item) => baseline?.has(item.notice.id) === true && generation.get(item.notice.id) === item.generation;
|
|
2140
|
+
const registrations = async () => {
|
|
2141
|
+
const doc = await harness.snapshot(PushDoc, context2);
|
|
2142
|
+
return doc?.registrations ?? [];
|
|
2143
|
+
};
|
|
2144
|
+
const findRegistration = (entries, item) => entries.find((entry) => entry.token === item.token && entry.environment === item.environment);
|
|
2145
|
+
const namedTitle = async (id) => {
|
|
2146
|
+
const doc = await harness.snapshot(TitleDoc, id, context2);
|
|
2147
|
+
return typeof doc?.title === "string" && doc.title !== "" ? doc.title : undefined;
|
|
2148
|
+
};
|
|
2149
|
+
const titleFor = async (notice) => {
|
|
2150
|
+
const named = await namedTitle(notice.id);
|
|
2151
|
+
if (named !== undefined)
|
|
2152
|
+
return named;
|
|
2153
|
+
if (notice.body !== "Finished" || disposed)
|
|
2154
|
+
return FALLBACK_TITLE;
|
|
2155
|
+
return await new Promise((resolve4) => {
|
|
2156
|
+
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
2157
|
+
titleWaiters.set(notice.id, waiters);
|
|
2158
|
+
const settle = (title) => {
|
|
2159
|
+
waiters.delete(settle);
|
|
2160
|
+
if (waiters.size === 0)
|
|
2161
|
+
titleWaiters.delete(notice.id);
|
|
2162
|
+
timers.clearTimeout(timer);
|
|
2163
|
+
resolve4(title);
|
|
2164
|
+
};
|
|
2165
|
+
const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
2166
|
+
waiters.add(settle);
|
|
2167
|
+
namedTitle(notice.id).then((title) => {
|
|
2168
|
+
if (title !== undefined)
|
|
2169
|
+
settle(title);
|
|
2170
|
+
});
|
|
2171
|
+
});
|
|
2172
|
+
};
|
|
2173
|
+
const drop = (item) => {
|
|
2174
|
+
pending.delete(item);
|
|
2175
|
+
if (item.timer !== undefined)
|
|
2176
|
+
timers.clearTimeout(item.timer);
|
|
2177
|
+
const queuedAt = readyQueue.indexOf(item);
|
|
2178
|
+
if (queuedAt >= 0)
|
|
2179
|
+
readyQueue.splice(queuedAt, 1);
|
|
2180
|
+
};
|
|
2181
|
+
const pump = () => {
|
|
2182
|
+
while (inflight < maxInflight) {
|
|
2183
|
+
const next = readyQueue.shift();
|
|
2184
|
+
if (next === undefined)
|
|
2185
|
+
return;
|
|
2186
|
+
next.queued = false;
|
|
2187
|
+
attempt(next);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
const scheduleRetry = (item, retryAfterMs) => {
|
|
2191
|
+
if (item.attempts + 1 >= MAX_ATTEMPTS) {
|
|
2192
|
+
console.error(`push: send to ${tokenHint(item.token)} gave up`);
|
|
2193
|
+
drop(item);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
const delayMs = retryAfterMs ?? RETRY_BASE_MS * 2 ** item.attempts;
|
|
2197
|
+
item.attempts += 1;
|
|
2198
|
+
item.timer = timers.setTimeout(() => void attempt(item), delayMs);
|
|
2199
|
+
};
|
|
2200
|
+
const attempt = async (item) => {
|
|
2201
|
+
if (disposed || !pending.has(item))
|
|
2202
|
+
return;
|
|
2203
|
+
try {
|
|
2204
|
+
if (findRegistration(await registrations(), item) === undefined || !fresh(item))
|
|
2205
|
+
return drop(item);
|
|
2206
|
+
if (item.title === undefined) {
|
|
2207
|
+
item.title = await titleFor(item.notice);
|
|
2208
|
+
if (disposed || !pending.has(item) || findRegistration(await registrations(), item) === undefined || !fresh(item)) {
|
|
2209
|
+
return drop(item);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
if (inflight >= maxInflight) {
|
|
2213
|
+
if (!item.queued) {
|
|
2214
|
+
item.queued = true;
|
|
2215
|
+
readyQueue.push(item);
|
|
2216
|
+
}
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
inflight += 1;
|
|
2220
|
+
let verdict;
|
|
2221
|
+
try {
|
|
2222
|
+
verdict = await transport.send({
|
|
2223
|
+
token: item.token,
|
|
2224
|
+
environment: item.environment,
|
|
2225
|
+
notification: {
|
|
2226
|
+
title: item.title.slice(0, TITLE_MAX_CHARS),
|
|
2227
|
+
body: item.notice.body,
|
|
2228
|
+
collapseId: collapseId(options.hostKey, item.notice.id),
|
|
2229
|
+
hostKey: options.hostKey,
|
|
2230
|
+
conversationId: item.notice.id
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
} catch {
|
|
2234
|
+
verdict = { result: "retry", reason: "transport error" };
|
|
2235
|
+
} finally {
|
|
2236
|
+
inflight -= 1;
|
|
2237
|
+
pump();
|
|
2238
|
+
}
|
|
2239
|
+
if (disposed || !pending.has(item))
|
|
2240
|
+
return drop(item);
|
|
2241
|
+
switch (verdict.result) {
|
|
2242
|
+
case "accepted":
|
|
2243
|
+
drop(item);
|
|
2244
|
+
return;
|
|
2245
|
+
case "unregistered":
|
|
2246
|
+
drop(item);
|
|
2247
|
+
await dropStaleRegistration(harness, item.token, item.environment, verdict.unregisteredAt, context2);
|
|
2248
|
+
return;
|
|
2249
|
+
case "retry":
|
|
2250
|
+
scheduleRetry(item, verdict.retryAfterMs);
|
|
2251
|
+
return;
|
|
2252
|
+
case "rejected":
|
|
2253
|
+
console.error(`push: send to ${tokenHint(item.token)} rejected: ${verdict.reason}`);
|
|
2254
|
+
drop(item);
|
|
2255
|
+
return;
|
|
2256
|
+
case "unavailable":
|
|
2257
|
+
if (!unavailableLogged) {
|
|
2258
|
+
unavailableLogged = true;
|
|
2259
|
+
console.error(`push: ${verdict.reason} — notifications are not being delivered`);
|
|
2260
|
+
}
|
|
2261
|
+
drop(item);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
console.error(`push: send to ${tokenHint(item.token)} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2266
|
+
scheduleRetry(item);
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
const enqueue = async (notice) => {
|
|
2270
|
+
if (disposed)
|
|
2271
|
+
return;
|
|
2272
|
+
const itemGeneration = generation.get(notice.id) ?? 0;
|
|
2273
|
+
try {
|
|
2274
|
+
const entries = await registrations();
|
|
2275
|
+
const seen = new Set;
|
|
2276
|
+
for (const entry of entries) {
|
|
2277
|
+
if (disposed)
|
|
2278
|
+
return;
|
|
2279
|
+
const identity = `${entry.environment}:${entry.token}`;
|
|
2280
|
+
if (seen.has(identity))
|
|
2281
|
+
continue;
|
|
2282
|
+
seen.add(identity);
|
|
2283
|
+
if (pending.size >= maxPending) {
|
|
2284
|
+
const [oldest] = pending;
|
|
2285
|
+
drop(oldest);
|
|
2286
|
+
}
|
|
2287
|
+
const item = { token: entry.token, environment: entry.environment, notice, generation: itemGeneration, attempts: 0, queued: false };
|
|
2288
|
+
pending.add(item);
|
|
2289
|
+
attempt(item);
|
|
2290
|
+
}
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
console.error(`push: could not read registrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
const unsubscribeConversations = conversations.subscribe(() => update());
|
|
2296
|
+
const unsubscribeCommits = harness.subscribeCommits((publication) => {
|
|
2297
|
+
for (const change of publication.changes) {
|
|
2298
|
+
if (change.type !== "document" || change.record.kind !== TitleDoc.definition.kind)
|
|
2299
|
+
continue;
|
|
2300
|
+
const id = change.conversationId;
|
|
2301
|
+
if (id === undefined)
|
|
2302
|
+
continue;
|
|
2303
|
+
const title = change.value?.title;
|
|
2304
|
+
if (typeof title !== "string" || title === "")
|
|
2305
|
+
continue;
|
|
2306
|
+
const waiters = titleWaiters.get(id);
|
|
2307
|
+
titleWaiters.delete(id);
|
|
2308
|
+
for (const settle of waiters ?? [])
|
|
2309
|
+
settle(title);
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
indexWatch?.start(async (value) => {
|
|
2313
|
+
index = value ?? undefined;
|
|
2314
|
+
update();
|
|
2315
|
+
});
|
|
2316
|
+
update();
|
|
2317
|
+
return {
|
|
2318
|
+
stats: () => ({ pending: pending.size, queued: readyQueue.length }),
|
|
2319
|
+
async close() {
|
|
2320
|
+
disposed = true;
|
|
2321
|
+
unsubscribeConversations();
|
|
2322
|
+
unsubscribeCommits();
|
|
2323
|
+
await indexWatch?.stop();
|
|
2324
|
+
for (const item of pending)
|
|
2325
|
+
drop(item);
|
|
2326
|
+
pending.clear();
|
|
2327
|
+
readyQueue.length = 0;
|
|
2328
|
+
titleWaiters.clear();
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
|
|
1654
2333
|
// packages/protocol/src/tool-target.ts
|
|
1655
2334
|
var TARGET_KEYS = [
|
|
1656
2335
|
"path",
|
|
@@ -1678,154 +2357,8 @@ function toolTarget(args) {
|
|
|
1678
2357
|
return;
|
|
1679
2358
|
}
|
|
1680
2359
|
|
|
1681
|
-
// apps/host/src/relay-link.ts
|
|
1682
|
-
import { WebSocket } from "ws";
|
|
1683
|
-
function relayWsBase(origin) {
|
|
1684
|
-
const normalized = origin.replace(/\/+$/, "");
|
|
1685
|
-
if (normalized.startsWith("https://"))
|
|
1686
|
-
return `wss://${normalized.slice("https://".length)}`;
|
|
1687
|
-
if (normalized.startsWith("http://"))
|
|
1688
|
-
return `ws://${normalized.slice("http://".length)}`;
|
|
1689
|
-
throw new Error(`relay origin must be an http(s) URL: ${origin}`);
|
|
1690
|
-
}
|
|
1691
|
-
var REJECTION_REASONS = {
|
|
1692
|
-
[RELAY_CLOSE.badSignature]: () => `the relay rejected this host's registration signature — a version mismatch?`,
|
|
1693
|
-
[RELAY_CLOSE.hostNotAllowed]: (hostId) => `the relay does not allow this host; add --allow-host ${hostId} to the relay's arguments`,
|
|
1694
|
-
[RELAY_CLOSE.replaced]: () => `the relay replaced this control connection — another process registered the same hostId`
|
|
1695
|
-
};
|
|
1696
|
-
function startRelayLink(options) {
|
|
1697
|
-
const origin = options.origin.replace(/\/+$/, "");
|
|
1698
|
-
const wsBase = relayWsBase(origin);
|
|
1699
|
-
const hostId = relayHostId(options.signingKey.publicKey);
|
|
1700
|
-
const minDelay = options.reconnectDelayMs?.min ?? 1000;
|
|
1701
|
-
const maxDelay = options.reconnectDelayMs?.max ?? 30000;
|
|
1702
|
-
const maxPendingAccepts = options.maxPendingAccepts ?? 16;
|
|
1703
|
-
let state = { status: "connecting" };
|
|
1704
|
-
let stopped = false;
|
|
1705
|
-
let control;
|
|
1706
|
-
let delay = minDelay;
|
|
1707
|
-
let retryTimer;
|
|
1708
|
-
let pendingAccepts = 0;
|
|
1709
|
-
function setState(next) {
|
|
1710
|
-
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
1711
|
-
state = next;
|
|
1712
|
-
if (changed)
|
|
1713
|
-
options.onStateChange?.(next);
|
|
1714
|
-
}
|
|
1715
|
-
const watched = new Set;
|
|
1716
|
-
const ponged = new WeakMap;
|
|
1717
|
-
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
1718
|
-
const pingTimer = setInterval(() => {
|
|
1719
|
-
for (const socket of watched) {
|
|
1720
|
-
if (socket.readyState !== WebSocket.OPEN)
|
|
1721
|
-
continue;
|
|
1722
|
-
if (ponged.get(socket) === true) {
|
|
1723
|
-
ponged.set(socket, false);
|
|
1724
|
-
socket.ping();
|
|
1725
|
-
} else {
|
|
1726
|
-
socket.terminate();
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
}, pingIntervalMs);
|
|
1730
|
-
function watch2(socket) {
|
|
1731
|
-
watched.add(socket);
|
|
1732
|
-
ponged.set(socket, true);
|
|
1733
|
-
socket.on("pong", () => ponged.set(socket, true));
|
|
1734
|
-
socket.once("close", () => watched.delete(socket));
|
|
1735
|
-
socket.on("error", () => socket.terminate());
|
|
1736
|
-
}
|
|
1737
|
-
function connectControl() {
|
|
1738
|
-
if (stopped)
|
|
1739
|
-
return;
|
|
1740
|
-
const socket = new WebSocket(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
1741
|
-
control = socket;
|
|
1742
|
-
watch2(socket);
|
|
1743
|
-
socket.on("message", onControlMessage);
|
|
1744
|
-
socket.once("close", onControlClosed);
|
|
1745
|
-
}
|
|
1746
|
-
function reconnect(at) {
|
|
1747
|
-
if (stopped || retryTimer !== undefined)
|
|
1748
|
-
return;
|
|
1749
|
-
retryTimer = setTimeout(() => {
|
|
1750
|
-
retryTimer = undefined;
|
|
1751
|
-
connectControl();
|
|
1752
|
-
}, at);
|
|
1753
|
-
}
|
|
1754
|
-
function onControlMessage(data, isBinary) {
|
|
1755
|
-
let frame;
|
|
1756
|
-
try {
|
|
1757
|
-
if (isBinary)
|
|
1758
|
-
throw new Error("binary control frame");
|
|
1759
|
-
frame = decodeRelayToHost(data.toString());
|
|
1760
|
-
} catch {
|
|
1761
|
-
control?.close();
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
switch (frame.t) {
|
|
1765
|
-
case "challenge":
|
|
1766
|
-
control?.send(encodeRelayFrame({
|
|
1767
|
-
t: "register",
|
|
1768
|
-
hostId,
|
|
1769
|
-
signature: signRelayChallenge(options.signingKey.secretKey, { relayOrigin: origin, hostId, nonce: frame.nonce })
|
|
1770
|
-
}));
|
|
1771
|
-
break;
|
|
1772
|
-
case "registered":
|
|
1773
|
-
delay = minDelay;
|
|
1774
|
-
setState({ status: "registered" });
|
|
1775
|
-
break;
|
|
1776
|
-
case "incoming": {
|
|
1777
|
-
if (pendingAccepts >= maxPendingAccepts)
|
|
1778
|
-
break;
|
|
1779
|
-
pendingAccepts += 1;
|
|
1780
|
-
const data2 = new WebSocket(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
1781
|
-
watch2(data2);
|
|
1782
|
-
const release = () => {
|
|
1783
|
-
pendingAccepts -= 1;
|
|
1784
|
-
clearTimeout(ageOut);
|
|
1785
|
-
data2.off("close", release);
|
|
1786
|
-
};
|
|
1787
|
-
const ageOut = setTimeout(release, 1e4);
|
|
1788
|
-
data2.once("close", release);
|
|
1789
|
-
options.accept(data2);
|
|
1790
|
-
break;
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
function onControlClosed(code) {
|
|
1795
|
-
control = undefined;
|
|
1796
|
-
if (stopped)
|
|
1797
|
-
return;
|
|
1798
|
-
const rejection = REJECTION_REASONS[code];
|
|
1799
|
-
if (rejection !== undefined) {
|
|
1800
|
-
const reason = `relay ${origin}: ${rejection(hostId)} (${code})`;
|
|
1801
|
-
setState({ status: "rejected", reason });
|
|
1802
|
-
delay = maxDelay;
|
|
1803
|
-
reconnect(maxDelay);
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
setState({ status: "connecting" });
|
|
1807
|
-
reconnect(delay);
|
|
1808
|
-
delay = Math.min(maxDelay, delay * 2);
|
|
1809
|
-
}
|
|
1810
|
-
connectControl();
|
|
1811
|
-
return {
|
|
1812
|
-
hostId,
|
|
1813
|
-
origin,
|
|
1814
|
-
state: () => state,
|
|
1815
|
-
close() {
|
|
1816
|
-
stopped = true;
|
|
1817
|
-
if (retryTimer !== undefined)
|
|
1818
|
-
clearTimeout(retryTimer);
|
|
1819
|
-
clearInterval(pingTimer);
|
|
1820
|
-
for (const socket of watched)
|
|
1821
|
-
socket.terminate();
|
|
1822
|
-
return Promise.resolve();
|
|
1823
|
-
}
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
2360
|
// apps/host/src/gateway.ts
|
|
1828
|
-
var
|
|
2361
|
+
var context3 = BACKGROUND_CONTEXT3;
|
|
1829
2362
|
var forkTails = new WeakMap;
|
|
1830
2363
|
function serializedFork(harness, work) {
|
|
1831
2364
|
const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
|
|
@@ -1840,6 +2373,7 @@ function serializedFork(harness, work) {
|
|
|
1840
2373
|
var LOOPBACK_SECURE_PATH = "/secure";
|
|
1841
2374
|
async function startGateway(options) {
|
|
1842
2375
|
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
2376
|
+
const fileLister = createCandidateLister(enumerateRoot);
|
|
1843
2377
|
const detachAutoTitle = options.autoTitle === false ? undefined : attachAutoTitle(conversations, {
|
|
1844
2378
|
harness: options.harness,
|
|
1845
2379
|
models: options.models,
|
|
@@ -1853,8 +2387,8 @@ async function startGateway(options) {
|
|
|
1853
2387
|
});
|
|
1854
2388
|
});
|
|
1855
2389
|
const server = new WebSocketServer({ server: http });
|
|
1856
|
-
await new Promise((
|
|
1857
|
-
http.once("listening",
|
|
2390
|
+
await new Promise((resolve4, reject) => {
|
|
2391
|
+
http.once("listening", resolve4);
|
|
1858
2392
|
http.once("error", reject);
|
|
1859
2393
|
http.listen(options.port, "127.0.0.1");
|
|
1860
2394
|
});
|
|
@@ -1867,8 +2401,11 @@ async function startGateway(options) {
|
|
|
1867
2401
|
};
|
|
1868
2402
|
let remote;
|
|
1869
2403
|
let relay;
|
|
1870
|
-
const register = (connection, devicePublicKey) => {
|
|
1871
|
-
|
|
2404
|
+
const register = (connection, devicePublicKey, joined) => {
|
|
2405
|
+
if (joined?.source === "account") {
|
|
2406
|
+
broadcast("info", `New device "${joined.name}" joined through your account`);
|
|
2407
|
+
}
|
|
2408
|
+
const client = new GatewayClient(connection, options, conversations, fileLister, {
|
|
1872
2409
|
devicePublicKey,
|
|
1873
2410
|
closed: (self) => {
|
|
1874
2411
|
clients.delete(self);
|
|
@@ -1884,20 +2421,30 @@ async function startGateway(options) {
|
|
|
1884
2421
|
const spec = options.secure;
|
|
1885
2422
|
if (spec === undefined)
|
|
1886
2423
|
return;
|
|
1887
|
-
const
|
|
1888
|
-
const pair = `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret}`;
|
|
2424
|
+
const pair = (secret2) => `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret2}`;
|
|
1889
2425
|
if (relay !== undefined) {
|
|
2426
|
+
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
1890
2427
|
const target2 = `${relayWsBase(relay.origin)}/c/${relay.hostId}`;
|
|
1891
|
-
return { url: `${relay.origin}/#${pair}&url=${encodeURIComponent(target2)}`, expiresAt };
|
|
2428
|
+
return { url: `${relay.origin}/#${pair(secret2)}&url=${encodeURIComponent(target2)}`, expiresAt: expiresAt2 };
|
|
2429
|
+
}
|
|
2430
|
+
if (remote !== undefined) {
|
|
2431
|
+
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
2432
|
+
return { url: `${remote.advertiseUrl}/#${pair(secret2)}`, expiresAt: expiresAt2 };
|
|
1892
2433
|
}
|
|
1893
|
-
|
|
1894
|
-
return { url: `${remote.advertiseUrl}/#${pair}`, expiresAt };
|
|
2434
|
+
const { secret, expiresAt } = spec.offers.create("local");
|
|
1895
2435
|
const target = `ws://127.0.0.1:${port}${LOOPBACK_SECURE_PATH}`;
|
|
1896
|
-
return { url: `http://127.0.0.1:${port}/#${pair}&url=${encodeURIComponent(target)}`, expiresAt };
|
|
2436
|
+
return { url: `http://127.0.0.1:${port}/#${pair(secret)}&url=${encodeURIComponent(target)}`, expiresAt };
|
|
1897
2437
|
}
|
|
1898
2438
|
});
|
|
1899
2439
|
clients.add(client);
|
|
1900
2440
|
};
|
|
2441
|
+
const admission = {
|
|
2442
|
+
harness: options.harness,
|
|
2443
|
+
acceptAccountDevices: () => options.settings.value.acceptAccountDevices,
|
|
2444
|
+
serverBinding: async () => await readServerBinding(options.dataDir).catch(() => {
|
|
2445
|
+
return;
|
|
2446
|
+
})
|
|
2447
|
+
};
|
|
1901
2448
|
server.on("connection", (socket, request) => {
|
|
1902
2449
|
socket.on("error", () => socket.terminate());
|
|
1903
2450
|
const url = new URL(request.url ?? "/", "ws://127.0.0.1");
|
|
@@ -1907,7 +2454,7 @@ async function startGateway(options) {
|
|
|
1907
2454
|
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
1908
2455
|
return;
|
|
1909
2456
|
}
|
|
1910
|
-
secureHandshake(socket, secure2,
|
|
2457
|
+
secureHandshake(socket, secure2, admission, register);
|
|
1911
2458
|
return;
|
|
1912
2459
|
}
|
|
1913
2460
|
const token = url.searchParams.get("token");
|
|
@@ -1918,17 +2465,38 @@ async function startGateway(options) {
|
|
|
1918
2465
|
}
|
|
1919
2466
|
register(tokenConnection(socket));
|
|
1920
2467
|
});
|
|
1921
|
-
remote = await startRemote(options, register);
|
|
2468
|
+
remote = await startRemote(options, admission, register);
|
|
1922
2469
|
const secure = options.secure;
|
|
2470
|
+
const detachSettingsWatch = options.settings.subscribe((settings) => {
|
|
2471
|
+
if (!settings.acceptAccountDevices)
|
|
2472
|
+
secure?.offers.revokeAccount();
|
|
2473
|
+
});
|
|
1923
2474
|
if (secure?.relay !== undefined) {
|
|
2475
|
+
const relayOrigin = secure.relay.origin;
|
|
1924
2476
|
let link;
|
|
1925
2477
|
link = startRelayLink({
|
|
1926
|
-
origin:
|
|
2478
|
+
origin: relayOrigin,
|
|
1927
2479
|
signingKey: secure.relay.signingKey,
|
|
1928
|
-
|
|
2480
|
+
noisePublicKey: secure.hostKey.publicKey,
|
|
2481
|
+
accept: (socket) => secureHandshake(socket, secure, admission, register),
|
|
2482
|
+
accountOffer: async () => {
|
|
2483
|
+
if (!options.settings.value.acceptAccountDevices)
|
|
2484
|
+
return { ok: false, reason: "disabled" };
|
|
2485
|
+
const config = await readServerConfig(options.dataDir).catch(() => {
|
|
2486
|
+
return;
|
|
2487
|
+
});
|
|
2488
|
+
if (config?.binding === undefined)
|
|
2489
|
+
return { ok: false, reason: "not_joined" };
|
|
2490
|
+
if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin)) {
|
|
2491
|
+
return { ok: false, reason: "rebinding" };
|
|
2492
|
+
}
|
|
2493
|
+
const { secret, expiresAt } = secure.offers.createAccount(config.binding);
|
|
2494
|
+
return { ok: true, secret, expiresAt };
|
|
2495
|
+
},
|
|
1929
2496
|
onStateChange: (state) => {
|
|
1930
2497
|
if (state.status === "registered") {
|
|
1931
|
-
|
|
2498
|
+
const push = state.push === undefined ? "undeclared" : state.push.length === 0 ? "none" : state.push.join(",");
|
|
2499
|
+
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId, push }));
|
|
1932
2500
|
} else if (state.status === "connecting") {
|
|
1933
2501
|
console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
|
|
1934
2502
|
} else {
|
|
@@ -1941,6 +2509,13 @@ async function startGateway(options) {
|
|
|
1941
2509
|
});
|
|
1942
2510
|
relay = link;
|
|
1943
2511
|
}
|
|
2512
|
+
const detachPush = options.push === undefined ? undefined : await attachPushSender({
|
|
2513
|
+
harness: options.harness,
|
|
2514
|
+
conversations,
|
|
2515
|
+
hostKey: options.push.hostKey,
|
|
2516
|
+
...options.push.transport === undefined ? relay === undefined ? {} : { transport: { send: (request) => relay.sendPush(request) } } : { transport: options.push.transport },
|
|
2517
|
+
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
2518
|
+
});
|
|
1944
2519
|
return {
|
|
1945
2520
|
url: `ws://127.0.0.1:${port}`,
|
|
1946
2521
|
...remote === undefined ? {} : { remote: { url: remote.url, advertiseUrl: remote.advertiseUrl } },
|
|
@@ -1949,22 +2524,24 @@ async function startGateway(options) {
|
|
|
1949
2524
|
async close() {
|
|
1950
2525
|
await relay?.close();
|
|
1951
2526
|
detachAutoTitle?.();
|
|
2527
|
+
detachSettingsWatch();
|
|
2528
|
+
await detachPush?.close();
|
|
1952
2529
|
conversations.dispose();
|
|
1953
2530
|
await Promise.all([...clients].map((client) => client.dispose()));
|
|
1954
2531
|
for (const socket of server.clients)
|
|
1955
2532
|
socket.terminate();
|
|
1956
2533
|
await remote?.close();
|
|
1957
|
-
await new Promise((
|
|
1958
|
-
await new Promise((
|
|
2534
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
2535
|
+
await new Promise((resolve4) => http.close(() => resolve4()));
|
|
1959
2536
|
}
|
|
1960
2537
|
};
|
|
1961
2538
|
}
|
|
1962
2539
|
function tokenConnection(socket) {
|
|
1963
2540
|
return {
|
|
1964
2541
|
get open() {
|
|
1965
|
-
return socket.readyState ===
|
|
2542
|
+
return socket.readyState === WebSocket.OPEN;
|
|
1966
2543
|
},
|
|
1967
|
-
send: (text) => socket.readyState ===
|
|
2544
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve4) => socket.send(text, () => resolve4())) : Promise.resolve(),
|
|
1968
2545
|
close: (code, reason) => socket.close(code, reason),
|
|
1969
2546
|
handle(handlers) {
|
|
1970
2547
|
socket.on("message", (data) => handlers.message(String(data)));
|
|
@@ -1975,11 +2552,11 @@ function tokenConnection(socket) {
|
|
|
1975
2552
|
function secureConnection(socket, session) {
|
|
1976
2553
|
return {
|
|
1977
2554
|
get open() {
|
|
1978
|
-
return socket.readyState ===
|
|
2555
|
+
return socket.readyState === WebSocket.OPEN;
|
|
1979
2556
|
},
|
|
1980
|
-
send: (text) => socket.readyState ===
|
|
2557
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve4) => {
|
|
1981
2558
|
const chunks = session.seal(text);
|
|
1982
|
-
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () =>
|
|
2559
|
+
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () => resolve4() : undefined));
|
|
1983
2560
|
}) : Promise.resolve(),
|
|
1984
2561
|
close: (code, reason) => socket.close(code, reason),
|
|
1985
2562
|
handle(handlers) {
|
|
@@ -2012,7 +2589,7 @@ function pickLanAddress(interfaces) {
|
|
|
2012
2589
|
}
|
|
2013
2590
|
return fallback;
|
|
2014
2591
|
}
|
|
2015
|
-
function secureHandshake(socket, secure,
|
|
2592
|
+
function secureHandshake(socket, secure, policy, register) {
|
|
2016
2593
|
socket.on("error", () => socket.terminate());
|
|
2017
2594
|
const responder = respondIK({ prologue: SECURE_PROLOGUE, static: secure.hostKey });
|
|
2018
2595
|
let settled = false;
|
|
@@ -2034,7 +2611,7 @@ function secureHandshake(socket, secure, harness, register) {
|
|
|
2034
2611
|
clearTimeout(timeout);
|
|
2035
2612
|
const session = new SecureSession(admitted.result);
|
|
2036
2613
|
socket.send(admitted.message);
|
|
2037
|
-
register(secureConnection(socket, session), admitted.deviceKey);
|
|
2614
|
+
register(secureConnection(socket, session), admitted.deviceKey, admitted.joined);
|
|
2038
2615
|
}).catch(() => socket.close(1011, "handshake failed"));
|
|
2039
2616
|
async function admit() {
|
|
2040
2617
|
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
@@ -2049,19 +2626,28 @@ function secureHandshake(socket, secure, harness, register) {
|
|
|
2049
2626
|
return;
|
|
2050
2627
|
}
|
|
2051
2628
|
const deviceKey = toBase64Url(remoteStatic);
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2629
|
+
const decision = await admitDevice(policy.harness, {
|
|
2630
|
+
publicKey: deviceKey,
|
|
2631
|
+
...hello.pair === undefined ? {} : { pair: hello.pair },
|
|
2632
|
+
offers: secure.offers,
|
|
2633
|
+
acceptAccountDevices: policy.acceptAccountDevices,
|
|
2634
|
+
serverBinding: policy.serverBinding
|
|
2635
|
+
}, context3);
|
|
2636
|
+
if (decision.outcome === "refused") {
|
|
2637
|
+
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
2638
|
+
return;
|
|
2058
2639
|
}
|
|
2059
2640
|
const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
|
|
2060
|
-
return {
|
|
2641
|
+
return {
|
|
2642
|
+
result: result2,
|
|
2643
|
+
message,
|
|
2644
|
+
deviceKey,
|
|
2645
|
+
...decision.outcome === "admitted" ? { joined: decision.entry } : {}
|
|
2646
|
+
};
|
|
2061
2647
|
}
|
|
2062
2648
|
});
|
|
2063
2649
|
}
|
|
2064
|
-
async function startRemote(options, register) {
|
|
2650
|
+
async function startRemote(options, admission, register) {
|
|
2065
2651
|
const spec = options.secure;
|
|
2066
2652
|
const remote = spec?.lan;
|
|
2067
2653
|
if (spec === undefined || remote === undefined)
|
|
@@ -2074,7 +2660,7 @@ async function startRemote(options, register) {
|
|
|
2074
2660
|
});
|
|
2075
2661
|
});
|
|
2076
2662
|
const secure = new WebSocketServer({ server: http });
|
|
2077
|
-
secure.on("connection", (socket) => secureHandshake(socket, spec,
|
|
2663
|
+
secure.on("connection", (socket) => secureHandshake(socket, spec, admission, register));
|
|
2078
2664
|
await new Promise((resolveListen, reject) => {
|
|
2079
2665
|
http.once("listening", resolveListen);
|
|
2080
2666
|
http.once("error", reject);
|
|
@@ -2089,8 +2675,8 @@ async function startRemote(options, register) {
|
|
|
2089
2675
|
async close() {
|
|
2090
2676
|
for (const socket of secure.clients)
|
|
2091
2677
|
socket.terminate();
|
|
2092
|
-
await new Promise((
|
|
2093
|
-
await new Promise((
|
|
2678
|
+
await new Promise((resolve4) => secure.close(() => resolve4()));
|
|
2679
|
+
await new Promise((resolve4) => http.close(() => resolve4()));
|
|
2094
2680
|
}
|
|
2095
2681
|
};
|
|
2096
2682
|
}
|
|
@@ -2101,6 +2687,7 @@ class ConversationList {
|
|
|
2101
2687
|
#questionKinds;
|
|
2102
2688
|
#records = new Map;
|
|
2103
2689
|
#facts = new Map;
|
|
2690
|
+
#roles = new Map;
|
|
2104
2691
|
#index = new Map;
|
|
2105
2692
|
#runEnd = new Set;
|
|
2106
2693
|
#list = [];
|
|
@@ -2114,41 +2701,48 @@ class ConversationList {
|
|
|
2114
2701
|
const list = new ConversationList(docs);
|
|
2115
2702
|
const summaries = [];
|
|
2116
2703
|
let cursor;
|
|
2704
|
+
const records = [];
|
|
2117
2705
|
do {
|
|
2118
|
-
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor),
|
|
2706
|
+
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context3);
|
|
2119
2707
|
for (const record of page.items) {
|
|
2120
2708
|
list.#records.set(record.id, record);
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
list.#index.set(record.id, summaries.length);
|
|
2124
|
-
summaries.push(summaryOf(record, facts));
|
|
2709
|
+
list.#facts.set(record.id, await list.#load(harness, record));
|
|
2710
|
+
records.push(record);
|
|
2125
2711
|
}
|
|
2126
2712
|
cursor = page.next;
|
|
2127
2713
|
} while (cursor !== undefined);
|
|
2714
|
+
list.#roles = rolesOf(await harness.snapshot(IndexDoc, context3));
|
|
2715
|
+
for (const record of records) {
|
|
2716
|
+
list.#index.set(record.id, summaries.length);
|
|
2717
|
+
summaries.push(list.#summaryOf(record));
|
|
2718
|
+
}
|
|
2128
2719
|
list.#list = summaries;
|
|
2129
2720
|
list.#unsubscribe = harness.subscribeCommits((publication) => list.#observe(publication));
|
|
2721
|
+
const touched = new Set;
|
|
2722
|
+
list.#reindex(await harness.snapshot(IndexDoc, context3), touched);
|
|
2723
|
+
list.#refreshSummaries(touched);
|
|
2130
2724
|
return list;
|
|
2131
2725
|
}
|
|
2132
2726
|
async#load(harness, record) {
|
|
2133
2727
|
const history = await historyFacts(harness, record.id);
|
|
2134
2728
|
const facts = { pending: new Set, ...history };
|
|
2135
|
-
const live = await harness.snapshot(LiveDoc, record.id,
|
|
2729
|
+
const live = await harness.snapshot(LiveDoc, record.id, context3);
|
|
2136
2730
|
facts.running = live?.run !== undefined;
|
|
2137
2731
|
facts.tools = live?.tools;
|
|
2138
2732
|
for (const token of this.#questionTokens) {
|
|
2139
|
-
const value = await harness.snapshot(token, record.id,
|
|
2733
|
+
const value = await harness.snapshot(token, record.id, context3);
|
|
2140
2734
|
if (Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
|
|
2141
2735
|
facts.pending.add(token.definition.kind);
|
|
2142
2736
|
}
|
|
2143
2737
|
}
|
|
2144
2738
|
if (record.owner !== undefined) {
|
|
2145
|
-
const subagent = await harness.snapshot(SubagentDoc, record.id,
|
|
2739
|
+
const subagent = await harness.snapshot(SubagentDoc, record.id, context3);
|
|
2146
2740
|
if (typeof subagent?.label === "string" && subagent.label !== "")
|
|
2147
2741
|
facts.label = subagent.label;
|
|
2148
2742
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
2149
2743
|
facts.role = subagent.role;
|
|
2150
2744
|
}
|
|
2151
|
-
const named = (await harness.snapshot(TitleDoc, record.id,
|
|
2745
|
+
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
2152
2746
|
if (typeof named === "string" && named !== "")
|
|
2153
2747
|
facts.named = named;
|
|
2154
2748
|
return facts;
|
|
@@ -2180,8 +2774,11 @@ class ConversationList {
|
|
|
2180
2774
|
facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
|
|
2181
2775
|
} else if (change.type === "document") {
|
|
2182
2776
|
const id = change.conversationId;
|
|
2183
|
-
if (id === undefined)
|
|
2777
|
+
if (id === undefined) {
|
|
2778
|
+
if (change.record.kind === IndexDoc.definition.kind)
|
|
2779
|
+
this.#reindex(change.value, touched);
|
|
2184
2780
|
continue;
|
|
2781
|
+
}
|
|
2185
2782
|
const facts = this.#fact(id);
|
|
2186
2783
|
if (change.record.kind === "pi.live") {
|
|
2187
2784
|
const live = change.value;
|
|
@@ -2194,12 +2791,12 @@ class ConversationList {
|
|
|
2194
2791
|
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
2195
2792
|
const value = change.value;
|
|
2196
2793
|
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
2197
|
-
const
|
|
2794
|
+
const next = new Set(facts.pending);
|
|
2198
2795
|
if (pending)
|
|
2199
|
-
|
|
2796
|
+
next.add(change.record.kind);
|
|
2200
2797
|
else
|
|
2201
|
-
|
|
2202
|
-
facts.pending =
|
|
2798
|
+
next.delete(change.record.kind);
|
|
2799
|
+
facts.pending = next;
|
|
2203
2800
|
touched.add(id);
|
|
2204
2801
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
2205
2802
|
const value = change.value;
|
|
@@ -2213,12 +2810,35 @@ class ConversationList {
|
|
|
2213
2810
|
}
|
|
2214
2811
|
}
|
|
2215
2812
|
}
|
|
2813
|
+
this.#refreshSummaries(touched);
|
|
2814
|
+
for (const id of ended)
|
|
2815
|
+
for (const listener of this.#runEnd)
|
|
2816
|
+
listener(id);
|
|
2817
|
+
}
|
|
2818
|
+
onRunEnd(listener) {
|
|
2819
|
+
this.#runEnd.add(listener);
|
|
2820
|
+
return () => this.#runEnd.delete(listener);
|
|
2821
|
+
}
|
|
2822
|
+
#reindex(index, touched) {
|
|
2823
|
+
const next = rolesOf(index);
|
|
2824
|
+
for (const record of this.#records.values()) {
|
|
2825
|
+
if (record.owner === undefined && topLevelRole(record, this.#records, this.#roles) !== topLevelRole(record, this.#records, next)) {
|
|
2826
|
+
touched.add(record.id);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
this.#roles = next;
|
|
2830
|
+
}
|
|
2831
|
+
#summaryOf(record) {
|
|
2832
|
+
const facts = this.#fact(record.id);
|
|
2833
|
+
return summaryOf(record, facts, record.owner === undefined ? topLevelRole(record, this.#records, this.#roles) : facts.role);
|
|
2834
|
+
}
|
|
2835
|
+
#refreshSummaries(touched) {
|
|
2216
2836
|
let next;
|
|
2217
2837
|
for (const id of touched) {
|
|
2218
2838
|
const record = this.#records.get(id);
|
|
2219
2839
|
if (record === undefined)
|
|
2220
2840
|
continue;
|
|
2221
|
-
const summary = summaryOf(record
|
|
2841
|
+
const summary = this.#summaryOf(record);
|
|
2222
2842
|
const index = this.#index.get(id);
|
|
2223
2843
|
if (index === undefined) {
|
|
2224
2844
|
next ??= this.#list.slice();
|
|
@@ -2231,13 +2851,6 @@ class ConversationList {
|
|
|
2231
2851
|
}
|
|
2232
2852
|
if (next !== undefined)
|
|
2233
2853
|
this.#set(next);
|
|
2234
|
-
for (const id of ended)
|
|
2235
|
-
for (const listener of this.#runEnd)
|
|
2236
|
-
listener(id);
|
|
2237
|
-
}
|
|
2238
|
-
onRunEnd(listener) {
|
|
2239
|
-
this.#runEnd.add(listener);
|
|
2240
|
-
return () => this.#runEnd.delete(listener);
|
|
2241
2854
|
}
|
|
2242
2855
|
#fact(id) {
|
|
2243
2856
|
let facts = this.#facts.get(id);
|
|
@@ -2268,7 +2881,26 @@ class ConversationList {
|
|
|
2268
2881
|
this.#listeners.clear();
|
|
2269
2882
|
}
|
|
2270
2883
|
}
|
|
2271
|
-
function
|
|
2884
|
+
function rolesOf(index) {
|
|
2885
|
+
const roles = new Map;
|
|
2886
|
+
for (const entry of index?.conversations ?? []) {
|
|
2887
|
+
if (typeof entry.role === "string" && entry.role !== "")
|
|
2888
|
+
roles.set(entry.id, entry.role);
|
|
2889
|
+
}
|
|
2890
|
+
return roles;
|
|
2891
|
+
}
|
|
2892
|
+
function topLevelRole(record, records, roles) {
|
|
2893
|
+
let current = record;
|
|
2894
|
+
const seen = new Set;
|
|
2895
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
2896
|
+
seen.add(current.id);
|
|
2897
|
+
if (current.parent === undefined)
|
|
2898
|
+
return roles.get(current.id);
|
|
2899
|
+
current = records.get(current.parent.conversationId);
|
|
2900
|
+
}
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
function summaryOf(record, facts, role) {
|
|
2272
2904
|
const parent = record.parent?.conversationId ?? record.owner?.conversationId;
|
|
2273
2905
|
const status = facts.pending.size > 0 ? "needs-answer" : facts.running === true ? "running" : facts.failed === true ? "failed" : undefined;
|
|
2274
2906
|
const runningSlot = facts.running === true ? facts.tools?.find((slot) => slot.status === "running") : undefined;
|
|
@@ -2285,7 +2917,7 @@ function summaryOf(record, facts) {
|
|
|
2285
2917
|
...status === undefined ? {} : { status },
|
|
2286
2918
|
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2287
2919
|
...facts.label === undefined ? {} : { label: facts.label },
|
|
2288
|
-
...
|
|
2920
|
+
...role === undefined ? {} : { role },
|
|
2289
2921
|
...activity === undefined ? {} : { activity }
|
|
2290
2922
|
};
|
|
2291
2923
|
}
|
|
@@ -2301,12 +2933,12 @@ function titleOf(entry) {
|
|
|
2301
2933
|
return title === "" ? {} : { title };
|
|
2302
2934
|
}
|
|
2303
2935
|
async function historyFacts(harness, id) {
|
|
2304
|
-
const conversation = await harness.conversation(id,
|
|
2936
|
+
const conversation = await harness.conversation(id, context3);
|
|
2305
2937
|
let first;
|
|
2306
2938
|
let newest;
|
|
2307
2939
|
let cursor;
|
|
2308
2940
|
do {
|
|
2309
|
-
const page = await conversation.entries({}, 256, cursor,
|
|
2941
|
+
const page = await conversation.entries({}, 256, cursor, context3);
|
|
2310
2942
|
first = page.items.findLast((entry) => entry.kind === "pi.user" && entry.conversationId === id && titleOf(entry).title !== undefined) ?? first;
|
|
2311
2943
|
newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
|
|
2312
2944
|
cursor = page.next;
|
|
@@ -2341,12 +2973,14 @@ class GatewayClient {
|
|
|
2341
2973
|
#connection;
|
|
2342
2974
|
#hooks;
|
|
2343
2975
|
#options;
|
|
2976
|
+
#files;
|
|
2344
2977
|
#hostValues;
|
|
2345
2978
|
#subscriptions = new Map;
|
|
2346
2979
|
#disposed = false;
|
|
2347
|
-
constructor(connection, options, conversations, hooks) {
|
|
2980
|
+
constructor(connection, options, conversations, files, hooks) {
|
|
2348
2981
|
this.#connection = connection;
|
|
2349
2982
|
this.#options = options;
|
|
2983
|
+
this.#files = files;
|
|
2350
2984
|
this.#hostValues = {
|
|
2351
2985
|
conversations,
|
|
2352
2986
|
mcp: options.mcp ?? constantValue(null),
|
|
@@ -2450,25 +3084,25 @@ class GatewayClient {
|
|
|
2450
3084
|
return { stop: source.subscribe((value) => void send({ type: "snapshot", stream, value })) };
|
|
2451
3085
|
}
|
|
2452
3086
|
if (stream === "index") {
|
|
2453
|
-
const watch2 = await this.#options.harness.watchDoc(IndexDoc,
|
|
3087
|
+
const watch2 = await this.#options.harness.watchDoc(IndexDoc, context3);
|
|
2454
3088
|
if (watch2 === undefined)
|
|
2455
3089
|
throw new Error("Index document is missing");
|
|
2456
3090
|
return this.#forward(stream, watch2, send);
|
|
2457
3091
|
}
|
|
2458
3092
|
if (stream === "devices") {
|
|
2459
|
-
const watch2 = await this.#options.harness.watchDoc(DevicesDoc,
|
|
3093
|
+
const watch2 = await this.#options.harness.watchDoc(DevicesDoc, context3);
|
|
2460
3094
|
if (watch2 === undefined)
|
|
2461
3095
|
throw new Error("Devices document is missing");
|
|
2462
3096
|
return this.#forward(stream, watch2, send);
|
|
2463
3097
|
}
|
|
2464
3098
|
if (stream === "tasks")
|
|
2465
|
-
return this.#forward(stream, await this.#options.harness.watchTaskGraph(
|
|
3099
|
+
return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
|
|
2466
3100
|
if (stream.startsWith("doc:")) {
|
|
2467
3101
|
const at = stream.lastIndexOf(":");
|
|
2468
3102
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
2469
3103
|
}
|
|
2470
3104
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
2471
|
-
return this.#forward(stream, await conversation.watch(
|
|
3105
|
+
return this.#forward(stream, await conversation.watch(context3), send);
|
|
2472
3106
|
}
|
|
2473
3107
|
#hostValue(stream) {
|
|
2474
3108
|
return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
|
|
@@ -2481,8 +3115,8 @@ class GatewayClient {
|
|
|
2481
3115
|
await this.#conversation(id);
|
|
2482
3116
|
const { harness } = this.#options;
|
|
2483
3117
|
let signalCreation;
|
|
2484
|
-
const created = new Promise((
|
|
2485
|
-
signalCreation =
|
|
3118
|
+
const created = new Promise((resolve4) => {
|
|
3119
|
+
signalCreation = resolve4;
|
|
2486
3120
|
});
|
|
2487
3121
|
let inner;
|
|
2488
3122
|
let stopped = false;
|
|
@@ -2494,7 +3128,7 @@ class GatewayClient {
|
|
|
2494
3128
|
setImmediate(() => signalCreation());
|
|
2495
3129
|
});
|
|
2496
3130
|
try {
|
|
2497
|
-
const existing = await harness.watchDoc(token, id,
|
|
3131
|
+
const existing = await harness.watchDoc(token, id, context3);
|
|
2498
3132
|
if (existing !== undefined) {
|
|
2499
3133
|
unsubscribe();
|
|
2500
3134
|
return this.#forward(stream, existing, send);
|
|
@@ -2507,7 +3141,7 @@ class GatewayClient {
|
|
|
2507
3141
|
created.then(async () => {
|
|
2508
3142
|
if (stopped)
|
|
2509
3143
|
return;
|
|
2510
|
-
const watch2 = await harness.watchDoc(token, id,
|
|
3144
|
+
const watch2 = await harness.watchDoc(token, id, context3);
|
|
2511
3145
|
if (watch2 === undefined)
|
|
2512
3146
|
return;
|
|
2513
3147
|
if (stopped)
|
|
@@ -2541,7 +3175,7 @@ class GatewayClient {
|
|
|
2541
3175
|
await subscription?.subscription?.stop();
|
|
2542
3176
|
}
|
|
2543
3177
|
async#conversation(id) {
|
|
2544
|
-
const conversation = await this.#options.harness.conversation(id,
|
|
3178
|
+
const conversation = await this.#options.harness.conversation(id, context3);
|
|
2545
3179
|
if (conversation === undefined)
|
|
2546
3180
|
throw new Error(`Conversation ${id} does not exist`);
|
|
2547
3181
|
return conversation;
|
|
@@ -2557,9 +3191,9 @@ class GatewayClient {
|
|
|
2557
3191
|
async#call(method, args) {
|
|
2558
3192
|
switch (method) {
|
|
2559
3193
|
case "addProject":
|
|
2560
|
-
return addProject(this.#options.harness, args.path,
|
|
3194
|
+
return addProject(this.#options.harness, args.path, context3);
|
|
2561
3195
|
case "removeProject":
|
|
2562
|
-
await removeProject(this.#options.harness, args.path,
|
|
3196
|
+
await removeProject(this.#options.harness, args.path, context3);
|
|
2563
3197
|
return null;
|
|
2564
3198
|
case "createConversation": {
|
|
2565
3199
|
const create = args;
|
|
@@ -2573,15 +3207,16 @@ class GatewayClient {
|
|
|
2573
3207
|
...create.images === undefined ? {} : { images: create.images },
|
|
2574
3208
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
2575
3209
|
...create.model === undefined ? {} : { model: create.model },
|
|
2576
|
-
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel }
|
|
2577
|
-
|
|
3210
|
+
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel },
|
|
3211
|
+
...create.agent === undefined ? {} : { agent: create.agent }
|
|
3212
|
+
}, settingsDefaults(this.#options.settings.value), this.#options.models, this.#options.modelSummaries, this.#options.agents?.value ?? { roles: [], problems: [] }, context3);
|
|
2578
3213
|
return { conversationId };
|
|
2579
3214
|
}
|
|
2580
3215
|
case "archive": {
|
|
2581
3216
|
const input = args;
|
|
2582
|
-
await archive(this.#options.harness, input.conversationId, input.archived,
|
|
3217
|
+
await archive(this.#options.harness, input.conversationId, input.archived, context3);
|
|
2583
3218
|
if (input.archived) {
|
|
2584
|
-
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId,
|
|
3219
|
+
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
|
|
2585
3220
|
for (const { record, reason } of kept) {
|
|
2586
3221
|
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
2587
3222
|
}
|
|
@@ -2596,10 +3231,20 @@ class GatewayClient {
|
|
|
2596
3231
|
}
|
|
2597
3232
|
case "revokeDevice": {
|
|
2598
3233
|
const { publicKey } = args;
|
|
2599
|
-
await revokeDevice(this.#options.harness, publicKey,
|
|
3234
|
+
await revokeDevice(this.#options.harness, publicKey, context3);
|
|
2600
3235
|
this.#hooks.closeDevice(publicKey);
|
|
2601
3236
|
return null;
|
|
2602
3237
|
}
|
|
3238
|
+
case "setPushToken": {
|
|
3239
|
+
const device = this.devicePublicKey;
|
|
3240
|
+
if (device === undefined)
|
|
3241
|
+
throw new Error("Push tokens register over the secure channel only");
|
|
3242
|
+
const joined = async () => await readServerConfig(this.#options.dataDir).catch(() => {
|
|
3243
|
+
return;
|
|
3244
|
+
}) !== undefined;
|
|
3245
|
+
await setPushToken(this.#options.harness, device, args.token, context3, joined);
|
|
3246
|
+
return null;
|
|
3247
|
+
}
|
|
2603
3248
|
case "logout": {
|
|
2604
3249
|
const modelAuth = this.#options.modelAuth;
|
|
2605
3250
|
if (modelAuth === undefined)
|
|
@@ -2634,6 +3279,10 @@ class GatewayClient {
|
|
|
2634
3279
|
}
|
|
2635
3280
|
case "listFolders":
|
|
2636
3281
|
return listFolders(args.path, homedir2());
|
|
3282
|
+
case "listFiles": {
|
|
3283
|
+
const input = args;
|
|
3284
|
+
return listFiles(this.#options.harness, this.#options.dataDir, this.#files, input.scope, input.query, context3);
|
|
3285
|
+
}
|
|
2637
3286
|
}
|
|
2638
3287
|
const conversation = await this.#conversation(args.conversationId);
|
|
2639
3288
|
switch (method) {
|
|
@@ -2642,9 +3291,9 @@ class GatewayClient {
|
|
|
2642
3291
|
const problem = imageProblem(images ?? []);
|
|
2643
3292
|
if (problem !== undefined)
|
|
2644
3293
|
throw new Error(problem);
|
|
2645
|
-
const dismissed = await this.#dismissPendingQuestions(conversation.id,
|
|
2646
|
-
const submission = await conversation.submit({ type: "input", content: messageContent(text, images), whenBusy: dismissed ? "steer" : whenBusy, requestId },
|
|
2647
|
-
submission.wait(
|
|
3294
|
+
const dismissed = await this.#dismissPendingQuestions(conversation.id, context3);
|
|
3295
|
+
const submission = await conversation.submit({ type: "input", content: messageContent(text, images), whenBusy: dismissed ? "steer" : whenBusy, requestId }, context3);
|
|
3296
|
+
submission.wait(context3).then((settled) => {
|
|
2648
3297
|
if (settled.status === "unanswered" && settled.reason !== "aborted") {
|
|
2649
3298
|
this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
|
|
2650
3299
|
}
|
|
@@ -2668,16 +3317,16 @@ class GatewayClient {
|
|
|
2668
3317
|
const doc = await tx.doc(TitleDoc, conversation.id);
|
|
2669
3318
|
doc.title = trimmed;
|
|
2670
3319
|
doc.source = "manual";
|
|
2671
|
-
},
|
|
3320
|
+
}, context3);
|
|
2672
3321
|
return null;
|
|
2673
3322
|
}
|
|
2674
3323
|
case "abort":
|
|
2675
|
-
await conversation.abort(
|
|
3324
|
+
await conversation.abort(context3);
|
|
2676
3325
|
return null;
|
|
2677
3326
|
case "compact": {
|
|
2678
3327
|
const { instructions } = args;
|
|
2679
|
-
const taskId = await conversation.compact(instructions,
|
|
2680
|
-
this.#options.harness.waitForTask(taskId,
|
|
3328
|
+
const taskId = await conversation.compact(instructions, context3);
|
|
3329
|
+
this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
2681
3330
|
return { taskId };
|
|
2682
3331
|
}
|
|
2683
3332
|
case "setModel": {
|
|
@@ -2686,7 +3335,7 @@ class GatewayClient {
|
|
|
2686
3335
|
if (model === undefined)
|
|
2687
3336
|
throw new Error(`Unknown model: ${ref.provider}/${ref.modelId}`);
|
|
2688
3337
|
const thinking = (await this.#agent(conversation.id)).thinkingLevel ?? "off";
|
|
2689
|
-
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) },
|
|
3338
|
+
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) }, context3);
|
|
2690
3339
|
return null;
|
|
2691
3340
|
}
|
|
2692
3341
|
case "setThinkingLevel": {
|
|
@@ -2698,7 +3347,7 @@ class GatewayClient {
|
|
|
2698
3347
|
if (!getSupportedThinkingLevels(model).includes(level)) {
|
|
2699
3348
|
throw new Error(`Thinking level ${level} is not supported by ${agent.model.provider}/${agent.model.modelId}`);
|
|
2700
3349
|
}
|
|
2701
|
-
await conversation.configure({ thinkingLevel: level },
|
|
3350
|
+
await conversation.configure({ thinkingLevel: level }, context3);
|
|
2702
3351
|
return null;
|
|
2703
3352
|
}
|
|
2704
3353
|
case "fork": {
|
|
@@ -2722,13 +3371,13 @@ class GatewayClient {
|
|
|
2722
3371
|
}
|
|
2723
3372
|
cursor = page.next;
|
|
2724
3373
|
} while (cursor !== undefined);
|
|
2725
|
-
},
|
|
2726
|
-
const agent = await conversation.agent(
|
|
3374
|
+
}, context3);
|
|
3375
|
+
const agent = await conversation.agent(context3);
|
|
2727
3376
|
const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
|
|
2728
3377
|
const fork = await conversation.fork(at, {
|
|
2729
3378
|
ownership: { kind: "ownerless" },
|
|
2730
3379
|
...remove.length === 0 ? {} : { agent: { tools: { remove } } }
|
|
2731
|
-
},
|
|
3380
|
+
}, context3);
|
|
2732
3381
|
return { conversationId: fork.id };
|
|
2733
3382
|
});
|
|
2734
3383
|
}
|
|
@@ -2764,16 +3413,16 @@ class GatewayClient {
|
|
|
2764
3413
|
}
|
|
2765
3414
|
request.resolution = { outcome: "answered", answers, at: Date.now() };
|
|
2766
3415
|
return { first: true };
|
|
2767
|
-
},
|
|
3416
|
+
}, context3);
|
|
2768
3417
|
}
|
|
2769
3418
|
case "changes": {
|
|
2770
|
-
return await this.#changes(conversation,
|
|
3419
|
+
return await this.#changes(conversation, context3);
|
|
2771
3420
|
}
|
|
2772
3421
|
}
|
|
2773
3422
|
}
|
|
2774
|
-
async#dismissPendingQuestions(conversationId,
|
|
3423
|
+
async#dismissPendingQuestions(conversationId, context4) {
|
|
2775
3424
|
const questionDocs = (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
2776
|
-
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId,
|
|
3425
|
+
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
2777
3426
|
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
2778
3427
|
if (!pending)
|
|
2779
3428
|
return false;
|
|
@@ -2791,18 +3440,18 @@ class GatewayClient {
|
|
|
2791
3440
|
}
|
|
2792
3441
|
}
|
|
2793
3442
|
return dismissed;
|
|
2794
|
-
},
|
|
3443
|
+
}, context4);
|
|
2795
3444
|
}
|
|
2796
3445
|
get #chatsPrefix() {
|
|
2797
|
-
return
|
|
3446
|
+
return join7(this.#options.dataDir, "chats") + sep4;
|
|
2798
3447
|
}
|
|
2799
|
-
async#changes(conversation,
|
|
2800
|
-
const cwd = (await conversation.agent(
|
|
3448
|
+
async#changes(conversation, context4) {
|
|
3449
|
+
const cwd = (await conversation.agent(context4)).cwd;
|
|
2801
3450
|
if (cwd === undefined)
|
|
2802
3451
|
return { available: false, reason: "No working directory" };
|
|
2803
3452
|
if (cwd.startsWith(this.#chatsPrefix))
|
|
2804
3453
|
return { available: false, reason: "Chat conversations have no repository" };
|
|
2805
|
-
const index = await this.#options.harness.snapshot(IndexDoc,
|
|
3454
|
+
const index = await this.#options.harness.snapshot(IndexDoc, context4);
|
|
2806
3455
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2807
3456
|
if (record !== undefined) {
|
|
2808
3457
|
if (!await worktreeExists(record.path))
|
|
@@ -2815,7 +3464,7 @@ class GatewayClient {
|
|
|
2815
3464
|
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
2816
3465
|
}
|
|
2817
3466
|
async#agent(id) {
|
|
2818
|
-
return await this.#options.harness.snapshot(
|
|
3467
|
+
return await this.#options.harness.snapshot(AgentDoc4, id, context3) ?? {};
|
|
2819
3468
|
}
|
|
2820
3469
|
async dispose() {
|
|
2821
3470
|
if (this.#disposed)
|
|
@@ -2829,9 +3478,9 @@ class GatewayClient {
|
|
|
2829
3478
|
}
|
|
2830
3479
|
|
|
2831
3480
|
// apps/host/src/host.ts
|
|
2832
|
-
var
|
|
3481
|
+
var context4 = BACKGROUND_CONTEXT4;
|
|
2833
3482
|
async function hostToken(dataDir) {
|
|
2834
|
-
const path =
|
|
3483
|
+
const path = join8(dataDir, "token");
|
|
2835
3484
|
try {
|
|
2836
3485
|
return (await readFile4(path, "utf8")).trim();
|
|
2837
3486
|
} catch {
|
|
@@ -2860,7 +3509,7 @@ async function openHost(options) {
|
|
|
2860
3509
|
const reports = [];
|
|
2861
3510
|
let report = (error) => void reports.push(error);
|
|
2862
3511
|
const registry = createRegistry();
|
|
2863
|
-
agentRoles = await openAgentRoles({ dir:
|
|
3512
|
+
agentRoles = await openAgentRoles({ dir: join8(options.dataDir, "agents") });
|
|
2864
3513
|
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
2865
3514
|
harness: () => {
|
|
2866
3515
|
if (harness === undefined)
|
|
@@ -2883,13 +3532,13 @@ async function openHost(options) {
|
|
|
2883
3532
|
}
|
|
2884
3533
|
});
|
|
2885
3534
|
const hostSettings = await openHostSettings({
|
|
2886
|
-
path:
|
|
3535
|
+
path: join8(options.dataDir, "settings.json"),
|
|
2887
3536
|
fallback: options.settingsFallback,
|
|
2888
3537
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
2889
3538
|
});
|
|
2890
3539
|
const environments = new Map;
|
|
2891
|
-
const chatsDir =
|
|
2892
|
-
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(
|
|
3540
|
+
const chatsDir = join8(options.dataDir, "chats");
|
|
3541
|
+
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join8(options.dataDir, "session.sqlite")), {
|
|
2893
3542
|
models: options.models,
|
|
2894
3543
|
registry,
|
|
2895
3544
|
env: async (target) => {
|
|
@@ -2897,8 +3546,8 @@ async function openHost(options) {
|
|
|
2897
3546
|
if (cwd === undefined)
|
|
2898
3547
|
return;
|
|
2899
3548
|
const worktrees = worktreeRoot(options.dataDir);
|
|
2900
|
-
if (cwd === worktrees || cwd.startsWith(worktrees +
|
|
2901
|
-
const index = await target.read.snapshot(IndexDoc,
|
|
3549
|
+
if (cwd === worktrees || cwd.startsWith(worktrees + sep5)) {
|
|
3550
|
+
const index = await target.read.snapshot(IndexDoc, context4);
|
|
2902
3551
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2903
3552
|
if (record !== undefined) {
|
|
2904
3553
|
await ensureWorktree(record);
|
|
@@ -2918,11 +3567,11 @@ async function openHost(options) {
|
|
|
2918
3567
|
settings: typeof options.settings === "function" ? options.settings(hostSettings) : options.settings
|
|
2919
3568
|
},
|
|
2920
3569
|
onReport: (error) => report(error)
|
|
2921
|
-
},
|
|
2922
|
-
await ensureIndex(harness,
|
|
2923
|
-
await ensureDevices(harness,
|
|
3570
|
+
}, context4);
|
|
3571
|
+
await ensureIndex(harness, context4);
|
|
3572
|
+
await ensureDevices(harness, context4);
|
|
2924
3573
|
for (const path of options.projects ?? [])
|
|
2925
|
-
await addProject(harness, path,
|
|
3574
|
+
await addProject(harness, path, context4);
|
|
2926
3575
|
const hostKey = await loadHostKey(options.dataDir);
|
|
2927
3576
|
const offers = pairingOffers(options.remote?.pairingTtlMs);
|
|
2928
3577
|
const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
|
|
@@ -2943,6 +3592,13 @@ async function openHost(options) {
|
|
|
2943
3592
|
...options.modelAuth === undefined ? {} : { modelAuth: options.modelAuth },
|
|
2944
3593
|
...options.webRoot === undefined ? {} : { webRoot: options.webRoot },
|
|
2945
3594
|
...options.autoTitle === undefined ? {} : { autoTitle: options.autoTitle },
|
|
3595
|
+
...options.push === undefined ? {} : {
|
|
3596
|
+
push: {
|
|
3597
|
+
hostKey: toBase64Url(hostKey.publicKey),
|
|
3598
|
+
...options.push.transport === undefined ? {} : { transport: options.push.transport },
|
|
3599
|
+
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
3600
|
+
}
|
|
3601
|
+
},
|
|
2946
3602
|
secure: {
|
|
2947
3603
|
hostKey,
|
|
2948
3604
|
offers,
|
|
@@ -2975,19 +3631,25 @@ async function openHost(options) {
|
|
|
2975
3631
|
harness,
|
|
2976
3632
|
closed: opened.closed,
|
|
2977
3633
|
settings: hostSettings,
|
|
3634
|
+
createAccountOffer: async () => {
|
|
3635
|
+
const binding = await readServerBinding(options.dataDir).catch(() => {
|
|
3636
|
+
return;
|
|
3637
|
+
});
|
|
3638
|
+
return binding === undefined ? undefined : offers.createAccount(binding);
|
|
3639
|
+
},
|
|
2978
3640
|
...gateway.remote === undefined || hostKey === undefined ? {} : { remote: { url: gateway.remote.url, advertiseUrl: gateway.remote.advertiseUrl, hostKey: hostKey.publicKey } },
|
|
2979
3641
|
...gateway.relay === undefined ? {} : { relay: { origin: gateway.relay.origin, hostId: gateway.relay.hostId, state: () => gateway.relay.state() } },
|
|
2980
3642
|
close() {
|
|
2981
3643
|
closing ??= (async () => {
|
|
2982
3644
|
try {
|
|
2983
3645
|
await gateway.close();
|
|
2984
|
-
await opened.close(
|
|
3646
|
+
await opened.close(context4);
|
|
2985
3647
|
await mcp?.close();
|
|
2986
3648
|
agentRoles?.close();
|
|
2987
3649
|
for (const each of extensions)
|
|
2988
3650
|
await each.close?.();
|
|
2989
3651
|
options.modelAuth?.close();
|
|
2990
|
-
await Promise.all([...environments.values()].map((env) => env.cleanup(
|
|
3652
|
+
await Promise.all([...environments.values()].map((env) => env.cleanup(context4)));
|
|
2991
3653
|
} finally {
|
|
2992
3654
|
await release();
|
|
2993
3655
|
}
|
|
@@ -3001,7 +3663,7 @@ async function openHost(options) {
|
|
|
3001
3663
|
for (const each of extensions)
|
|
3002
3664
|
await each.close?.().catch(() => {});
|
|
3003
3665
|
await mcp?.close().catch(() => {});
|
|
3004
|
-
await harness?.close(
|
|
3666
|
+
await harness?.close(context4).catch(() => {});
|
|
3005
3667
|
await release().catch(() => {});
|
|
3006
3668
|
throw error;
|
|
3007
3669
|
}
|
|
@@ -3209,11 +3871,18 @@ async function main(argv) {
|
|
|
3209
3871
|
relay: { type: "string" }
|
|
3210
3872
|
}
|
|
3211
3873
|
});
|
|
3212
|
-
const dataDir =
|
|
3874
|
+
const dataDir = resolve4(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
3875
|
+
let relayOrigin;
|
|
3876
|
+
try {
|
|
3877
|
+
relayOrigin = resolveRelayOrigin(values.relay, await readServerConfig(dataDir));
|
|
3878
|
+
} catch (error) {
|
|
3879
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3880
|
+
process.exit(1);
|
|
3881
|
+
}
|
|
3213
3882
|
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
3214
3883
|
createContext({
|
|
3215
|
-
agentsHome:
|
|
3216
|
-
checkout: (input,
|
|
3884
|
+
agentsHome: join9(homedir3(), ".agents"),
|
|
3885
|
+
checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
|
|
3217
3886
|
}),
|
|
3218
3887
|
createCoding(options2),
|
|
3219
3888
|
todo,
|
|
@@ -3230,7 +3899,7 @@ async function main(argv) {
|
|
|
3230
3899
|
dataDir,
|
|
3231
3900
|
projects: values.project ?? [],
|
|
3232
3901
|
port: Number(values.port),
|
|
3233
|
-
mcpConfig:
|
|
3902
|
+
mcpConfig: join9(homedir3(), ".agents", "mcp.json"),
|
|
3234
3903
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
3235
3904
|
webRoot,
|
|
3236
3905
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -3240,7 +3909,7 @@ async function main(argv) {
|
|
|
3240
3909
|
...values["public-url"] === undefined ? {} : { publicUrl: values["public-url"] }
|
|
3241
3910
|
}
|
|
3242
3911
|
},
|
|
3243
|
-
...
|
|
3912
|
+
...relayOrigin === undefined ? {} : { relay: { origin: relayOrigin } }
|
|
3244
3913
|
};
|
|
3245
3914
|
function fauxOptions(responses) {
|
|
3246
3915
|
const faux = fauxProvider({ tokensPerSecond: Number(values["faux-tps"]), tokenSize: { min: 1, max: 1 } });
|
|
@@ -3274,7 +3943,7 @@ async function main(argv) {
|
|
|
3274
3943
|
}
|
|
3275
3944
|
async function piOptions() {
|
|
3276
3945
|
const modelRuntime = await ModelRuntime.create();
|
|
3277
|
-
const modelAuth = await watchModelAuth(modelRuntime, { authPath:
|
|
3946
|
+
const modelAuth = await watchModelAuth(modelRuntime, { authPath: join9(getAgentDir(), "auth.json") });
|
|
3278
3947
|
const settingsManager = SettingsManager.create(process.cwd());
|
|
3279
3948
|
configureHarnessHttp(settingsManager);
|
|
3280
3949
|
const summaries = () => modelAuth.models.value;
|
|
@@ -3286,6 +3955,7 @@ async function main(argv) {
|
|
|
3286
3955
|
modelAuth,
|
|
3287
3956
|
extensions: extensionsFor(modelRuntime, summaries, { images }),
|
|
3288
3957
|
settings: (store) => createHarnessSettings(settingsManager, store),
|
|
3958
|
+
push: {},
|
|
3289
3959
|
settingsFallback: {
|
|
3290
3960
|
defaultModel: () => {
|
|
3291
3961
|
const model2 = defaultModel(settingsManager, modelRuntime);
|
|
@@ -3305,7 +3975,7 @@ async function main(argv) {
|
|
|
3305
3975
|
console.log(JSON.stringify({
|
|
3306
3976
|
event: "ready",
|
|
3307
3977
|
url: host.url,
|
|
3308
|
-
tokenFile:
|
|
3978
|
+
tokenFile: join9(dataDir, "token"),
|
|
3309
3979
|
dataDir,
|
|
3310
3980
|
model,
|
|
3311
3981
|
web,
|