pinomad 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ normalizeOrigin,
4
+ readServerBinding,
5
+ readServerConfig,
6
+ resolveRelayOrigin
7
+ } from "./chunk-302y6by9.js";
2
8
  import {
3
9
  serveWebClient
4
10
  } from "./chunk-r6w5aqkw.js";
11
+ import {
12
+ relayWsBase,
13
+ startRelayLink
14
+ } from "./chunk-knaw1a9g.js";
5
15
  import {
6
16
  packagedVersion,
7
17
  webRoot
8
- } from "./chunk-3tf23vjq.js";
18
+ } from "./chunk-fgx2sh7d.js";
9
19
  import {
10
20
  HOST_SETTING_KEYS,
11
21
  INVALID_FRAME_CLOSE_CODE,
@@ -14,31 +24,26 @@ import {
14
24
  UNAUTHORIZED_CLOSE_CODE,
15
25
  isQueueMode,
16
26
  isThinkingLevel,
27
+ organize,
17
28
  parseClientFrame
18
- } from "./chunk-c155p0t0.js";
29
+ } from "./chunk-myjp910s.js";
19
30
  import {
20
31
  DevicesDoc,
32
+ RevocationsDoc,
33
+ admitDevice,
21
34
  ensureDevices,
22
- isRegistered,
23
35
  loadHostKey,
24
36
  loadRelayKey,
25
- pairingOffers,
26
- registerDevice,
27
- revokeDevice
28
- } from "./chunk-fxaewke2.js";
37
+ pairingOffers
38
+ } from "./chunk-837gh8nr.js";
29
39
  import {
30
- RELAY_CLOSE,
31
40
  SECURE_PROLOGUE,
32
41
  SecureSession,
33
42
  decodeClientHello,
34
- decodeRelayToHost,
35
43
  encodeHostHello,
36
- encodeRelayFrame,
37
- relayHostId,
38
44
  respondIK,
39
- signRelayChallenge,
40
45
  toBase64Url
41
- } from "./chunk-z4x88s0q.js";
46
+ } from "./chunk-7gmkgyaz.js";
42
47
  import {
43
48
  DEFAULT_DATA_DIR
44
49
  } from "./chunk-cjfmfyjb.js";
@@ -522,7 +527,7 @@ var todo = {
522
527
  import { randomBytes as randomBytes3 } from "node:crypto";
523
528
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
524
529
  import { basename as basename2, join as join7, sep as sep4 } from "node:path";
525
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
530
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
526
531
  import { createRegistry, Harness } from "@earendil-works/pi-durable";
527
532
  import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
528
533
 
@@ -1366,10 +1371,12 @@ var fileValueError = (key, value, models) => {
1366
1371
  case "steeringMode":
1367
1372
  case "followUpMode":
1368
1373
  return isQueueMode(value) ? undefined : "malformed";
1374
+ case "acceptAccountDevices":
1375
+ return typeof value === "boolean" ? undefined : "malformed";
1369
1376
  }
1370
1377
  };
1371
1378
  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);
1379
+ 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
1380
  var parse = (path, text) => {
1374
1381
  let parsed;
1375
1382
  try {
@@ -1412,6 +1419,7 @@ async function openHostSettings(options) {
1412
1419
  defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
1413
1420
  steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
1414
1421
  followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
1422
+ acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
1415
1423
  custom
1416
1424
  };
1417
1425
  };
@@ -1458,13 +1466,13 @@ import lockfile from "proper-lockfile";
1458
1466
  import { createServer } from "node:http";
1459
1467
  import { homedir as homedir2, networkInterfaces } from "node:os";
1460
1468
  import { join as join6, sep as sep3 } from "node:path";
1461
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
1469
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
1462
1470
  import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
1463
1471
  import {
1464
1472
  AgentDoc as AgentDoc3,
1465
1473
  LiveDoc
1466
1474
  } from "@earendil-works/pi-durable";
1467
- import { WebSocket as WebSocket2, WebSocketServer } from "ws";
1475
+ import { WebSocket, WebSocketServer } from "ws";
1468
1476
  import { Value as Value2 } from "typebox/value";
1469
1477
 
1470
1478
  // apps/host/src/folders.ts
@@ -1651,6 +1659,353 @@ ${answer}`}`,
1651
1659
  });
1652
1660
  }
1653
1661
 
