pinomad 0.4.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-fgx2sh7d.js → chunk-1d9sppj0.js} +1 -1
- package/dist/{chunk-5785kwej.js → chunk-26d04z02.js} +3 -5
- package/dist/{chunk-vw5yb5pd.js → chunk-4by4x5c4.js} +290 -37
- package/dist/{chunk-prhvksh3.js → chunk-5ng86fmy.js} +4 -4
- package/dist/{chunk-b3f0ced1.js → chunk-9d59jjmt.js} +11 -14
- package/dist/{chunk-akgxwg8f.js → chunk-aqj4wg33.js} +2 -2
- package/dist/{chunk-17p6bfvy.js → chunk-crv3dqzp.js} +4 -4
- package/dist/{chunk-z5pmcscx.js → chunk-d4wdt5qs.js} +594 -212
- package/dist/{chunk-837gh8nr.js → chunk-ebfaf3k2.js} +69 -17
- package/dist/{chunk-xhwkx9bd.js → chunk-f0v3sht0.js} +11 -14
- package/dist/{chunk-n73t5xzw.js → chunk-gqr7ahv4.js} +1 -1
- package/dist/{chunk-myjp910s.js → chunk-hsp12cjt.js} +10 -6
- package/dist/{chunk-7gmkgyaz.js → chunk-kmbh44dt.js} +286 -2
- package/dist/{chunk-374zqx5h.js → chunk-m4q9wafw.js} +14 -1
- package/dist/{chunk-d4y99pfy.js → chunk-zttf1hvh.js} +2 -2
- package/dist/pinomad.js +14 -14
- 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-8mNNPXPT.js → index-D4lgm3Y4.js} +89 -89
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-302y6by9.js +0 -50
- package/dist/chunk-knaw1a9g.js +0 -182
- package/dist/chunk-xrvmnymc.js +0 -33
- package/web/assets/index-BFJJgGGo.css +0 -1
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
serveWebClient
|
|
4
|
+
} from "./chunk-r6w5aqkw.js";
|
|
5
|
+
import {
|
|
6
|
+
DevicesDoc,
|
|
7
|
+
RevocationsDoc,
|
|
8
|
+
admitDevice,
|
|
9
|
+
ensureDevices,
|
|
10
|
+
loadHostKey,
|
|
11
|
+
loadRelayKey,
|
|
3
12
|
normalizeOrigin,
|
|
13
|
+
pairingOffers,
|
|
4
14
|
readServerBinding,
|
|
5
15
|
readServerConfig,
|
|
6
16
|
resolveRelayOrigin
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import {
|
|
9
|
-
serveWebClient
|
|
10
|
-
} from "./chunk-r6w5aqkw.js";
|
|
17
|
+
} from "./chunk-ebfaf3k2.js";
|
|
11
18
|
import {
|
|
19
|
+
SECURE_PROLOGUE,
|
|
20
|
+
SecureSession,
|
|
21
|
+
decodeClientHello,
|
|
22
|
+
encodeHostHello,
|
|
12
23
|
relayWsBase,
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
respondIK,
|
|
25
|
+
startRelayLink,
|
|
26
|
+
toBase64Url
|
|
27
|
+
} from "./chunk-kmbh44dt.js";
|
|
15
28
|
import {
|
|
16
29
|
packagedVersion,
|
|
17
30
|
webRoot
|
|
18
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-1d9sppj0.js";
|
|
19
32
|
import {
|
|
20
33
|
HOST_SETTING_KEYS,
|
|
21
34
|
INVALID_FRAME_CLOSE_CODE,
|
|
@@ -26,24 +39,7 @@ import {
|
|
|
26
39
|
isThinkingLevel,
|
|
27
40
|
organize,
|
|
28
41
|
parseClientFrame
|
|
29
|
-
} from "./chunk-
|
|
30
|
-
import {
|
|
31
|
-
DevicesDoc,
|
|
32
|
-
RevocationsDoc,
|
|
33
|
-
admitDevice,
|
|
34
|
-
ensureDevices,
|
|
35
|
-
loadHostKey,
|
|
36
|
-
loadRelayKey,
|
|
37
|
-
pairingOffers
|
|
38
|
-
} from "./chunk-837gh8nr.js";
|
|
39
|
-
import {
|
|
40
|
-
SECURE_PROLOGUE,
|
|
41
|
-
SecureSession,
|
|
42
|
-
decodeClientHello,
|
|
43
|
-
encodeHostHello,
|
|
44
|
-
respondIK,
|
|
45
|
-
toBase64Url
|
|
46
|
-
} from "./chunk-7gmkgyaz.js";
|
|
42
|
+
} from "./chunk-hsp12cjt.js";
|
|
47
43
|
import {
|
|
48
44
|
DEFAULT_DATA_DIR
|
|
49
45
|
} from "./chunk-cjfmfyjb.js";
|
|
@@ -51,7 +47,7 @@ import"./chunk-df32avw6.js";
|
|
|
51
47
|
|
|
52
48
|
// apps/host/src/main.ts
|
|
53
49
|
import { homedir as homedir3 } from "node:os";
|
|
54
|
-
import { join as
|
|
50
|
+
import { join as join9, resolve as resolve4 } from "node:path";
|
|
55
51
|
import { parseArgs } from "node:util";
|
|
56
52
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
57
53
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
@@ -289,6 +285,7 @@ function createContext(options) {
|
|
|
289
285
|
].join(`
|
|
290
286
|
`);
|
|
291
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 }),
|
|
292
289
|
section("project_context", async (input, context) => {
|
|
293
290
|
const cwd = input.agent.cwd;
|
|
294
291
|
if (cwd === undefined)
|
|
@@ -526,7 +523,7 @@ var todo = {
|
|
|
526
523
|
// apps/host/src/host.ts
|
|
527
524
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
528
525
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
529
|
-
import { basename as basename2, join as
|
|
526
|
+
import { basename as basename2, join as join8, sep as sep5 } from "node:path";
|
|
530
527
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
531
528
|
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
532
529
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
@@ -686,7 +683,23 @@ class DuplicateRequest extends Error {
|
|
|
686
683
|
this.conversationId = conversationId;
|
|
687
684
|
}
|
|
688
685
|
}
|
|
689
|
-
|
|
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) {
|
|
690
703
|
const index = await harness.snapshot(IndexDoc, context);
|
|
691
704
|
const prior = index?.conversations.find((entry) => entry.requestId === input.requestId);
|
|
692
705
|
if (prior !== undefined) {
|
|
@@ -696,12 +709,24 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
696
709
|
}
|
|
697
710
|
return prior.id;
|
|
698
711
|
}
|
|
699
|
-
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;
|
|
700
715
|
const model = modelRef === undefined ? undefined : models.getModel(modelRef.provider, modelRef.modelId);
|
|
701
716
|
if (input.model !== undefined && model === undefined) {
|
|
702
717
|
throw new Error(`Unknown model: ${input.model.provider}/${input.model.modelId}`);
|
|
703
718
|
}
|
|
704
|
-
|
|
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");
|
|
705
730
|
const home = input.home;
|
|
706
731
|
const projectPath = home.kind === "project" ? await normalizePath(home.path).catch(() => home.path) : undefined;
|
|
707
732
|
const repo = home.kind === "project" && input.checkout !== "project" && projectPath !== undefined ? await gitBase(projectPath) : undefined;
|
|
@@ -712,7 +737,8 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
712
737
|
agent: {
|
|
713
738
|
...projectPath === undefined || repo !== undefined ? {} : { cwd: projectPath },
|
|
714
739
|
...modelRef === undefined ? {} : { model: modelRef },
|
|
715
|
-
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
740
|
+
...thinkingLevel === undefined ? {} : { thinkingLevel },
|
|
741
|
+
...role === undefined || role.prompt === "" ? {} : { instructions: role.prompt }
|
|
716
742
|
},
|
|
717
743
|
init: async (tx, id) => {
|
|
718
744
|
const doc = await tx.doc(IndexDoc);
|
|
@@ -723,7 +749,13 @@ async function createConversation(harness, dataDir, input, defaults, models, con
|
|
|
723
749
|
throw new Error(`Unknown project: ${home.path}`);
|
|
724
750
|
}
|
|
725
751
|
const stored = home.kind === "chat" ? { kind: "chat" } : { kind: "project", path: projectPath };
|
|
726
|
-
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
|
+
});
|
|
727
759
|
if (repo !== undefined && suffix !== undefined) {
|
|
728
760
|
const path = worktreePath(dataDir, id);
|
|
729
761
|
if (doc.checkouts === undefined)
|
|
@@ -1465,11 +1497,11 @@ import lockfile from "proper-lockfile";
|
|
|
1465
1497
|
// apps/host/src/gateway.ts
|
|
1466
1498
|
import { createServer } from "node:http";
|
|
1467
1499
|
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1468
|
-
import { join as
|
|
1500
|
+
import { join as join7, sep as sep4 } from "node:path";
|
|
1469
1501
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1470
1502
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1471
1503
|
import {
|
|
1472
|
-
AgentDoc as
|
|
1504
|
+
AgentDoc as AgentDoc4,
|
|
1473
1505
|
LiveDoc
|
|
1474
1506
|
} from "@earendil-works/pi-durable";
|
|
1475
1507
|
import { WebSocket, WebSocketServer } from "ws";
|
|
@@ -1516,6 +1548,273 @@ async function listFolders(path, home) {
|
|
|
1516
1548
|
return { path: dir, home: await realpath3(home).catch(() => home), folders };
|
|
1517
1549
|
}
|
|
1518
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
|
+
|
|
1519
1818
|
// packages/protocol/src/images.ts
|
|
1520
1819
|
var IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
1521
1820
|
var MAX_IMAGES_PER_MESSAGE = 4;
|
|
@@ -1660,6 +1959,7 @@ ${answer}`}`,
|
|
|
1660
1959
|
}
|
|
1661
1960
|
|
|
1662
1961
|
// apps/host/src/push.ts
|
|
1962
|
+
import { createHash } from "node:crypto";
|
|
1663
1963
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
1664
1964
|
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
1665
1965
|
|
|
@@ -1721,26 +2021,38 @@ function diffRunNotices(previous, organized) {
|
|
|
1721
2021
|
var context2 = BACKGROUND_CONTEXT2;
|
|
1722
2022
|
var PushDoc = defineDoc6({
|
|
1723
2023
|
kind: "pinomad.push",
|
|
1724
|
-
version:
|
|
2024
|
+
version: 2,
|
|
1725
2025
|
scope: "session",
|
|
1726
|
-
initial: () => ({ registrations: [] })
|
|
2026
|
+
initial: () => ({ registrations: [] }),
|
|
2027
|
+
migrate: () => ({ registrations: [] })
|
|
1727
2028
|
});
|
|
1728
|
-
var
|
|
1729
|
-
var
|
|
2029
|
+
var APNS_TOKEN_PATTERN = /^(?:[0-9a-f]{2})+$/;
|
|
2030
|
+
var APNS_TOKEN_MAX_CHARS = 256;
|
|
2031
|
+
var isApnsEnvironment = (value) => value === "production" || value === "development";
|
|
1730
2032
|
function pushTokenProblem(token) {
|
|
1731
2033
|
if (token === null)
|
|
1732
2034
|
return;
|
|
1733
|
-
if (token
|
|
1734
|
-
return
|
|
2035
|
+
if (typeof token === "string") {
|
|
2036
|
+
return "Push notifications no longer go through Expo — register the device's APNs token instead";
|
|
1735
2037
|
}
|
|
1736
|
-
if (!
|
|
1737
|
-
return
|
|
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)";
|
|
1738
2047
|
return;
|
|
1739
2048
|
}
|
|
1740
|
-
async function setPushToken(harness, publicKey, token, context3) {
|
|
2049
|
+
async function setPushToken(harness, publicKey, token, context3, joined) {
|
|
1741
2050
|
const problem = pushTokenProblem(token);
|
|
1742
2051
|
if (problem !== undefined)
|
|
1743
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
|
+
}
|
|
1744
2056
|
await harness.commit(async (tx) => {
|
|
1745
2057
|
const devices = await tx.doc(DevicesDoc);
|
|
1746
2058
|
if (!devices.devices.some((entry) => entry.publicKey === publicKey)) {
|
|
@@ -1748,8 +2060,14 @@ async function setPushToken(harness, publicKey, token, context3) {
|
|
|
1748
2060
|
}
|
|
1749
2061
|
const push = await tx.doc(PushDoc);
|
|
1750
2062
|
push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
|
|
1751
|
-
if (token !== null)
|
|
1752
|
-
push.registrations.push({
|
|
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
|
+
}
|
|
1753
2071
|
}, context3);
|
|
1754
2072
|
}
|
|
1755
2073
|
async function revokeDevice(harness, publicKey, context3) {
|
|
@@ -1763,38 +2081,43 @@ async function revokeDevice(harness, publicKey, context3) {
|
|
|
1763
2081
|
revocations.revoked.push(publicKey);
|
|
1764
2082
|
}, context3);
|
|
1765
2083
|
}
|
|
1766
|
-
async function
|
|
2084
|
+
async function dropStaleRegistration(harness, token, environment, unregisteredAt, context3) {
|
|
1767
2085
|
await harness.commit(async (tx) => {
|
|
1768
2086
|
const push = await tx.doc(PushDoc);
|
|
1769
|
-
push.registrations = push.registrations.filter((entry) => entry.token !== token);
|
|
2087
|
+
push.registrations = push.registrations.filter((entry) => entry.token !== token || entry.environment !== environment || entry.updatedAt >= unregisteredAt);
|
|
1770
2088
|
}, context3);
|
|
1771
2089
|
}
|
|
1772
|
-
var EXPO_SEND_URL = "https://exp.host/--/api/v2/push/send";
|
|
1773
|
-
var EXPO_RECEIPTS_URL = "https://exp.host/--/api/v2/push/getReceipts";
|
|
1774
2090
|
var FALLBACK_TITLE = "PiNomad conversation";
|
|
1775
2091
|
var TITLE_WAIT_MS = 15000;
|
|
1776
|
-
var
|
|
1777
|
-
var REQUEST_TIMEOUT_MS = 1e4;
|
|
2092
|
+
var TITLE_MAX_CHARS = 160;
|
|
1778
2093
|
var MAX_ATTEMPTS = 4;
|
|
1779
2094
|
var RETRY_BASE_MS = 5000;
|
|
1780
|
-
var
|
|
2095
|
+
var MAX_INFLIGHT = 8;
|
|
1781
2096
|
var MAX_PENDING = 128;
|
|
2097
|
+
var UNAVAILABLE_TRANSPORT = {
|
|
2098
|
+
send: () => Promise.resolve({ result: "unavailable", reason: "no server connection" })
|
|
2099
|
+
};
|
|
1782
2100
|
var realTimers = {
|
|
1783
2101
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
1784
2102
|
clearTimeout: (handle) => clearTimeout(handle)
|
|
1785
2103
|
};
|
|
1786
2104
|
var tokenHint = (token) => `…${token.slice(-6)}`;
|
|
2105
|
+
var collapseId = (hostKey, id) => createHash("sha256").update(`${hostKey}:${id}`).digest("hex").slice(0, 32);
|
|
1787
2106
|
async function attachPushSender(options) {
|
|
1788
2107
|
const { harness, conversations } = options;
|
|
1789
|
-
const
|
|
2108
|
+
const transport = options.transport ?? UNAVAILABLE_TRANSPORT;
|
|
1790
2109
|
const timers = options.timers ?? realTimers;
|
|
2110
|
+
const maxPending = options.limits?.maxPending ?? MAX_PENDING;
|
|
2111
|
+
const maxInflight = options.limits?.maxInflight ?? MAX_INFLIGHT;
|
|
1791
2112
|
const indexWatch = await harness.watchDoc(IndexDoc, context2);
|
|
1792
2113
|
let index = indexWatch?.value ?? undefined;
|
|
1793
2114
|
let baseline;
|
|
1794
2115
|
let disposed = false;
|
|
1795
2116
|
const pending = new Set;
|
|
1796
2117
|
const titleWaiters = new Map;
|
|
1797
|
-
|
|
2118
|
+
let inflight = 0;
|
|
2119
|
+
const readyQueue = [];
|
|
2120
|
+
let unavailableLogged = false;
|
|
1798
2121
|
const generation = new Map;
|
|
1799
2122
|
const update = () => {
|
|
1800
2123
|
if (index === undefined)
|
|
@@ -1816,32 +2139,9 @@ async function attachPushSender(options) {
|
|
|
1816
2139
|
const fresh = (item) => baseline?.has(item.notice.id) === true && generation.get(item.notice.id) === item.generation;
|
|
1817
2140
|
const registrations = async () => {
|
|
1818
2141
|
const doc = await harness.snapshot(PushDoc, context2);
|
|
1819
|
-
return
|
|
1820
|
-
};
|
|
1821
|
-
const post = async (url, payload) => {
|
|
1822
|
-
const response = await fetcher(url, {
|
|
1823
|
-
method: "POST",
|
|
1824
|
-
headers: { accept: "application/json", "content-type": "application/json" },
|
|
1825
|
-
body: JSON.stringify(payload),
|
|
1826
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1827
|
-
});
|
|
1828
|
-
return { status: response.status, body: await response.json() };
|
|
1829
|
-
};
|
|
1830
|
-
const ticketOf = (body) => {
|
|
1831
|
-
const data = body?.data;
|
|
1832
|
-
const ticket = Array.isArray(data) ? data[0] : data;
|
|
1833
|
-
return ticket !== null && typeof ticket === "object" ? ticket : undefined;
|
|
2142
|
+
return doc?.registrations ?? [];
|
|
1834
2143
|
};
|
|
1835
|
-
const
|
|
1836
|
-
to: item.token,
|
|
1837
|
-
title,
|
|
1838
|
-
body: item.notice.body,
|
|
1839
|
-
sound: "default",
|
|
1840
|
-
ttl: TTL_SECONDS,
|
|
1841
|
-
data: { hostKey: options.hostKey, conversationId: item.notice.id },
|
|
1842
|
-
threadId: `${options.hostKey}:${item.notice.id}`,
|
|
1843
|
-
collapseId: `${options.hostKey}:${item.notice.id}`
|
|
1844
|
-
});
|
|
2144
|
+
const findRegistration = (entries, item) => entries.find((entry) => entry.token === item.token && entry.environment === item.environment);
|
|
1845
2145
|
const namedTitle = async (id) => {
|
|
1846
2146
|
const doc = await harness.snapshot(TitleDoc, id, context2);
|
|
1847
2147
|
return typeof doc?.title === "string" && doc.title !== "" ? doc.title : undefined;
|
|
@@ -1852,7 +2152,7 @@ async function attachPushSender(options) {
|
|
|
1852
2152
|
return named;
|
|
1853
2153
|
if (notice.body !== "Finished" || disposed)
|
|
1854
2154
|
return FALLBACK_TITLE;
|
|
1855
|
-
return await new Promise((
|
|
2155
|
+
return await new Promise((resolve4) => {
|
|
1856
2156
|
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
1857
2157
|
titleWaiters.set(notice.id, waiters);
|
|
1858
2158
|
const settle = (title) => {
|
|
@@ -1860,7 +2160,7 @@ async function attachPushSender(options) {
|
|
|
1860
2160
|
if (waiters.size === 0)
|
|
1861
2161
|
titleWaiters.delete(notice.id);
|
|
1862
2162
|
timers.clearTimeout(timer);
|
|
1863
|
-
|
|
2163
|
+
resolve4(title);
|
|
1864
2164
|
};
|
|
1865
2165
|
const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
1866
2166
|
waiters.add(settle);
|
|
@@ -1874,78 +2174,96 @@ async function attachPushSender(options) {
|
|
|
1874
2174
|
pending.delete(item);
|
|
1875
2175
|
if (item.timer !== undefined)
|
|
1876
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);
|
|
1877
2199
|
};
|
|
1878
2200
|
const attempt = async (item) => {
|
|
1879
2201
|
if (disposed || !pending.has(item))
|
|
1880
2202
|
return;
|
|
1881
2203
|
try {
|
|
1882
|
-
if (
|
|
2204
|
+
if (findRegistration(await registrations(), item) === undefined || !fresh(item))
|
|
1883
2205
|
return drop(item);
|
|
1884
2206
|
if (item.title === undefined) {
|
|
1885
2207
|
item.title = await titleFor(item.notice);
|
|
1886
|
-
if (disposed || !pending.has(item) ||
|
|
2208
|
+
if (disposed || !pending.has(item) || findRegistration(await registrations(), item) === undefined || !fresh(item)) {
|
|
1887
2209
|
return drop(item);
|
|
2210
|
+
}
|
|
1888
2211
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
await dropTokenIfCurrent(harness, item.token, context2);
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
if (status >= 200 && status < 300 && ticket?.status === "ok") {
|
|
1898
|
-
drop(item);
|
|
1899
|
-
if (typeof ticket.id === "string")
|
|
1900
|
-
scheduleReceiptCheck(item.token, ticket.id);
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
if ((status === 429 || status >= 500) && item.attempts + 1 < MAX_ATTEMPTS) {
|
|
1904
|
-
item.attempts += 1;
|
|
1905
|
-
item.timer = timers.setTimeout(() => void attempt(item), RETRY_BASE_MS * 2 ** (item.attempts - 1));
|
|
2212
|
+
if (inflight >= maxInflight) {
|
|
2213
|
+
if (!item.queued) {
|
|
2214
|
+
item.queued = true;
|
|
2215
|
+
readyQueue.push(item);
|
|
2216
|
+
}
|
|
1906
2217
|
return;
|
|
1907
2218
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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();
|
|
1915
2238
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
if (receipt.status === "error" && receipt.details?.error === "DeviceNotRegistered") {
|
|
1941
|
-
console.error(`push: token ${tokenHint(token)} is not registered (receipt)`);
|
|
1942
|
-
await dropTokenIfCurrent(harness, token, context2);
|
|
1943
|
-
return;
|
|
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;
|
|
1944
2263
|
}
|
|
1945
|
-
if (receipt.status === "error")
|
|
1946
|
-
console.error(`push: receipt ${ticketId} reports ${receipt.details?.error ?? "error"}`);
|
|
1947
2264
|
} catch (error) {
|
|
1948
|
-
console.error(`push:
|
|
2265
|
+
console.error(`push: send to ${tokenHint(item.token)} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2266
|
+
scheduleRetry(item);
|
|
1949
2267
|
}
|
|
1950
2268
|
};
|
|
1951
2269
|
const enqueue = async (notice) => {
|
|
@@ -1953,15 +2271,20 @@ async function attachPushSender(options) {
|
|
|
1953
2271
|
return;
|
|
1954
2272
|
const itemGeneration = generation.get(notice.id) ?? 0;
|
|
1955
2273
|
try {
|
|
1956
|
-
const
|
|
1957
|
-
|
|
2274
|
+
const entries = await registrations();
|
|
2275
|
+
const seen = new Set;
|
|
2276
|
+
for (const entry of entries) {
|
|
1958
2277
|
if (disposed)
|
|
1959
2278
|
return;
|
|
1960
|
-
|
|
2279
|
+
const identity = `${entry.environment}:${entry.token}`;
|
|
2280
|
+
if (seen.has(identity))
|
|
2281
|
+
continue;
|
|
2282
|
+
seen.add(identity);
|
|
2283
|
+
if (pending.size >= maxPending) {
|
|
1961
2284
|
const [oldest] = pending;
|
|
1962
2285
|
drop(oldest);
|
|
1963
2286
|
}
|
|
1964
|
-
const item = { token, notice, generation: itemGeneration, attempts: 0 };
|
|
2287
|
+
const item = { token: entry.token, environment: entry.environment, notice, generation: itemGeneration, attempts: 0, queued: false };
|
|
1965
2288
|
pending.add(item);
|
|
1966
2289
|
attempt(item);
|
|
1967
2290
|
}
|
|
@@ -1991,18 +2314,19 @@ async function attachPushSender(options) {
|
|
|
1991
2314
|
update();
|
|
1992
2315
|
});
|
|
1993
2316
|
update();
|
|
1994
|
-
return
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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
|
+
}
|
|
2006
2330
|
};
|
|
2007
2331
|
}
|
|
2008
2332
|
|
|
@@ -2049,18 +2373,12 @@ function serializedFork(harness, work) {
|
|
|
2049
2373
|
var LOOPBACK_SECURE_PATH = "/secure";
|
|
2050
2374
|
async function startGateway(options) {
|
|
2051
2375
|
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
2376
|
+
const fileLister = createCandidateLister(enumerateRoot);
|
|
2052
2377
|
const detachAutoTitle = options.autoTitle === false ? undefined : attachAutoTitle(conversations, {
|
|
2053
2378
|
harness: options.harness,
|
|
2054
2379
|
models: options.models,
|
|
2055
2380
|
settings: options.settings
|
|
2056
2381
|
});
|
|
2057
|
-
const detachPush = options.push === undefined ? undefined : await attachPushSender({
|
|
2058
|
-
harness: options.harness,
|
|
2059
|
-
conversations,
|
|
2060
|
-
hostKey: options.push.hostKey,
|
|
2061
|
-
...options.push.fetch === undefined ? {} : { fetch: options.push.fetch },
|
|
2062
|
-
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
2063
|
-
});
|
|
2064
2382
|
const http = createServer((request, response) => {
|
|
2065
2383
|
serveWebClient(request, response, options.webRoot).catch(() => {
|
|
2066
2384
|
if (!response.headersSent)
|
|
@@ -2069,8 +2387,8 @@ async function startGateway(options) {
|
|
|
2069
2387
|
});
|
|
2070
2388
|
});
|
|
2071
2389
|
const server = new WebSocketServer({ server: http });
|
|
2072
|
-
await new Promise((
|
|
2073
|
-
http.once("listening",
|
|
2390
|
+
await new Promise((resolve4, reject) => {
|
|
2391
|
+
http.once("listening", resolve4);
|
|
2074
2392
|
http.once("error", reject);
|
|
2075
2393
|
http.listen(options.port, "127.0.0.1");
|
|
2076
2394
|
});
|
|
@@ -2087,7 +2405,7 @@ async function startGateway(options) {
|
|
|
2087
2405
|
if (joined?.source === "account") {
|
|
2088
2406
|
broadcast("info", `New device "${joined.name}" joined through your account`);
|
|
2089
2407
|
}
|
|
2090
|
-
const client = new GatewayClient(connection, options, conversations, {
|
|
2408
|
+
const client = new GatewayClient(connection, options, conversations, fileLister, {
|
|
2091
2409
|
devicePublicKey,
|
|
2092
2410
|
closed: (self) => {
|
|
2093
2411
|
clients.delete(self);
|
|
@@ -2177,7 +2495,8 @@ async function startGateway(options) {
|
|
|
2177
2495
|
},
|
|
2178
2496
|
onStateChange: (state) => {
|
|
2179
2497
|
if (state.status === "registered") {
|
|
2180
|
-
|
|
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 }));
|
|
2181
2500
|
} else if (state.status === "connecting") {
|
|
2182
2501
|
console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
|
|
2183
2502
|
} else {
|
|
@@ -2190,6 +2509,13 @@ async function startGateway(options) {
|
|
|
2190
2509
|
});
|
|
2191
2510
|
relay = link;
|
|
2192
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
|
+
});
|
|
2193
2519
|
return {
|
|
2194
2520
|
url: `ws://127.0.0.1:${port}`,
|
|
2195
2521
|
...remote === undefined ? {} : { remote: { url: remote.url, advertiseUrl: remote.advertiseUrl } },
|
|
@@ -2199,14 +2525,14 @@ async function startGateway(options) {
|
|
|
2199
2525
|
await relay?.close();
|
|
2200
2526
|
detachAutoTitle?.();
|
|
2201
2527
|
detachSettingsWatch();
|
|
2202
|
-
await detachPush?.();
|
|
2528
|
+
await detachPush?.close();
|
|
2203
2529
|
conversations.dispose();
|
|
2204
2530
|
await Promise.all([...clients].map((client) => client.dispose()));
|
|
2205
2531
|
for (const socket of server.clients)
|
|
2206
2532
|
socket.terminate();
|
|
2207
2533
|
await remote?.close();
|
|
2208
|
-
await new Promise((
|
|
2209
|
-
await new Promise((
|
|
2534
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
2535
|
+
await new Promise((resolve4) => http.close(() => resolve4()));
|
|
2210
2536
|
}
|
|
2211
2537
|
};
|
|
2212
2538
|
}
|
|
@@ -2215,7 +2541,7 @@ function tokenConnection(socket) {
|
|
|
2215
2541
|
get open() {
|
|
2216
2542
|
return socket.readyState === WebSocket.OPEN;
|
|
2217
2543
|
},
|
|
2218
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
2544
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve4) => socket.send(text, () => resolve4())) : Promise.resolve(),
|
|
2219
2545
|
close: (code, reason) => socket.close(code, reason),
|
|
2220
2546
|
handle(handlers) {
|
|
2221
2547
|
socket.on("message", (data) => handlers.message(String(data)));
|
|
@@ -2228,9 +2554,9 @@ function secureConnection(socket, session) {
|
|
|
2228
2554
|
get open() {
|
|
2229
2555
|
return socket.readyState === WebSocket.OPEN;
|
|
2230
2556
|
},
|
|
2231
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
2557
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve4) => {
|
|
2232
2558
|
const chunks = session.seal(text);
|
|
2233
|
-
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));
|
|
2234
2560
|
}) : Promise.resolve(),
|
|
2235
2561
|
close: (code, reason) => socket.close(code, reason),
|
|
2236
2562
|
handle(handlers) {
|
|
@@ -2349,8 +2675,8 @@ async function startRemote(options, admission, register) {
|
|
|
2349
2675
|
async close() {
|
|
2350
2676
|
for (const socket of secure.clients)
|
|
2351
2677
|
socket.terminate();
|
|
2352
|
-
await new Promise((
|
|
2353
|
-
await new Promise((
|
|
2678
|
+
await new Promise((resolve4) => secure.close(() => resolve4()));
|
|
2679
|
+
await new Promise((resolve4) => http.close(() => resolve4()));
|
|
2354
2680
|
}
|
|
2355
2681
|
};
|
|
2356
2682
|
}
|
|
@@ -2361,6 +2687,7 @@ class ConversationList {
|
|
|
2361
2687
|
#questionKinds;
|
|
2362
2688
|
#records = new Map;
|
|
2363
2689
|
#facts = new Map;
|
|
2690
|
+
#roles = new Map;
|
|
2364
2691
|
#index = new Map;
|
|
2365
2692
|
#runEnd = new Set;
|
|
2366
2693
|
#list = [];
|
|
@@ -2374,19 +2701,26 @@ class ConversationList {
|
|
|
2374
2701
|
const list = new ConversationList(docs);
|
|
2375
2702
|
const summaries = [];
|
|
2376
2703
|
let cursor;
|
|
2704
|
+
const records = [];
|
|
2377
2705
|
do {
|
|
2378
2706
|
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context3);
|
|
2379
2707
|
for (const record of page.items) {
|
|
2380
2708
|
list.#records.set(record.id, record);
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
list.#index.set(record.id, summaries.length);
|
|
2384
|
-
summaries.push(summaryOf(record, facts));
|
|
2709
|
+
list.#facts.set(record.id, await list.#load(harness, record));
|
|
2710
|
+
records.push(record);
|
|
2385
2711
|
}
|
|
2386
2712
|
cursor = page.next;
|
|
2387
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
|
+
}
|
|
2388
2719
|
list.#list = summaries;
|
|
2389
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);
|
|
2390
2724
|
return list;
|
|
2391
2725
|
}
|
|
2392
2726
|
async#load(harness, record) {
|
|
@@ -2440,8 +2774,11 @@ class ConversationList {
|
|
|
2440
2774
|
facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
|
|
2441
2775
|
} else if (change.type === "document") {
|
|
2442
2776
|
const id = change.conversationId;
|
|
2443
|
-
if (id === undefined)
|
|
2777
|
+
if (id === undefined) {
|
|
2778
|
+
if (change.record.kind === IndexDoc.definition.kind)
|
|
2779
|
+
this.#reindex(change.value, touched);
|
|
2444
2780
|
continue;
|
|
2781
|
+
}
|
|
2445
2782
|
const facts = this.#fact(id);
|
|
2446
2783
|
if (change.record.kind === "pi.live") {
|
|
2447
2784
|
const live = change.value;
|
|
@@ -2454,12 +2791,12 @@ class ConversationList {
|
|
|
2454
2791
|
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
2455
2792
|
const value = change.value;
|
|
2456
2793
|
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
2457
|
-
const
|
|
2794
|
+
const next = new Set(facts.pending);
|
|
2458
2795
|
if (pending)
|
|
2459
|
-
|
|
2796
|
+
next.add(change.record.kind);
|
|
2460
2797
|
else
|
|
2461
|
-
|
|
2462
|
-
facts.pending =
|
|
2798
|
+
next.delete(change.record.kind);
|
|
2799
|
+
facts.pending = next;
|
|
2463
2800
|
touched.add(id);
|
|
2464
2801
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
2465
2802
|
const value = change.value;
|
|
@@ -2473,12 +2810,35 @@ class ConversationList {
|
|
|
2473
2810
|
}
|
|
2474
2811
|
}
|
|
2475
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) {
|
|
2476
2836
|
let next;
|
|
2477
2837
|
for (const id of touched) {
|
|
2478
2838
|
const record = this.#records.get(id);
|
|
2479
2839
|
if (record === undefined)
|
|
2480
2840
|
continue;
|
|
2481
|
-
const summary = summaryOf(record
|
|
2841
|
+
const summary = this.#summaryOf(record);
|
|
2482
2842
|
const index = this.#index.get(id);
|
|
2483
2843
|
if (index === undefined) {
|
|
2484
2844
|
next ??= this.#list.slice();
|
|
@@ -2491,13 +2851,6 @@ class ConversationList {
|
|
|
2491
2851
|
}
|
|
2492
2852
|
if (next !== undefined)
|
|
2493
2853
|
this.#set(next);
|
|
2494
|
-
for (const id of ended)
|
|
2495
|
-
for (const listener of this.#runEnd)
|
|
2496
|
-
listener(id);
|
|
2497
|
-
}
|
|
2498
|
-
onRunEnd(listener) {
|
|
2499
|
-
this.#runEnd.add(listener);
|
|
2500
|
-
return () => this.#runEnd.delete(listener);
|
|
2501
2854
|
}
|
|
2502
2855
|
#fact(id) {
|
|
2503
2856
|
let facts = this.#facts.get(id);
|
|
@@ -2528,7 +2881,26 @@ class ConversationList {
|
|
|
2528
2881
|
this.#listeners.clear();
|
|
2529
2882
|
}
|
|
2530
2883
|
}
|
|
2531
|
-
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) {
|
|
2532
2904
|
const parent = record.parent?.conversationId ?? record.owner?.conversationId;
|
|
2533
2905
|
const status = facts.pending.size > 0 ? "needs-answer" : facts.running === true ? "running" : facts.failed === true ? "failed" : undefined;
|
|
2534
2906
|
const runningSlot = facts.running === true ? facts.tools?.find((slot) => slot.status === "running") : undefined;
|
|
@@ -2545,7 +2917,7 @@ function summaryOf(record, facts) {
|
|
|
2545
2917
|
...status === undefined ? {} : { status },
|
|
2546
2918
|
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2547
2919
|
...facts.label === undefined ? {} : { label: facts.label },
|
|
2548
|
-
...
|
|
2920
|
+
...role === undefined ? {} : { role },
|
|
2549
2921
|
...activity === undefined ? {} : { activity }
|
|
2550
2922
|
};
|
|
2551
2923
|
}
|
|
@@ -2601,12 +2973,14 @@ class GatewayClient {
|
|
|
2601
2973
|
#connection;
|
|
2602
2974
|
#hooks;
|
|
2603
2975
|
#options;
|
|
2976
|
+
#files;
|
|
2604
2977
|
#hostValues;
|
|
2605
2978
|
#subscriptions = new Map;
|
|
2606
2979
|
#disposed = false;
|
|
2607
|
-
constructor(connection, options, conversations, hooks) {
|
|
2980
|
+
constructor(connection, options, conversations, files, hooks) {
|
|
2608
2981
|
this.#connection = connection;
|
|
2609
2982
|
this.#options = options;
|
|
2983
|
+
this.#files = files;
|
|
2610
2984
|
this.#hostValues = {
|
|
2611
2985
|
conversations,
|
|
2612
2986
|
mcp: options.mcp ?? constantValue(null),
|
|
@@ -2741,8 +3115,8 @@ class GatewayClient {
|
|
|
2741
3115
|
await this.#conversation(id);
|
|
2742
3116
|
const { harness } = this.#options;
|
|
2743
3117
|
let signalCreation;
|
|
2744
|
-
const created = new Promise((
|
|
2745
|
-
signalCreation =
|
|
3118
|
+
const created = new Promise((resolve4) => {
|
|
3119
|
+
signalCreation = resolve4;
|
|
2746
3120
|
});
|
|
2747
3121
|
let inner;
|
|
2748
3122
|
let stopped = false;
|
|
@@ -2833,8 +3207,9 @@ class GatewayClient {
|
|
|
2833
3207
|
...create.images === undefined ? {} : { images: create.images },
|
|
2834
3208
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
2835
3209
|
...create.model === undefined ? {} : { model: create.model },
|
|
2836
|
-
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel }
|
|
2837
|
-
|
|
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);
|
|
2838
3213
|
return { conversationId };
|
|
2839
3214
|
}
|
|
2840
3215
|
case "archive": {
|
|
@@ -2864,7 +3239,10 @@ class GatewayClient {
|
|
|
2864
3239
|
const device = this.devicePublicKey;
|
|
2865
3240
|
if (device === undefined)
|
|
2866
3241
|
throw new Error("Push tokens register over the secure channel only");
|
|
2867
|
-
await
|
|
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);
|
|
2868
3246
|
return null;
|
|
2869
3247
|
}
|
|
2870
3248
|
case "logout": {
|
|
@@ -2901,6 +3279,10 @@ class GatewayClient {
|
|
|
2901
3279
|
}
|
|
2902
3280
|
case "listFolders":
|
|
2903
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
|
+
}
|
|
2904
3286
|
}
|
|
2905
3287
|
const conversation = await this.#conversation(args.conversationId);
|
|
2906
3288
|
switch (method) {
|
|
@@ -3061,7 +3443,7 @@ class GatewayClient {
|
|
|
3061
3443
|
}, context4);
|
|
3062
3444
|
}
|
|
3063
3445
|
get #chatsPrefix() {
|
|
3064
|
-
return
|
|
3446
|
+
return join7(this.#options.dataDir, "chats") + sep4;
|
|
3065
3447
|
}
|
|
3066
3448
|
async#changes(conversation, context4) {
|
|
3067
3449
|
const cwd = (await conversation.agent(context4)).cwd;
|
|
@@ -3082,7 +3464,7 @@ class GatewayClient {
|
|
|
3082
3464
|
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
3083
3465
|
}
|
|
3084
3466
|
async#agent(id) {
|
|
3085
|
-
return await this.#options.harness.snapshot(
|
|
3467
|
+
return await this.#options.harness.snapshot(AgentDoc4, id, context3) ?? {};
|
|
3086
3468
|
}
|
|
3087
3469
|
async dispose() {
|
|
3088
3470
|
if (this.#disposed)
|
|
@@ -3098,7 +3480,7 @@ class GatewayClient {
|
|
|
3098
3480
|
// apps/host/src/host.ts
|
|
3099
3481
|
var context4 = BACKGROUND_CONTEXT4;
|
|
3100
3482
|
async function hostToken(dataDir) {
|
|
3101
|
-
const path =
|
|
3483
|
+
const path = join8(dataDir, "token");
|
|
3102
3484
|
try {
|
|
3103
3485
|
return (await readFile4(path, "utf8")).trim();
|
|
3104
3486
|
} catch {
|
|
@@ -3127,7 +3509,7 @@ async function openHost(options) {
|
|
|
3127
3509
|
const reports = [];
|
|
3128
3510
|
let report = (error) => void reports.push(error);
|
|
3129
3511
|
const registry = createRegistry();
|
|
3130
|
-
agentRoles = await openAgentRoles({ dir:
|
|
3512
|
+
agentRoles = await openAgentRoles({ dir: join8(options.dataDir, "agents") });
|
|
3131
3513
|
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
3132
3514
|
harness: () => {
|
|
3133
3515
|
if (harness === undefined)
|
|
@@ -3150,13 +3532,13 @@ async function openHost(options) {
|
|
|
3150
3532
|
}
|
|
3151
3533
|
});
|
|
3152
3534
|
const hostSettings = await openHostSettings({
|
|
3153
|
-
path:
|
|
3535
|
+
path: join8(options.dataDir, "settings.json"),
|
|
3154
3536
|
fallback: options.settingsFallback,
|
|
3155
3537
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
3156
3538
|
});
|
|
3157
3539
|
const environments = new Map;
|
|
3158
|
-
const chatsDir =
|
|
3159
|
-
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")), {
|
|
3160
3542
|
models: options.models,
|
|
3161
3543
|
registry,
|
|
3162
3544
|
env: async (target) => {
|
|
@@ -3164,7 +3546,7 @@ async function openHost(options) {
|
|
|
3164
3546
|
if (cwd === undefined)
|
|
3165
3547
|
return;
|
|
3166
3548
|
const worktrees = worktreeRoot(options.dataDir);
|
|
3167
|
-
if (cwd === worktrees || cwd.startsWith(worktrees +
|
|
3549
|
+
if (cwd === worktrees || cwd.startsWith(worktrees + sep5)) {
|
|
3168
3550
|
const index = await target.read.snapshot(IndexDoc, context4);
|
|
3169
3551
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
3170
3552
|
if (record !== undefined) {
|
|
@@ -3213,7 +3595,7 @@ async function openHost(options) {
|
|
|
3213
3595
|
...options.push === undefined ? {} : {
|
|
3214
3596
|
push: {
|
|
3215
3597
|
hostKey: toBase64Url(hostKey.publicKey),
|
|
3216
|
-
...options.push.
|
|
3598
|
+
...options.push.transport === undefined ? {} : { transport: options.push.transport },
|
|
3217
3599
|
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
3218
3600
|
}
|
|
3219
3601
|
},
|
|
@@ -3489,7 +3871,7 @@ async function main(argv) {
|
|
|
3489
3871
|
relay: { type: "string" }
|
|
3490
3872
|
}
|
|
3491
3873
|
});
|
|
3492
|
-
const dataDir =
|
|
3874
|
+
const dataDir = resolve4(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
3493
3875
|
let relayOrigin;
|
|
3494
3876
|
try {
|
|
3495
3877
|
relayOrigin = resolveRelayOrigin(values.relay, await readServerConfig(dataDir));
|
|
@@ -3499,7 +3881,7 @@ async function main(argv) {
|
|
|
3499
3881
|
}
|
|
3500
3882
|
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
3501
3883
|
createContext({
|
|
3502
|
-
agentsHome:
|
|
3884
|
+
agentsHome: join9(homedir3(), ".agents"),
|
|
3503
3885
|
checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
|
|
3504
3886
|
}),
|
|
3505
3887
|
createCoding(options2),
|
|
@@ -3517,7 +3899,7 @@ async function main(argv) {
|
|
|
3517
3899
|
dataDir,
|
|
3518
3900
|
projects: values.project ?? [],
|
|
3519
3901
|
port: Number(values.port),
|
|
3520
|
-
mcpConfig:
|
|
3902
|
+
mcpConfig: join9(homedir3(), ".agents", "mcp.json"),
|
|
3521
3903
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
3522
3904
|
webRoot,
|
|
3523
3905
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -3561,7 +3943,7 @@ async function main(argv) {
|
|
|
3561
3943
|
}
|
|
3562
3944
|
async function piOptions() {
|
|
3563
3945
|
const modelRuntime = await ModelRuntime.create();
|
|
3564
|
-
const modelAuth = await watchModelAuth(modelRuntime, { authPath:
|
|
3946
|
+
const modelAuth = await watchModelAuth(modelRuntime, { authPath: join9(getAgentDir(), "auth.json") });
|
|
3565
3947
|
const settingsManager = SettingsManager.create(process.cwd());
|
|
3566
3948
|
configureHarnessHttp(settingsManager);
|
|
3567
3949
|
const summaries = () => modelAuth.models.value;
|
|
@@ -3593,7 +3975,7 @@ async function main(argv) {
|
|
|
3593
3975
|
console.log(JSON.stringify({
|
|
3594
3976
|
event: "ready",
|
|
3595
3977
|
url: host.url,
|
|
3596
|
-
tokenFile:
|
|
3978
|
+
tokenFile: join9(dataDir, "token"),
|
|
3597
3979
|
dataDir,
|
|
3598
3980
|
model,
|
|
3599
3981
|
web,
|