1662
+ // apps/host/src/push.ts
1663
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
1664
+ import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
1665
+
1666
+ // packages/client/src/family.ts
1667
+ var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
1668
+ function displayName(summary) {
1669
+ if (summary === undefined)
1670
+ return "Conversation";
1671
+ if (summary.kind !== "subagent")
1672
+ return summary.title ?? "New conversation";
1673
+ if (summary.label !== undefined)
1674
+ return summary.label;
1675
+ const title = summary.title?.replace(SUBAGENT_TASK_PREFIX, "").trim();
1676
+ return title === undefined || title === "" ? "Subagent" : title;
1677
+ }
1678
+
1679
+ // packages/client/src/run-notifications.ts
1680
+ var everySummary = (organized) => {
1681
+ const summaries = [];
1682
+ const visit = (node) => {
1683
+ summaries.push(node.summary);
1684
+ for (const child of node.children)
1685
+ visit(child);
1686
+ };
1687
+ for (const root of [...organized.chats, ...organized.projects.flatMap((entry) => entry.conversations)])
1688
+ visit(root);
1689
+ return summaries;
1690
+ };
1691
+ function diffRunNotices(previous, organized) {
1692
+ const notices = [];
1693
+ const changed = [];
1694
+ const baseline = new Map;
1695
+ for (const summary of everySummary(organized)) {
1696
+ baseline.set(summary.id, summary.status);
1697
+ if (previous === undefined)
1698
+ continue;
1699
+ const before = previous.get(summary.id);
1700
+ if (before !== summary.status)
1701
+ changed.push(summary.id);
1702
+ if (summary.status === "needs-answer" && before !== "needs-answer") {
1703
+ notices.push({ id: summary.id, title: displayName(summary), body: "Waiting for your answer" });
1704
+ continue;
1705
+ }
1706
+ const runEnded = before === "running" || before === "needs-answer";
1707
+ if (runEnded && (summary.status === undefined || summary.status === "failed") && summary.kind !== "subagent") {
1708
+ notices.push({ id: summary.id, title: displayName(summary), body: summary.status === "failed" ? "Failed" : "Finished" });
1709
+ }
1710
+ }
1711
+ if (previous !== undefined) {
1712
+ for (const gone of previous.keys()) {
1713
+ if (!baseline.has(gone))
1714
+ changed.push(gone);
1715
+ }
1716
+ }
1717
+ return { notices, changed, baseline };
1718
+ }
1719
+
1720
+ // apps/host/src/push.ts
1721
+ var context2 = BACKGROUND_CONTEXT2;
1722
+ var PushDoc = defineDoc6({
1723
+ kind: "pinomad.push",
1724
+ version: 1,
1725
+ scope: "session",
1726
+ initial: () => ({ registrations: [] })
1727
+ });
1728
+ var PUSH_TOKEN_PATTERN = /^Expo(?:nent)?PushToken\[[^\]]+\]$/;
1729
+ var PUSH_TOKEN_MAX_LENGTH = 256;
1730
+ function pushTokenProblem(token) {
1731
+ if (token === null)
1732
+ return;
1733
+ if (token.length > PUSH_TOKEN_MAX_LENGTH) {
1734
+ return `Push token is too long (${PUSH_TOKEN_MAX_LENGTH} characters at most)`;
1735
+ }
1736
+ if (!PUSH_TOKEN_PATTERN.test(token))
1737
+ return "Not an Expo push token";
1738
+ return;
1739
+ }
1740
+ async function setPushToken(harness, publicKey, token, context3) {
1741
+ const problem = pushTokenProblem(token);
1742
+ if (problem !== undefined)
1743
+ throw new Error(problem);
1744
+ await harness.commit(async (tx) => {
1745
+ const devices = await tx.doc(DevicesDoc);
1746
+ if (!devices.devices.some((entry) => entry.publicKey === publicKey)) {
1747
+ throw new Error("Device is not paired");
1748
+ }
1749
+ const push = await tx.doc(PushDoc);
1750
+ push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
1751
+ if (token !== null)
1752
+ push.registrations.push({ publicKey, token, updatedAt: Date.now() });
1753
+ }, context3);
1754
+ }
1755
+ async function revokeDevice(harness, publicKey, context3) {
1756
+ await harness.commit(async (tx) => {
1757
+ const devices = await tx.doc(DevicesDoc);
1758
+ devices.devices = devices.devices.filter((entry) => entry.publicKey !== publicKey);
1759
+ const push = await tx.doc(PushDoc);
1760
+ push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
1761
+ const revocations = await tx.doc(RevocationsDoc);
1762
+ if (!revocations.revoked.includes(publicKey))
1763
+ revocations.revoked.push(publicKey);
1764
+ }, context3);
1765
+ }
1766
+ async function dropTokenIfCurrent(harness, token, context3) {
1767
+ await harness.commit(async (tx) => {
1768
+ const push = await tx.doc(PushDoc);
1769
+ push.registrations = push.registrations.filter((entry) => entry.token !== token);
1770
+ }, context3);
1771
+ }
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
+ var FALLBACK_TITLE = "PiNomad conversation";
1775
+ var TITLE_WAIT_MS = 15000;
1776
+ var TTL_SECONDS = 3600;
1777
+ var REQUEST_TIMEOUT_MS = 1e4;
1778
+ var MAX_ATTEMPTS = 4;
1779
+ var RETRY_BASE_MS = 5000;
1780
+ var RECEIPT_DELAY_MS = 15 * 60 * 1000;
1781
+ var MAX_PENDING = 128;
1782
+ var realTimers = {
1783
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
1784
+ clearTimeout: (handle) => clearTimeout(handle)
1785
+ };
1786
+ var tokenHint = (token) => `…${token.slice(-6)}`;
1787
+ async function attachPushSender(options) {
1788
+ const { harness, conversations } = options;
1789
+ const fetcher = options.fetch ?? fetch;
1790
+ const timers = options.timers ?? realTimers;
1791
+ const indexWatch = await harness.watchDoc(IndexDoc, context2);
1792
+ let index = indexWatch?.value ?? undefined;
1793
+ let baseline;
1794
+ let disposed = false;
1795
+ const pending = new Set;
1796
+ const titleWaiters = new Map;
1797
+ const receiptTimers = new Set;
1798
+ const generation = new Map;
1799
+ const update = () => {
1800
+ if (index === undefined)
1801
+ return;
1802
+ const organized = organize(index, conversations.value);
1803
+ if (baseline === undefined) {
1804
+ baseline = diffRunNotices(undefined, organized).baseline;
1805
+ for (const id of baseline.keys())
1806
+ generation.set(id, 0);
1807
+ return;
1808
+ }
1809
+ const diff = diffRunNotices(baseline, organized);
1810
+ baseline = diff.baseline;
1811
+ for (const id of diff.changed)
1812
+ generation.set(id, (generation.get(id) ?? 0) + 1);
1813
+ for (const notice of diff.notices)
1814
+ enqueue(notice);
1815
+ };
1816
+ const fresh = (item) => baseline?.has(item.notice.id) === true && generation.get(item.notice.id) === item.generation;
1817
+ const registrations = async () => {
1818
+ const doc = await harness.snapshot(PushDoc, context2);
1819
+ return new Set((doc?.registrations ?? []).map((entry) => entry.token));
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;
1834
+ };
1835
+ const message = (item, title) => ({
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
+ });
1845
+ const namedTitle = async (id) => {
1846
+ const doc = await harness.snapshot(TitleDoc, id, context2);
1847
+ return typeof doc?.title === "string" && doc.title !== "" ? doc.title : undefined;
1848
+ };
1849
+ const titleFor = async (notice) => {
1850
+ const named = await namedTitle(notice.id);
1851
+ if (named !== undefined)
1852
+ return named;
1853
+ if (notice.body !== "Finished" || disposed)
1854
+ return FALLBACK_TITLE;
1855
+ return await new Promise((resolve3) => {
1856
+ const waiters = titleWaiters.get(notice.id) ?? new Set;
1857
+ titleWaiters.set(notice.id, waiters);
1858
+ const settle = (title) => {
1859
+ waiters.delete(settle);
1860
+ if (waiters.size === 0)
1861
+ titleWaiters.delete(notice.id);
1862
+ timers.clearTimeout(timer);
1863
+ resolve3(title);
1864
+ };
1865
+ const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
1866
+ waiters.add(settle);
1867
+ namedTitle(notice.id).then((title) => {
1868
+ if (title !== undefined)
1869
+ settle(title);
1870
+ });
1871
+ });
1872
+ };
1873
+ const drop = (item) => {
1874
+ pending.delete(item);
1875
+ if (item.timer !== undefined)
1876
+ timers.clearTimeout(item.timer);
1877
+ };
1878
+ const attempt = async (item) => {
1879
+ if (disposed || !pending.has(item))
1880
+ return;
1881
+ try {
1882
+ if (!(await registrations()).has(item.token) || !fresh(item))
1883
+ return drop(item);
1884
+ if (item.title === undefined) {
1885
+ item.title = await titleFor(item.notice);
1886
+ if (disposed || !pending.has(item) || !(await registrations()).has(item.token) || !fresh(item))
1887
+ return drop(item);
1888
+ }
1889
+ const { status, body } = await post(EXPO_SEND_URL, message(item, item.title));
1890
+ const ticket = ticketOf(body);
1891
+ if (ticket?.details?.error === "DeviceNotRegistered") {
1892
+ drop(item);
1893
+ console.error(`push: token ${tokenHint(item.token)} is not registered (ticket)`);
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));
1906
+ return;
1907
+ }
1908
+ console.error(`push: send to ${tokenHint(item.token)} failed permanently (HTTP ${status})`);
1909
+ drop(item);
1910
+ } catch (error) {
1911
+ if (item.attempts + 1 < MAX_ATTEMPTS) {
1912
+ item.attempts += 1;
1913
+ item.timer = timers.setTimeout(() => void attempt(item), RETRY_BASE_MS * 2 ** (item.attempts - 1));
1914
+ return;
1915
+ }
1916
+ console.error(`push: send to ${tokenHint(item.token)} gave up: ${error instanceof Error ? error.message : String(error)}`);
1917
+ drop(item);
1918
+ }
1919
+ };
1920
+ const scheduleReceiptCheck = (token, ticketId) => {
1921
+ const timer = timers.setTimeout(() => {
1922
+ receiptTimers.delete(timer);
1923
+ checkReceipt(token, ticketId);
1924
+ }, RECEIPT_DELAY_MS);
1925
+ receiptTimers.add(timer);
1926
+ while (receiptTimers.size > MAX_PENDING) {
1927
+ const oldest = receiptTimers.values().next().value;
1928
+ receiptTimers.delete(oldest);
1929
+ timers.clearTimeout(oldest);
1930
+ }
1931
+ };
1932
+ const checkReceipt = async (token, ticketId) => {
1933
+ if (disposed)
1934
+ return;
1935
+ try {
1936
+ const { status, body } = await post(EXPO_RECEIPTS_URL, { ids: [ticketId] });
1937
+ const receipt = status >= 200 && status < 300 ? body?.data?.[ticketId] : undefined;
1938
+ if (receipt === undefined || disposed)
1939
+ return;
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;
1944
+ }
1945
+ if (receipt.status === "error")
1946
+ console.error(`push: receipt ${ticketId} reports ${receipt.details?.error ?? "error"}`);
1947
+ } catch (error) {
1948
+ console.error(`push: receipt check ${ticketId} failed: ${error instanceof Error ? error.message : String(error)}`);
1949
+ }
1950
+ };
1951
+ const enqueue = async (notice) => {
1952
+ if (disposed)
1953
+ return;
1954
+ const itemGeneration = generation.get(notice.id) ?? 0;
1955
+ try {
1956
+ const tokens = await registrations();
1957
+ for (const token of tokens) {
1958
+ if (disposed)
1959
+ return;
1960
+ if (pending.size >= MAX_PENDING) {
1961
+ const [oldest] = pending;
1962
+ drop(oldest);
1963
+ }
1964
+ const item = { token, notice, generation: itemGeneration, attempts: 0 };
1965
+ pending.add(item);
1966
+ attempt(item);
1967
+ }
1968
+ } catch (error) {
1969
+ console.error(`push: could not read registrations: ${error instanceof Error ? error.message : String(error)}`);
1970
+ }
1971
+ };
1972
+ const unsubscribeConversations = conversations.subscribe(() => update());
1973
+ const unsubscribeCommits = harness.subscribeCommits((publication) => {
1974
+ for (const change of publication.changes) {
1975
+ if (change.type !== "document" || change.record.kind !== TitleDoc.definition.kind)
1976
+ continue;
1977
+ const id = change.conversationId;
1978
+ if (id === undefined)
1979
+ continue;
1980
+ const title = change.value?.title;
1981
+ if (typeof title !== "string" || title === "")
1982
+ continue;
1983
+ const waiters = titleWaiters.get(id);
1984
+ titleWaiters.delete(id);
1985
+ for (const settle of waiters ?? [])
1986
+ settle(title);
1987
+ }
1988
+ });
1989
+ indexWatch?.start(async (value) => {
1990
+ index = value ?? undefined;
1991
+ update();
1992
+ });
1993
+ update();
1994
+ return async () => {
1995
+ disposed = true;
1996
+ unsubscribeConversations();
1997
+ unsubscribeCommits();
1998
+ await indexWatch?.stop();
1999
+ for (const item of pending)
2000
+ drop(item);
2001
+ pending.clear();
2002
+ titleWaiters.clear();
2003
+ for (const timer of receiptTimers)
2004
+ timers.clearTimeout(timer);
2005
+ receiptTimers.clear();
2006
+ };
2007
+ }
2008
+
1654
2009
  // packages/protocol/src/tool-target.ts
1655
2010
  var TARGET_KEYS = [
1656
2011
  "path",
@@ -1678,154 +2033,8 @@ function toolTarget(args) {
1678
2033
  return;
1679
2034
  }
1680
2035
 
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
2036
  // apps/host/src/gateway.ts
1828
- var context2 = BACKGROUND_CONTEXT2;
2037
+ var context3 = BACKGROUND_CONTEXT3;
1829
2038
  var forkTails = new WeakMap;
1830
2039
  function serializedFork(harness, work) {
1831
2040
  const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
@@ -1845,6 +2054,13 @@ async function startGateway(options) {
1845
2054
  models: options.models,
1846
2055
  settings: options.settings
1847
2056
  });
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
+ });
1848
2064
  const http = createServer((request, response) => {
1849
2065
  serveWebClient(request, response, options.webRoot).catch(() => {
1850
2066
  if (!response.headersSent)
@@ -1867,7 +2083,10 @@ async function startGateway(options) {
1867
2083
  };
1868
2084
  let remote;
1869
2085
  let relay;
1870
- const register = (connection, devicePublicKey) => {
2086
+ const register = (connection, devicePublicKey, joined) => {
2087
+ if (joined?.source === "account") {
2088
+ broadcast("info", `New device "${joined.name}" joined through your account`);
2089
+ }
1871
2090
  const client = new GatewayClient(connection, options, conversations, {
1872
2091
  devicePublicKey,
1873
2092
  closed: (self) => {
@@ -1884,20 +2103,30 @@ async function startGateway(options) {
1884
2103
  const spec = options.secure;
1885
2104
  if (spec === undefined)
1886
2105
  return;
1887
- const { secret, expiresAt } = spec.offers.create();
1888
- const pair = `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret}`;
2106
+ const pair = (secret2) => `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret2}`;
1889
2107
  if (relay !== undefined) {
2108
+ const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
1890
2109
  const target2 = `${relayWsBase(relay.origin)}/c/${relay.hostId}`;
1891
- return { url: `${relay.origin}/#${pair}&url=${encodeURIComponent(target2)}`, expiresAt };
2110
+ return { url: `${relay.origin}/#${pair(secret2)}&url=${encodeURIComponent(target2)}`, expiresAt: expiresAt2 };
2111
+ }
2112
+ if (remote !== undefined) {
2113
+ const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
2114
+ return { url: `${remote.advertiseUrl}/#${pair(secret2)}`, expiresAt: expiresAt2 };
1892
2115
  }
1893
- if (remote !== undefined)
1894
- return { url: `${remote.advertiseUrl}/#${pair}`, expiresAt };
2116
+ const { secret, expiresAt } = spec.offers.create("local");
1895
2117
  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 };
2118
+ return { url: `http://127.0.0.1:${port}/#${pair(secret)}&url=${encodeURIComponent(target)}`, expiresAt };
1897
2119
  }
1898
2120
  });
1899
2121
  clients.add(client);
1900
2122
  };
2123
+ const admission = {
2124
+ harness: options.harness,
2125
+ acceptAccountDevices: () => options.settings.value.acceptAccountDevices,
2126
+ serverBinding: async () => await readServerBinding(options.dataDir).catch(() => {
2127
+ return;
2128
+ })
2129
+ };
1901
2130
  server.on("connection", (socket, request) => {
1902
2131
  socket.on("error", () => socket.terminate());
1903
2132
  const url = new URL(request.url ?? "/", "ws://127.0.0.1");
@@ -1907,7 +2136,7 @@ async function startGateway(options) {
1907
2136
  socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
1908
2137
  return;
1909
2138
  }
1910
- secureHandshake(socket, secure2, options.harness, register);
2139
+ secureHandshake(socket, secure2, admission, register);
1911
2140
  return;
1912
2141
  }
1913
2142
  const token = url.searchParams.get("token");
@@ -1918,14 +2147,34 @@ async function startGateway(options) {
1918
2147
  }
1919
2148
  register(tokenConnection(socket));
1920
2149
  });
1921
- remote = await startRemote(options, register);
2150
+ remote = await startRemote(options, admission, register);
1922
2151
  const secure = options.secure;
2152
+ const detachSettingsWatch = options.settings.subscribe((settings) => {
2153
+ if (!settings.acceptAccountDevices)
2154
+ secure?.offers.revokeAccount();
2155
+ });
1923
2156
  if (secure?.relay !== undefined) {
2157
+ const relayOrigin = secure.relay.origin;
1924
2158
  let link;
1925
2159
  link = startRelayLink({
1926
- origin: secure.relay.origin,
2160
+ origin: relayOrigin,
1927
2161
  signingKey: secure.relay.signingKey,
1928
- accept: (socket) => secureHandshake(socket, secure, options.harness, register),
2162
+ noisePublicKey: secure.hostKey.publicKey,
2163
+ accept: (socket) => secureHandshake(socket, secure, admission, register),
2164
+ accountOffer: async () => {
2165
+ if (!options.settings.value.acceptAccountDevices)
2166
+ return { ok: false, reason: "disabled" };
2167
+ const config = await readServerConfig(options.dataDir).catch(() => {
2168
+ return;
2169
+ });
2170
+ if (config?.binding === undefined)
2171
+ return { ok: false, reason: "not_joined" };
2172
+ if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin)) {
2173
+ return { ok: false, reason: "rebinding" };
2174
+ }
2175
+ const { secret, expiresAt } = secure.offers.createAccount(config.binding);
2176
+ return { ok: true, secret, expiresAt };
2177
+ },
1929
2178
  onStateChange: (state) => {
1930
2179
  if (state.status === "registered") {
1931
2180
  console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId }));
@@ -1949,6 +2198,8 @@ async function startGateway(options) {
1949
2198
  async close() {
1950
2199
  await relay?.close();
1951
2200
  detachAutoTitle?.();
2201
+ detachSettingsWatch();
2202
+ await detachPush?.();
1952
2203
  conversations.dispose();
1953
2204
  await Promise.all([...clients].map((client) => client.dispose()));
1954
2205
  for (const socket of server.clients)
@@ -1962,9 +2213,9 @@ async function startGateway(options) {
1962
2213
  function tokenConnection(socket) {
1963
2214
  return {
1964
2215
  get open() {
1965
- return socket.readyState === WebSocket2.OPEN;
2216
+ return socket.readyState === WebSocket.OPEN;
1966
2217
  },
1967
- send: (text) => socket.readyState === WebSocket2.OPEN ? new Promise((resolve3) => socket.send(text, () => resolve3())) : Promise.resolve(),
2218
+ send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve3) => socket.send(text, () => resolve3())) : Promise.resolve(),
1968
2219
  close: (code, reason) => socket.close(code, reason),
1969
2220
  handle(handlers) {
1970
2221
  socket.on("message", (data) => handlers.message(String(data)));
@@ -1975,9 +2226,9 @@ function tokenConnection(socket) {
1975
2226
  function secureConnection(socket, session) {
1976
2227
  return {
1977
2228
  get open() {
1978
- return socket.readyState === WebSocket2.OPEN;
2229
+ return socket.readyState === WebSocket.OPEN;
1979
2230
  },
1980
- send: (text) => socket.readyState === WebSocket2.OPEN ? new Promise((resolve3) => {
2231
+ send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve3) => {
1981
2232
  const chunks = session.seal(text);
1982
2233
  chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () => resolve3() : undefined));
1983
2234
  }) : Promise.resolve(),
@@ -2012,7 +2263,7 @@ function pickLanAddress(interfaces) {
2012
2263
  }
2013
2264
  return fallback;
2014
2265
  }
2015
- function secureHandshake(socket, secure, harness, register) {
2266
+ function secureHandshake(socket, secure, policy, register) {
2016
2267
  socket.on("error", () => socket.terminate());
2017
2268
  const responder = respondIK({ prologue: SECURE_PROLOGUE, static: secure.hostKey });
2018
2269
  let settled = false;
@@ -2034,7 +2285,7 @@ function secureHandshake(socket, secure, harness, register) {
2034
2285
  clearTimeout(timeout);
2035
2286
  const session = new SecureSession(admitted.result);
2036
2287
  socket.send(admitted.message);
2037
- register(secureConnection(socket, session), admitted.deviceKey);
2288
+ register(secureConnection(socket, session), admitted.deviceKey, admitted.joined);
2038
2289
  }).catch(() => socket.close(1011, "handshake failed"));
2039
2290
  async function admit() {
2040
2291
  const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
@@ -2049,19 +2300,28 @@ function secureHandshake(socket, secure, harness, register) {
2049
2300
  return;
2050
2301
  }
2051
2302
  const deviceKey = toBase64Url(remoteStatic);
2052
- if (!await isRegistered(harness, deviceKey, context2)) {
2053
- if (hello.pair === undefined || !secure.offers.consume(hello.pair.secret)) {
2054
- socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
2055
- return;
2056
- }
2057
- await registerDevice(harness, { publicKey: deviceKey, name: hello.pair.name }, context2);
2303
+ const decision = await admitDevice(policy.harness, {
2304
+ publicKey: deviceKey,
2305
+ ...hello.pair === undefined ? {} : { pair: hello.pair },
2306
+ offers: secure.offers,
2307
+ acceptAccountDevices: policy.acceptAccountDevices,
2308
+ serverBinding: policy.serverBinding
2309
+ }, context3);
2310
+ if (decision.outcome === "refused") {
2311
+ socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
2312
+ return;
2058
2313
  }
2059
2314
  const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
2060
- return { result: result2, message, deviceKey };
2315
+ return {
2316
+ result: result2,
2317
+ message,
2318
+ deviceKey,
2319
+ ...decision.outcome === "admitted" ? { joined: decision.entry } : {}
2320
+ };
2061
2321
  }
2062
2322
  });
2063
2323
  }
2064
- async function startRemote(options, register) {
2324
+ async function startRemote(options, admission, register) {
2065
2325
  const spec = options.secure;
2066
2326
  const remote = spec?.lan;
2067
2327
  if (spec === undefined || remote === undefined)
@@ -2074,7 +2334,7 @@ async function startRemote(options, register) {
2074
2334
  });
2075
2335
  });
2076
2336
  const secure = new WebSocketServer({ server: http });
2077
- secure.on("connection", (socket) => secureHandshake(socket, spec, options.harness, register));
2337
+ secure.on("connection", (socket) => secureHandshake(socket, spec, admission, register));
2078
2338
  await new Promise((resolveListen, reject) => {
2079
2339
  http.once("listening", resolveListen);
2080
2340
  http.once("error", reject);
@@ -2115,7 +2375,7 @@ class ConversationList {
2115
2375
  const summaries = [];
2116
2376
  let cursor;
2117
2377
  do {
2118
- const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context2);
2378
+ const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context3);
2119
2379
  for (const record of page.items) {
2120
2380
  list.#records.set(record.id, record);
2121
2381
  const facts = await list.#load(harness, record);
@@ -2132,23 +2392,23 @@ class ConversationList {
2132
2392
  async#load(harness, record) {
2133
2393
  const history = await historyFacts(harness, record.id);
2134
2394
  const facts = { pending: new Set, ...history };
2135
- const live = await harness.snapshot(LiveDoc, record.id, context2);
2395
+ const live = await harness.snapshot(LiveDoc, record.id, context3);
2136
2396
  facts.running = live?.run !== undefined;
2137
2397
  facts.tools = live?.tools;
2138
2398
  for (const token of this.#questionTokens) {
2139
- const value = await harness.snapshot(token, record.id, context2);
2399
+ const value = await harness.snapshot(token, record.id, context3);
2140
2400
  if (Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
2141
2401
  facts.pending.add(token.definition.kind);
2142
2402
  }
2143
2403
  }
2144
2404
  if (record.owner !== undefined) {
2145
- const subagent = await harness.snapshot(SubagentDoc, record.id, context2);
2405
+ const subagent = await harness.snapshot(SubagentDoc, record.id, context3);
2146
2406
  if (typeof subagent?.label === "string" && subagent.label !== "")
2147
2407
  facts.label = subagent.label;
2148
2408
  if (typeof subagent?.role === "string" && subagent.role !== "")
2149
2409
  facts.role = subagent.role;
2150
2410
  }
2151
- const named = (await harness.snapshot(TitleDoc, record.id, context2))?.title;
2411
+ const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
2152
2412
  if (typeof named === "string" && named !== "")
2153
2413
  facts.named = named;
2154
2414
  return facts;
@@ -2301,12 +2561,12 @@ function titleOf(entry) {
2301
2561
  return title === "" ? {} : { title };
2302
2562
  }
2303
2563
  async function historyFacts(harness, id) {
2304
- const conversation = await harness.conversation(id, context2);
2564
+ const conversation = await harness.conversation(id, context3);
2305
2565
  let first;
2306
2566
  let newest;
2307
2567
  let cursor;
2308
2568
  do {
2309
- const page = await conversation.entries({}, 256, cursor, context2);
2569
+ const page = await conversation.entries({}, 256, cursor, context3);
2310
2570
  first = page.items.findLast((entry) => entry.kind === "pi.user" && entry.conversationId === id && titleOf(entry).title !== undefined) ?? first;
2311
2571
  newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
2312
2572
  cursor = page.next;
@@ -2450,25 +2710,25 @@ class GatewayClient {
2450
2710
  return { stop: source.subscribe((value) => void send({ type: "snapshot", stream, value })) };
2451
2711
  }
2452
2712
  if (stream === "index") {
2453
- const watch2 = await this.#options.harness.watchDoc(IndexDoc, context2);
2713
+ const watch2 = await this.#options.harness.watchDoc(IndexDoc, context3);
2454
2714
  if (watch2 === undefined)
2455
2715
  throw new Error("Index document is missing");
2456
2716
  return this.#forward(stream, watch2, send);
2457
2717
  }
2458
2718
  if (stream === "devices") {
2459
- const watch2 = await this.#options.harness.watchDoc(DevicesDoc, context2);
2719
+ const watch2 = await this.#options.harness.watchDoc(DevicesDoc, context3);
2460
2720
  if (watch2 === undefined)
2461
2721
  throw new Error("Devices document is missing");
2462
2722
  return this.#forward(stream, watch2, send);
2463
2723
  }
2464
2724
  if (stream === "tasks")
2465
- return this.#forward(stream, await this.#options.harness.watchTaskGraph(context2), send);
2725
+ return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
2466
2726
  if (stream.startsWith("doc:")) {
2467
2727
  const at = stream.lastIndexOf(":");
2468
2728
  return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
2469
2729
  }
2470
2730
  const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
2471
- return this.#forward(stream, await conversation.watch(context2), send);
2731
+ return this.#forward(stream, await conversation.watch(context3), send);
2472
2732
  }
2473
2733
  #hostValue(stream) {
2474
2734
  return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
@@ -2494,7 +2754,7 @@ class GatewayClient {
2494
2754
  setImmediate(() => signalCreation());
2495
2755
  });
2496
2756
  try {
2497
- const existing = await harness.watchDoc(token, id, context2);
2757
+ const existing = await harness.watchDoc(token, id, context3);
2498
2758
  if (existing !== undefined) {
2499
2759
  unsubscribe();
2500
2760
  return this.#forward(stream, existing, send);
@@ -2507,7 +2767,7 @@ class GatewayClient {
2507
2767
  created.then(async () => {
2508
2768
  if (stopped)
2509
2769
  return;
2510
- const watch2 = await harness.watchDoc(token, id, context2);
2770
+ const watch2 = await harness.watchDoc(token, id, context3);
2511
2771
  if (watch2 === undefined)
2512
2772
  return;
2513
2773
  if (stopped)
@@ -2541,7 +2801,7 @@ class GatewayClient {
2541
2801
  await subscription?.subscription?.stop();
2542
2802
  }
2543
2803
  async#conversation(id) {
2544
- const conversation = await this.#options.harness.conversation(id, context2);
2804
+ const conversation = await this.#options.harness.conversation(id, context3);
2545
2805
  if (conversation === undefined)
2546
2806
  throw new Error(`Conversation ${id} does not exist`);
2547
2807
  return conversation;
@@ -2557,9 +2817,9 @@ class GatewayClient {
2557
2817
  async#call(method, args) {
2558
2818
  switch (method) {
2559
2819
  case "addProject":
2560
- return addProject(this.#options.harness, args.path, context2);
2820
+ return addProject(this.#options.harness, args.path, context3);
2561
2821
  case "removeProject":
2562
- await removeProject(this.#options.harness, args.path, context2);
2822
+ await removeProject(this.#options.harness, args.path, context3);
2563
2823
  return null;
2564
2824
  case "createConversation": {
2565
2825
  const create = args;
@@ -2574,14 +2834,14 @@ class GatewayClient {
2574
2834
  ...create.checkout === undefined ? {} : { checkout: create.checkout },
2575
2835
  ...create.model === undefined ? {} : { model: create.model },
2576
2836
  ...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel }
2577
- }, settingsDefaults(this.#options.settings.value), this.#options.models, context2);
2837
+ }, settingsDefaults(this.#options.settings.value), this.#options.models, context3);
2578
2838
  return { conversationId };
2579
2839
  }
2580
2840
  case "archive": {
2581
2841
  const input = args;
2582
- await archive(this.#options.harness, input.conversationId, input.archived, context2);
2842
+ await archive(this.#options.harness, input.conversationId, input.archived, context3);
2583
2843
  if (input.archived) {
2584
- const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context2);
2844
+ const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
2585
2845
  for (const { record, reason } of kept) {
2586
2846
  await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
2587
2847
  }
@@ -2596,10 +2856,17 @@ class GatewayClient {
2596
2856
  }
2597
2857
  case "revokeDevice": {
2598
2858
  const { publicKey } = args;
2599
- await revokeDevice(this.#options.harness, publicKey, context2);
2859
+ await revokeDevice(this.#options.harness, publicKey, context3);
2600
2860
  this.#hooks.closeDevice(publicKey);
2601
2861
  return null;
2602
2862
  }
2863
+ case "setPushToken": {
2864
+ const device = this.devicePublicKey;
2865
+ if (device === undefined)
2866
+ throw new Error("Push tokens register over the secure channel only");
2867
+ await setPushToken(this.#options.harness, device, args.token, context3);
2868
+ return null;
2869
+ }
2603
2870
  case "logout": {
2604
2871
  const modelAuth = this.#options.modelAuth;
2605
2872
  if (modelAuth === undefined)
@@ -2642,9 +2909,9 @@ class GatewayClient {
2642
2909
  const problem = imageProblem(images ?? []);
2643
2910
  if (problem !== undefined)
2644
2911
  throw new Error(problem);
2645
- const dismissed = await this.#dismissPendingQuestions(conversation.id, context2);
2646
- const submission = await conversation.submit({ type: "input", content: messageContent(text, images), whenBusy: dismissed ? "steer" : whenBusy, requestId }, context2);
2647
- submission.wait(context2).then((settled) => {
2912
+ const dismissed = await this.#dismissPendingQuestions(conversation.id, context3);
2913
+ const submission = await conversation.submit({ type: "input", content: messageContent(text, images), whenBusy: dismissed ? "steer" : whenBusy, requestId }, context3);
2914
+ submission.wait(context3).then((settled) => {
2648
2915
  if (settled.status === "unanswered" && settled.reason !== "aborted") {
2649
2916
  this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
2650
2917
  }
@@ -2668,16 +2935,16 @@ class GatewayClient {
2668
2935
  const doc = await tx.doc(TitleDoc, conversation.id);
2669
2936
  doc.title = trimmed;
2670
2937
  doc.source = "manual";
2671
- }, context2);
2938
+ }, context3);
2672
2939
  return null;
2673
2940
  }
2674
2941
  case "abort":
2675
- await conversation.abort(context2);
2942
+ await conversation.abort(context3);
2676
2943
  return null;
2677
2944
  case "compact": {
2678
2945
  const { instructions } = args;
2679
- const taskId = await conversation.compact(instructions, context2);
2680
- this.#options.harness.waitForTask(taskId, context2).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
2946
+ const taskId = await conversation.compact(instructions, context3);
2947
+ this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
2681
2948
  return { taskId };
2682
2949
  }
2683
2950
  case "setModel": {
@@ -2686,7 +2953,7 @@ class GatewayClient {
2686
2953
  if (model === undefined)
2687
2954
  throw new Error(`Unknown model: ${ref.provider}/${ref.modelId}`);
2688
2955
  const thinking = (await this.#agent(conversation.id)).thinkingLevel ?? "off";
2689
- await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) }, context2);
2956
+ await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) }, context3);
2690
2957
  return null;
2691
2958
  }
2692
2959
  case "setThinkingLevel": {
@@ -2698,7 +2965,7 @@ class GatewayClient {
2698
2965
  if (!getSupportedThinkingLevels(model).includes(level)) {
2699
2966
  throw new Error(`Thinking level ${level} is not supported by ${agent.model.provider}/${agent.model.modelId}`);
2700
2967
  }
2701
- await conversation.configure({ thinkingLevel: level }, context2);
2968
+ await conversation.configure({ thinkingLevel: level }, context3);
2702
2969
  return null;
2703
2970
  }
2704
2971
  case "fork": {
@@ -2722,13 +2989,13 @@ class GatewayClient {
2722
2989
  }
2723
2990
  cursor = page.next;
2724
2991
  } while (cursor !== undefined);
2725
- }, context2);
2726
- const agent = await conversation.agent(context2);
2992
+ }, context3);
2993
+ const agent = await conversation.agent(context3);
2727
2994
  const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
2728
2995
  const fork = await conversation.fork(at, {
2729
2996
  ownership: { kind: "ownerless" },
2730
2997
  ...remove.length === 0 ? {} : { agent: { tools: { remove } } }
2731
- }, context2);
2998
+ }, context3);
2732
2999
  return { conversationId: fork.id };
2733
3000
  });
2734
3001
  }
@@ -2764,16 +3031,16 @@ class GatewayClient {
2764
3031
  }
2765
3032
  request.resolution = { outcome: "answered", answers, at: Date.now() };
2766
3033
  return { first: true };
2767
- }, context2);
3034
+ }, context3);
2768
3035
  }
2769
3036
  case "changes": {
2770
- return await this.#changes(conversation, context2);
3037
+ return await this.#changes(conversation, context3);
2771
3038
  }
2772
3039
  }
2773
3040
  }
2774
- async#dismissPendingQuestions(conversationId, context3) {
3041
+ async#dismissPendingQuestions(conversationId, context4) {
2775
3042
  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, context3)));
3043
+ const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
2777
3044
  const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
2778
3045
  if (!pending)
2779
3046
  return false;
@@ -2791,18 +3058,18 @@ class GatewayClient {
2791
3058
  }
2792
3059
  }
2793
3060
  return dismissed;
2794
- }, context3);
3061
+ }, context4);
2795
3062
  }
2796
3063
  get #chatsPrefix() {
2797
3064
  return join6(this.#options.dataDir, "chats") + sep3;
2798
3065
  }
2799
- async#changes(conversation, context3) {
2800
- const cwd = (await conversation.agent(context3)).cwd;
3066
+ async#changes(conversation, context4) {
3067
+ const cwd = (await conversation.agent(context4)).cwd;
2801
3068
  if (cwd === undefined)
2802
3069
  return { available: false, reason: "No working directory" };
2803
3070
  if (cwd.startsWith(this.#chatsPrefix))
2804
3071
  return { available: false, reason: "Chat conversations have no repository" };
2805
- const index = await this.#options.harness.snapshot(IndexDoc, context3);
3072
+ const index = await this.#options.harness.snapshot(IndexDoc, context4);
2806
3073
  const record = checkoutAt(index?.checkouts, cwd);
2807
3074
  if (record !== undefined) {
2808
3075
  if (!await worktreeExists(record.path))
@@ -2815,7 +3082,7 @@ class GatewayClient {
2815
3082
  return { available: true, ...await changesOf(base.repo, base.base) };
2816
3083
  }
2817
3084
  async#agent(id) {
2818
- return await this.#options.harness.snapshot(AgentDoc3, id, context2) ?? {};
3085
+ return await this.#options.harness.snapshot(AgentDoc3, id, context3) ?? {};
2819
3086
  }
2820
3087
  async dispose() {
2821
3088
  if (this.#disposed)
@@ -2829,7 +3096,7 @@ class GatewayClient {
2829
3096
  }
2830
3097
 
2831
3098
  // apps/host/src/host.ts
2832
- var context3 = BACKGROUND_CONTEXT3;
3099
+ var context4 = BACKGROUND_CONTEXT4;
2833
3100
  async function hostToken(dataDir) {
2834
3101
  const path = join7(dataDir, "token");
2835
3102
  try {
@@ -2898,7 +3165,7 @@ async function openHost(options) {
2898
3165
  return;
2899
3166
  const worktrees = worktreeRoot(options.dataDir);
2900
3167
  if (cwd === worktrees || cwd.startsWith(worktrees + sep4)) {
2901
- const index = await target.read.snapshot(IndexDoc, context3);
3168
+ const index = await target.read.snapshot(IndexDoc, context4);
2902
3169
  const record = checkoutAt(index?.checkouts, cwd);
2903
3170
  if (record !== undefined) {
2904
3171
  await ensureWorktree(record);
@@ -2918,11 +3185,11 @@ async function openHost(options) {
2918
3185
  settings: typeof options.settings === "function" ? options.settings(hostSettings) : options.settings
2919
3186
  },
2920
3187
  onReport: (error) => report(error)
2921
- }, context3);
2922
- await ensureIndex(harness, context3);
2923
- await ensureDevices(harness, context3);
3188
+ }, context4);
3189
+ await ensureIndex(harness, context4);
3190
+ await ensureDevices(harness, context4);
2924
3191
  for (const path of options.projects ?? [])
2925
- await addProject(harness, path, context3);
3192
+ await addProject(harness, path, context4);
2926
3193
  const hostKey = await loadHostKey(options.dataDir);
2927
3194
  const offers = pairingOffers(options.remote?.pairingTtlMs);
2928
3195
  const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
@@ -2943,6 +3210,13 @@ async function openHost(options) {
2943
3210
  ...options.modelAuth === undefined ? {} : { modelAuth: options.modelAuth },
2944
3211
  ...options.webRoot === undefined ? {} : { webRoot: options.webRoot },
2945
3212
  ...options.autoTitle === undefined ? {} : { autoTitle: options.autoTitle },
3213
+ ...options.push === undefined ? {} : {
3214
+ push: {
3215
+ hostKey: toBase64Url(hostKey.publicKey),
3216
+ ...options.push.fetch === undefined ? {} : { fetch: options.push.fetch },
3217
+ ...options.push.timers === undefined ? {} : { timers: options.push.timers }
3218
+ }
3219
+ },
2946
3220
  secure: {
2947
3221
  hostKey,
2948
3222
  offers,
@@ -2975,19 +3249,25 @@ async function openHost(options) {
2975
3249
  harness,
2976
3250
  closed: opened.closed,
2977
3251
  settings: hostSettings,
3252
+ createAccountOffer: async () => {
3253
+ const binding = await readServerBinding(options.dataDir).catch(() => {
3254
+ return;
3255
+ });
3256
+ return binding === undefined ? undefined : offers.createAccount(binding);
3257
+ },
2978
3258
  ...gateway.remote === undefined || hostKey === undefined ? {} : { remote: { url: gateway.remote.url, advertiseUrl: gateway.remote.advertiseUrl, hostKey: hostKey.publicKey } },
2979
3259
  ...gateway.relay === undefined ? {} : { relay: { origin: gateway.relay.origin, hostId: gateway.relay.hostId, state: () => gateway.relay.state() } },
2980
3260
  close() {
2981
3261
  closing ??= (async () => {
2982
3262
  try {
2983
3263
  await gateway.close();
2984
- await opened.close(context3);
3264
+ await opened.close(context4);
2985
3265
  await mcp?.close();
2986
3266
  agentRoles?.close();
2987
3267
  for (const each of extensions)
2988
3268
  await each.close?.();
2989
3269
  options.modelAuth?.close();
2990
- await Promise.all([...environments.values()].map((env) => env.cleanup(context3)));
3270
+ await Promise.all([...environments.values()].map((env) => env.cleanup(context4)));
2991
3271
  } finally {
2992
3272
  await release();
2993
3273
  }
@@ -3001,7 +3281,7 @@ async function openHost(options) {
3001
3281
  for (const each of extensions)
3002
3282
  await each.close?.().catch(() => {});
3003
3283
  await mcp?.close().catch(() => {});
3004
- await harness?.close(context3).catch(() => {});
3284
+ await harness?.close(context4).catch(() => {});
3005
3285
  await release().catch(() => {});
3006
3286
  throw error;
3007
3287
  }
@@ -3210,10 +3490,17 @@ async function main(argv) {
3210
3490
  }
3211
3491
  });
3212
3492
  const dataDir = resolve3(values["data-dir"] ?? DEFAULT_DATA_DIR);
3493
+ let relayOrigin;
3494
+ try {
3495
+ relayOrigin = resolveRelayOrigin(values.relay, await readServerConfig(dataDir));
3496
+ } catch (error) {
3497
+ console.error(error instanceof Error ? error.message : String(error));
3498
+ process.exit(1);
3499
+ }
3213
3500
  const extensionsFor = (models, summaries, options2) => (host2) => [
3214
3501
  createContext({
3215
3502
  agentsHome: join8(homedir3(), ".agents"),
3216
- checkout: (input, context4) => checkoutInfo(host2.harness(), input, context4)
3503
+ checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
3217
3504
  }),
3218
3505
  createCoding(options2),
3219
3506
  todo,
@@ -3240,7 +3527,7 @@ async function main(argv) {
3240
3527
  ...values["public-url"] === undefined ? {} : { publicUrl: values["public-url"] }
3241
3528
  }
3242
3529
  },
3243
- ...values.relay === undefined ? {} : { relay: { origin: values.relay } }
3530
+ ...relayOrigin === undefined ? {} : { relay: { origin: relayOrigin } }
3244
3531
  };
3245
3532
  function fauxOptions(responses) {
3246
3533
  const faux = fauxProvider({ tokensPerSecond: Number(values["faux-tps"]), tokenSize: { min: 1, max: 1 } });
@@ -3286,6 +3573,7 @@ async function main(argv) {
3286
3573
  modelAuth,
3287
3574
  extensions: extensionsFor(modelRuntime, summaries, { images }),
3288
3575
  settings: (store) => createHarnessSettings(settingsManager, store),
3576
+ push: {},
3289
3577
  settingsFallback: {
3290
3578
  defaultModel: () => {
3291
3579
  const model2 = defaultModel(settingsManager, modelRuntime);