pinomad 0.11.0 → 0.13.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 +1 -1
- package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
- package/dist/{chunk-jt34g8st.js → chunk-4fferbxw.js} +247 -27
- package/dist/{chunk-67gq282t.js → chunk-53xv3fny.js} +725 -75
- package/dist/{chunk-44pnts98.js → chunk-563fnad9.js} +1 -1
- package/dist/{chunk-b457q0tm.js → chunk-6th9xv39.js} +11 -8
- package/dist/{chunk-ncbqa8jk.js → chunk-7mn38n4f.js} +1 -1
- package/dist/{chunk-as6pyf00.js → chunk-90g1cg9n.js} +1 -1
- package/dist/{chunk-9h5vawnm.js → chunk-aapsc9g0.js} +270 -66
- package/dist/{chunk-n1m1tjrk.js → chunk-d4tq62yr.js} +57 -16
- package/dist/{chunk-gtdpjqa3.js → chunk-d9mq5d46.js} +205 -49
- package/dist/{chunk-y127fe1a.js → chunk-g3ahp7b9.js} +11 -0
- package/dist/{chunk-xqycs43g.js → chunk-kmzzjv53.js} +83 -8
- package/dist/{chunk-4dcs8sbj.js → chunk-m51mq8vh.js} +43 -21
- package/dist/{chunk-tj57aqav.js → chunk-maqv34cs.js} +2 -2
- package/dist/chunk-nq57fdnq.js +106 -0
- package/dist/{chunk-h5q2y57m.js → chunk-ra86wjp5.js} +10 -4
- package/dist/{chunk-sd5fkxjj.js → chunk-s0h7c7da.js} +1 -1
- package/dist/chunk-sd640dt7.js +22 -0
- package/dist/{chunk-6d16c2fv.js → chunk-t6fw0wa9.js} +13 -6
- package/dist/{chunk-vvj9b1gc.js → chunk-wd7aracm.js} +188 -54
- package/dist/pinomad.js +15 -14
- package/package.json +13 -11
- package/web/assets/astro-DOly5024.js +1 -0
- package/web/assets/c-BIGW1oBm.js +1 -0
- package/web/assets/core-BHm2gdHL.js +4 -0
- package/web/assets/cpp-DddhVWhK.js +1 -0
- package/web/assets/csharp-B_TYUkTI.js +1 -0
- package/web/assets/css-Z72CF80r.js +1 -0
- package/web/assets/dart-CtKP3W-j.js +1 -0
- package/web/assets/diff-D97Zzqfu.js +1 -0
- package/web/assets/dockerfile-BcOcwvcX.js +1 -0
- package/web/assets/elixir-DkJLSxtU.js +1 -0
- package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
- package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
- package/web/assets/fish-BvzEVeQv.js +1 -0
- package/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/assets/go-C27-OAKa.js +1 -0
- package/web/assets/graphql-ChdNCCLP.js +1 -0
- package/web/assets/haskell-Df6bDoY_.js +1 -0
- package/web/assets/hcl-BWvSN4gD.js +1 -0
- package/web/assets/highlighted-diff-DrHvSLK9.js +175 -0
- package/web/assets/highlights-1aLwssz-.js +4 -0
- package/web/assets/highlights-themes-C1p4pFYS.js +1 -0
- package/web/assets/html-BlS82ilI.js +1 -0
- package/web/assets/index-A_AUJoO6.css +1 -0
- package/web/assets/index-BafYMKiN.js +396 -0
- package/web/assets/ini-BEwlwnbL.js +1 -0
- package/web/assets/java-CylS5w8V.js +1 -0
- package/web/assets/javascript-wDzz0qaB.js +1 -0
- package/web/assets/json-Cp-IABpG.js +1 -0
- package/web/assets/jsonc-Des-eS-w.js +1 -0
- package/web/assets/jsx-g9-lgVsj.js +1 -0
- package/web/assets/kotlin-BdnUsdx6.js +1 -0
- package/web/assets/less-B1dDrJ26.js +1 -0
- package/web/assets/lua-BaeVxFsk.js +1 -0
- package/web/assets/make-CHLpvVh8.js +1 -0
- package/web/assets/markdown-Cvjx9yec.js +1 -0
- package/web/assets/nix-CwoSXNpI.js +1 -0
- package/web/assets/objective-c-DXmwc3jG.js +1 -0
- package/web/assets/php-DoruYMF0.js +1 -0
- package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
- package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
- package/web/assets/pierre-light-480U9XYS.js +1 -0
- package/web/assets/pierre-light-BVSW-Xev.js +1 -0
- package/web/assets/postcss-DnzrXTVv.js +1 -0
- package/web/assets/powershell-Dd0kTxKP.js +1 -0
- package/web/assets/prisma-Vru482bI.js +1 -0
- package/web/assets/proto-C7zT0LnQ.js +1 -0
- package/web/assets/python-B6aJPvgy.js +1 -0
- package/web/assets/ruby-B43yHlf9.js +1 -0
- package/web/assets/rust-DPvrY3IQ.js +1 -0
- package/web/assets/scala-CqE71os6.js +1 -0
- package/web/assets/scss-Cw6XMfVs.js +1 -0
- package/web/assets/shellscript-Yzrsuije.js +1 -0
- package/web/assets/shiki-BwyCm3kZ.js +2 -0
- package/web/assets/shiki-langs-CMlALLSg.js +2 -0
- package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
- package/web/assets/sql-DkvoVjww.js +1 -0
- package/web/assets/stream-tokenizer-BlgIhw0y.js +4 -0
- package/web/assets/svelte-DoIXUgJc.js +1 -0
- package/web/assets/swift-C2oV4EkX.js +1 -0
- package/web/assets/terraform-BETggiCN.js +1 -0
- package/web/assets/theming-themes-DDvoqEqT.js +1 -0
- package/web/assets/toml-vGWfd6FD.js +1 -0
- package/web/assets/tsx-COt5Ahok.js +1 -0
- package/web/assets/typescript-BPQ3VLAy.js +1 -0
- package/web/assets/vue-CwSeJimo.js +1 -0
- package/web/assets/xml-sdJ4AIDG.js +1 -0
- package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
- package/web/assets/xterm-view-BnKOnF3C.js +38 -0
- package/web/assets/yaml-Buea-lGh.js +1 -0
- package/web/assets/zig-VOosw3JB.js +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-etf7ef97.js +0 -55
- package/dist/chunk-greb3dp6.js +0 -22
- package/web/assets/index-BhHrKpoo.js +0 -395
- package/web/assets/index-DoyHMPwf.css +0 -1
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
openHostSettings,
|
|
6
6
|
sameList,
|
|
7
7
|
settingsDefaults
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-563fnad9.js";
|
|
9
9
|
import {
|
|
10
10
|
DevicesDoc,
|
|
11
11
|
RevocationsDoc,
|
|
@@ -16,19 +16,21 @@ import {
|
|
|
16
16
|
pairingOffers
|
|
17
17
|
} from "./chunk-t91zt5f0.js";
|
|
18
18
|
import {
|
|
19
|
+
REMOVAL_NOTICE_FILE,
|
|
20
|
+
markRemoved,
|
|
19
21
|
normalizeOrigin,
|
|
20
22
|
readServerBinding,
|
|
21
23
|
readServerConfig,
|
|
22
24
|
resolveRelayOrigin
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-nq57fdnq.js";
|
|
24
26
|
import {
|
|
25
27
|
serveWebClient
|
|
26
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-445cevpm.js";
|
|
27
29
|
import {
|
|
28
30
|
relayHostId,
|
|
29
31
|
relayWsBase,
|
|
30
32
|
startRelayLink
|
|
31
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-g3ahp7b9.js";
|
|
32
34
|
import {
|
|
33
35
|
ATTACHMENT_TTL_MS,
|
|
34
36
|
INVALID_FRAME_CLOSE_CODE,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
MAX_ATTACHMENT_NAME_LENGTH,
|
|
37
39
|
MAX_PENDING_ATTACHMENTS,
|
|
38
40
|
MAX_PENDING_ATTACHMENT_BYTES,
|
|
41
|
+
MAX_TRANSCRIPT_PAGE,
|
|
39
42
|
PROTOCOL,
|
|
40
43
|
RTC_DATA_CHANNEL_LABEL,
|
|
41
44
|
RTC_DEFAULT_STUN,
|
|
@@ -54,7 +57,7 @@ import {
|
|
|
54
57
|
rtcCandidateType,
|
|
55
58
|
sdpCandidates,
|
|
56
59
|
withRtcCandidates
|
|
57
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-kmzzjv53.js";
|
|
58
61
|
import {
|
|
59
62
|
ENDPOINT_SET_SCHEMA_VERSION,
|
|
60
63
|
MAX_HOST_ENDPOINT_URL_LENGTH,
|
|
@@ -77,13 +80,13 @@ import {
|
|
|
77
80
|
import {
|
|
78
81
|
packagedVersion,
|
|
79
82
|
webRoot
|
|
80
|
-
} from "./chunk-
|
|
83
|
+
} from "./chunk-s0h7c7da.js";
|
|
81
84
|
import {
|
|
82
85
|
__require
|
|
83
86
|
} from "./chunk-df32avw6.js";
|
|
84
87
|
|
|
85
88
|
// apps/host/src/main.ts
|
|
86
|
-
import { randomUUID as
|
|
89
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
87
90
|
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
88
91
|
import { homedir as homedir3 } from "node:os";
|
|
89
92
|
import { join as join12, resolve as resolve7 } from "node:path";
|
|
@@ -176,8 +179,10 @@ var QuestionWait = defineTask({
|
|
|
176
179
|
resolve(initial);
|
|
177
180
|
return;
|
|
178
181
|
}
|
|
179
|
-
watch.start(async (
|
|
180
|
-
|
|
182
|
+
watch.start(async (call) => {
|
|
183
|
+
if (call.kind !== "value")
|
|
184
|
+
return;
|
|
185
|
+
const found = resolutionOf(call.value, task.input.requestId);
|
|
181
186
|
if (found !== undefined)
|
|
182
187
|
resolve(found);
|
|
183
188
|
});
|
|
@@ -712,11 +717,7 @@ async function checkoutInfo(harness, input, context) {
|
|
|
712
717
|
}
|
|
713
718
|
};
|
|
714
719
|
}
|
|
715
|
-
async function
|
|
716
|
-
const index = await harness.snapshot(IndexDoc, context);
|
|
717
|
-
const checkouts = index?.checkouts ?? [];
|
|
718
|
-
if (checkouts.length === 0)
|
|
719
|
-
return [];
|
|
720
|
+
async function conversationSubtree(harness, rootId, context) {
|
|
720
721
|
const all = [];
|
|
721
722
|
let cursor;
|
|
722
723
|
do {
|
|
@@ -743,6 +744,27 @@ async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
|
743
744
|
}
|
|
744
745
|
}
|
|
745
746
|
}
|
|
747
|
+
return subtree;
|
|
748
|
+
}
|
|
749
|
+
async function familyRoot(harness, id, context) {
|
|
750
|
+
let current = id;
|
|
751
|
+
const seen = new Set;
|
|
752
|
+
while (!seen.has(current)) {
|
|
753
|
+
seen.add(current);
|
|
754
|
+
const record = await harness.commit((tx) => tx.conversation(current), context);
|
|
755
|
+
const parent = record?.parent?.conversationId ?? record?.owner?.conversationId;
|
|
756
|
+
if (parent === undefined)
|
|
757
|
+
return current;
|
|
758
|
+
current = parent;
|
|
759
|
+
}
|
|
760
|
+
return current;
|
|
761
|
+
}
|
|
762
|
+
async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
763
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
764
|
+
const checkouts = index?.checkouts ?? [];
|
|
765
|
+
if (checkouts.length === 0)
|
|
766
|
+
return [];
|
|
767
|
+
const subtree = await conversationSubtree(harness, rootId, context);
|
|
746
768
|
const kept = [];
|
|
747
769
|
for (const record of checkouts.filter((checkout) => subtree.has(checkout.conversationId))) {
|
|
748
770
|
const reason = await removeWorktree(record);
|
|
@@ -1451,6 +1473,7 @@ var todo = {
|
|
|
1451
1473
|
};
|
|
1452
1474
|
|
|
1453
1475
|
// apps/host/src/gateway.ts
|
|
1476
|
+
import { stat as stat6 } from "node:fs/promises";
|
|
1454
1477
|
import { createServer } from "node:http";
|
|
1455
1478
|
import { homedir, networkInterfaces } from "node:os";
|
|
1456
1479
|
import { join as join7, sep as sep5 } from "node:path";
|
|
@@ -2056,6 +2079,329 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
|
2056
2079
|
};
|
|
2057
2080
|
}
|
|
2058
2081
|
|
|
2082
|
+
// apps/host/src/terminals.ts
|
|
2083
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2084
|
+
import { userInfo } from "node:os";
|
|
2085
|
+
var TERMINAL_LIMIT = 8;
|
|
2086
|
+
var BUFFER_SIZE = 512 * 1024;
|
|
2087
|
+
var FLUSH_MS = 8;
|
|
2088
|
+
var MAX_CHUNK = 16 * 1024;
|
|
2089
|
+
var MAX_TITLE = 1024;
|
|
2090
|
+
var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
|
|
2091
|
+
|
|
2092
|
+
class Terminals {
|
|
2093
|
+
#options;
|
|
2094
|
+
#limit;
|
|
2095
|
+
#terminals = new Map;
|
|
2096
|
+
#numbers = new Map;
|
|
2097
|
+
#watchers = new Map;
|
|
2098
|
+
constructor(options) {
|
|
2099
|
+
this.#options = options;
|
|
2100
|
+
this.#limit = options.limit ?? TERMINAL_LIMIT;
|
|
2101
|
+
}
|
|
2102
|
+
get limit() {
|
|
2103
|
+
return this.#limit;
|
|
2104
|
+
}
|
|
2105
|
+
list(owner) {
|
|
2106
|
+
const terminals = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner).map(info);
|
|
2107
|
+
return { owner, limit: this.#limit, terminals };
|
|
2108
|
+
}
|
|
2109
|
+
watch(owner, listener) {
|
|
2110
|
+
let set = this.#watchers.get(owner);
|
|
2111
|
+
if (set === undefined)
|
|
2112
|
+
this.#watchers.set(owner, set = new Set);
|
|
2113
|
+
set.add(listener);
|
|
2114
|
+
listener(this.list(owner));
|
|
2115
|
+
return () => {
|
|
2116
|
+
set.delete(listener);
|
|
2117
|
+
if (set.size === 0)
|
|
2118
|
+
this.#watchers.delete(owner);
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
open(owner, cwd, size) {
|
|
2122
|
+
const count = [...this.#terminals.values()].filter((terminal2) => terminal2.owner === owner).length;
|
|
2123
|
+
if (count >= this.#limit)
|
|
2124
|
+
throw new Error(`Terminal limit reached (${this.#limit})`);
|
|
2125
|
+
const number = (this.#numbers.get(owner) ?? 0) + 1;
|
|
2126
|
+
this.#numbers.set(owner, number);
|
|
2127
|
+
const terminal = {
|
|
2128
|
+
id: randomUUID2(),
|
|
2129
|
+
owner,
|
|
2130
|
+
cwd,
|
|
2131
|
+
number,
|
|
2132
|
+
pty: undefined,
|
|
2133
|
+
generation: 0,
|
|
2134
|
+
cols: size.cols,
|
|
2135
|
+
rows: size.rows,
|
|
2136
|
+
title: undefined,
|
|
2137
|
+
rename: undefined,
|
|
2138
|
+
exit: undefined,
|
|
2139
|
+
buffer: "",
|
|
2140
|
+
start: 0,
|
|
2141
|
+
pending: "",
|
|
2142
|
+
flushTimer: undefined,
|
|
2143
|
+
titleCarry: "",
|
|
2144
|
+
viewers: new Set
|
|
2145
|
+
};
|
|
2146
|
+
this.#terminals.set(terminal.id, terminal);
|
|
2147
|
+
try {
|
|
2148
|
+
this.#start(terminal);
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
this.#terminals.delete(terminal.id);
|
|
2151
|
+
throw error;
|
|
2152
|
+
}
|
|
2153
|
+
this.#changed(owner);
|
|
2154
|
+
return terminal.id;
|
|
2155
|
+
}
|
|
2156
|
+
input(id, data, size) {
|
|
2157
|
+
const terminal = this.#get(id);
|
|
2158
|
+
if (size !== undefined)
|
|
2159
|
+
this.#resize(terminal, size);
|
|
2160
|
+
terminal.pty?.write(data);
|
|
2161
|
+
}
|
|
2162
|
+
resize(id, size) {
|
|
2163
|
+
this.#resize(this.#get(id), size);
|
|
2164
|
+
}
|
|
2165
|
+
rename(id, name) {
|
|
2166
|
+
const terminal = this.#get(id);
|
|
2167
|
+
const trimmed = name?.trim();
|
|
2168
|
+
terminal.rename = trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
2169
|
+
this.#changed(terminal.owner);
|
|
2170
|
+
}
|
|
2171
|
+
restart(id, size) {
|
|
2172
|
+
const terminal = this.#get(id);
|
|
2173
|
+
this.#stop(terminal);
|
|
2174
|
+
terminal.start += terminal.buffer.length + terminal.pending.length + 1;
|
|
2175
|
+
terminal.buffer = "";
|
|
2176
|
+
terminal.pending = "";
|
|
2177
|
+
terminal.titleCarry = "";
|
|
2178
|
+
terminal.title = undefined;
|
|
2179
|
+
terminal.exit = undefined;
|
|
2180
|
+
terminal.cols = size.cols;
|
|
2181
|
+
terminal.rows = size.rows;
|
|
2182
|
+
for (const viewer of terminal.viewers)
|
|
2183
|
+
viewer.position = undefined;
|
|
2184
|
+
try {
|
|
2185
|
+
this.#start(terminal);
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
terminal.exit = { code: null };
|
|
2188
|
+
this.#changed(terminal.owner);
|
|
2189
|
+
throw error;
|
|
2190
|
+
}
|
|
2191
|
+
this.#changed(terminal.owner);
|
|
2192
|
+
}
|
|
2193
|
+
close(id) {
|
|
2194
|
+
const terminal = this.#get(id);
|
|
2195
|
+
this.#remove(terminal);
|
|
2196
|
+
this.#changed(terminal.owner);
|
|
2197
|
+
}
|
|
2198
|
+
closeOwners(owners) {
|
|
2199
|
+
for (const owner of new Set(owners)) {
|
|
2200
|
+
const owned = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner);
|
|
2201
|
+
if (owned.length === 0)
|
|
2202
|
+
continue;
|
|
2203
|
+
for (const terminal of owned)
|
|
2204
|
+
this.#remove(terminal);
|
|
2205
|
+
this.#changed(owner);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
closeAll() {
|
|
2209
|
+
this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
|
|
2210
|
+
}
|
|
2211
|
+
attach(id, since, sink) {
|
|
2212
|
+
const terminal = this.#get(id);
|
|
2213
|
+
const end = terminal.start + terminal.buffer.length;
|
|
2214
|
+
const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
|
|
2215
|
+
const viewer = { sink, position, sending: false };
|
|
2216
|
+
terminal.viewers.add(viewer);
|
|
2217
|
+
this.#pump(terminal, viewer);
|
|
2218
|
+
return () => {
|
|
2219
|
+
terminal.viewers.delete(viewer);
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
#get(id) {
|
|
2223
|
+
const terminal = this.#terminals.get(id);
|
|
2224
|
+
if (terminal === undefined)
|
|
2225
|
+
throw new Error("Unknown terminal");
|
|
2226
|
+
return terminal;
|
|
2227
|
+
}
|
|
2228
|
+
#start(terminal) {
|
|
2229
|
+
const generation = ++terminal.generation;
|
|
2230
|
+
const { shell, env } = this.#options;
|
|
2231
|
+
const pty = this.#options.spawn(shell.file, [...shell.args], {
|
|
2232
|
+
name: "xterm-256color",
|
|
2233
|
+
cols: terminal.cols,
|
|
2234
|
+
rows: terminal.rows,
|
|
2235
|
+
cwd: terminal.cwd,
|
|
2236
|
+
env: { ...env, TERM: "xterm-256color", COLORTERM: "truecolor" }
|
|
2237
|
+
});
|
|
2238
|
+
terminal.pty = pty;
|
|
2239
|
+
pty.onData((data) => {
|
|
2240
|
+
if (terminal.generation !== generation)
|
|
2241
|
+
return;
|
|
2242
|
+
terminal.pending += data;
|
|
2243
|
+
if (terminal.pending.length >= (this.#options.maxChunk ?? MAX_CHUNK))
|
|
2244
|
+
this.#flush(terminal);
|
|
2245
|
+
else
|
|
2246
|
+
terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
|
|
2247
|
+
});
|
|
2248
|
+
pty.onExit(({ exitCode, signal }) => {
|
|
2249
|
+
if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
|
|
2250
|
+
return;
|
|
2251
|
+
this.#flush(terminal);
|
|
2252
|
+
terminal.pty = undefined;
|
|
2253
|
+
terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
|
|
2254
|
+
this.#changed(terminal.owner);
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
#stop(terminal) {
|
|
2258
|
+
if (terminal.flushTimer !== undefined)
|
|
2259
|
+
clearTimeout(terminal.flushTimer);
|
|
2260
|
+
terminal.flushTimer = undefined;
|
|
2261
|
+
terminal.generation++;
|
|
2262
|
+
const pty = terminal.pty;
|
|
2263
|
+
terminal.pty = undefined;
|
|
2264
|
+
try {
|
|
2265
|
+
pty?.kill();
|
|
2266
|
+
} catch {}
|
|
2267
|
+
}
|
|
2268
|
+
#remove(terminal) {
|
|
2269
|
+
this.#stop(terminal);
|
|
2270
|
+
this.#terminals.delete(terminal.id);
|
|
2271
|
+
for (const viewer of terminal.viewers)
|
|
2272
|
+
viewer.sink.ended("Terminal closed");
|
|
2273
|
+
terminal.viewers.clear();
|
|
2274
|
+
}
|
|
2275
|
+
#resize(terminal, size) {
|
|
2276
|
+
if (terminal.cols === size.cols && terminal.rows === size.rows)
|
|
2277
|
+
return;
|
|
2278
|
+
terminal.cols = size.cols;
|
|
2279
|
+
terminal.rows = size.rows;
|
|
2280
|
+
try {
|
|
2281
|
+
terminal.pty?.resize(size.cols, size.rows);
|
|
2282
|
+
} catch {}
|
|
2283
|
+
this.#changed(terminal.owner);
|
|
2284
|
+
}
|
|
2285
|
+
#flush(terminal) {
|
|
2286
|
+
if (terminal.flushTimer !== undefined)
|
|
2287
|
+
clearTimeout(terminal.flushTimer);
|
|
2288
|
+
terminal.flushTimer = undefined;
|
|
2289
|
+
const data = terminal.pending;
|
|
2290
|
+
if (data === "")
|
|
2291
|
+
return;
|
|
2292
|
+
terminal.pending = "";
|
|
2293
|
+
terminal.buffer += data;
|
|
2294
|
+
const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
|
|
2295
|
+
if (terminal.buffer.length > bufferSize) {
|
|
2296
|
+
let cut = terminal.buffer.length - bufferSize;
|
|
2297
|
+
const line = terminal.buffer.indexOf(`
|
|
2298
|
+
`, cut);
|
|
2299
|
+
if (line !== -1 && line - cut < 1024)
|
|
2300
|
+
cut = line + 1;
|
|
2301
|
+
terminal.buffer = terminal.buffer.slice(cut);
|
|
2302
|
+
terminal.start += cut;
|
|
2303
|
+
}
|
|
2304
|
+
if (this.#readTitle(terminal, data))
|
|
2305
|
+
this.#changed(terminal.owner);
|
|
2306
|
+
for (const viewer of terminal.viewers)
|
|
2307
|
+
this.#pump(terminal, viewer);
|
|
2308
|
+
}
|
|
2309
|
+
#readTitle(terminal, data) {
|
|
2310
|
+
const text = terminal.titleCarry + data;
|
|
2311
|
+
let title;
|
|
2312
|
+
let consumed = 0;
|
|
2313
|
+
for (const match of text.matchAll(TITLE)) {
|
|
2314
|
+
title = match[1];
|
|
2315
|
+
consumed = match.index + match[0].length;
|
|
2316
|
+
}
|
|
2317
|
+
const open = text.lastIndexOf("\x1B]");
|
|
2318
|
+
terminal.titleCarry = open >= consumed && text.length - open <= MAX_TITLE ? text.slice(open) : "";
|
|
2319
|
+
if (title === undefined || title === terminal.title)
|
|
2320
|
+
return false;
|
|
2321
|
+
terminal.title = title;
|
|
2322
|
+
return terminal.rename === undefined;
|
|
2323
|
+
}
|
|
2324
|
+
async#pump(terminal, viewer) {
|
|
2325
|
+
if (viewer.sending)
|
|
2326
|
+
return;
|
|
2327
|
+
viewer.sending = true;
|
|
2328
|
+
try {
|
|
2329
|
+
const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
|
|
2330
|
+
while (terminal.viewers.has(viewer)) {
|
|
2331
|
+
const end = terminal.start + terminal.buffer.length;
|
|
2332
|
+
const reset = viewer.position === undefined || viewer.position < terminal.start;
|
|
2333
|
+
const from = reset ? terminal.start : viewer.position;
|
|
2334
|
+
if (!reset && from >= end)
|
|
2335
|
+
return;
|
|
2336
|
+
let to = reset ? end : Math.min(end, from + maxChunk);
|
|
2337
|
+
if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
|
|
2338
|
+
to--;
|
|
2339
|
+
const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
|
|
2340
|
+
viewer.position = to;
|
|
2341
|
+
await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
|
|
2342
|
+
}
|
|
2343
|
+
} finally {
|
|
2344
|
+
viewer.sending = false;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
#changed(owner) {
|
|
2348
|
+
const watchers = this.#watchers.get(owner);
|
|
2349
|
+
if (watchers === undefined)
|
|
2350
|
+
return;
|
|
2351
|
+
const list = this.list(owner);
|
|
2352
|
+
for (const watcher of watchers)
|
|
2353
|
+
watcher(list);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
async function startTerminals(options = {}) {
|
|
2357
|
+
let spawn3 = options.spawn;
|
|
2358
|
+
if (spawn3 === undefined) {
|
|
2359
|
+
try {
|
|
2360
|
+
const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
|
|
2361
|
+
if (typeof loaded.spawn !== "function")
|
|
2362
|
+
throw new Error("the module has no spawn()");
|
|
2363
|
+
spawn3 = loaded.spawn;
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2366
|
+
(options.diagnostic ?? console.error)(`Terminals unavailable: cannot load @lydell/node-pty on ${process.platform} (${process.arch}); missing @lydell/node-pty-${process.platform}-${process.arch}. Install without --omit=optional; the host continues without hello.terminals. (${detail})`);
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
return new Terminals({ ...options, spawn: spawn3, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
|
|
2371
|
+
}
|
|
2372
|
+
function loginShell() {
|
|
2373
|
+
let file = process.env.SHELL;
|
|
2374
|
+
if (file === undefined || file === "") {
|
|
2375
|
+
try {
|
|
2376
|
+
file = userInfo().shell ?? undefined;
|
|
2377
|
+
} catch {
|
|
2378
|
+
file = undefined;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
|
|
2382
|
+
}
|
|
2383
|
+
function shellEnv(env) {
|
|
2384
|
+
const result2 = {};
|
|
2385
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2386
|
+
if (value !== undefined && !key.startsWith("PINOMAD_"))
|
|
2387
|
+
result2[key] = value;
|
|
2388
|
+
}
|
|
2389
|
+
return result2;
|
|
2390
|
+
}
|
|
2391
|
+
function info(terminal) {
|
|
2392
|
+
return {
|
|
2393
|
+
id: terminal.id,
|
|
2394
|
+
name: terminal.rename ?? terminal.title ?? `Terminal ${terminal.number}`,
|
|
2395
|
+
renamed: terminal.rename !== undefined,
|
|
2396
|
+
cols: terminal.cols,
|
|
2397
|
+
rows: terminal.rows,
|
|
2398
|
+
...terminal.exit === undefined ? {} : { exit: terminal.exit }
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function isHighSurrogate(code) {
|
|
2402
|
+
return code >= 55296 && code <= 56319;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2059
2405
|
// apps/host/src/folders.ts
|
|
2060
2406
|
import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
2061
2407
|
import { isAbsolute, join as join6 } from "node:path";
|
|
@@ -3186,24 +3532,29 @@ async function attachPushSender(options) {
|
|
|
3186
3532
|
}
|
|
3187
3533
|
};
|
|
3188
3534
|
const unsubscribeConversations = conversations.subscribe(() => update());
|
|
3189
|
-
const
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
settle
|
|
3203
|
-
|
|
3535
|
+
const commits = harness.observeCommits({
|
|
3536
|
+
advance: (publication) => {
|
|
3537
|
+
for (const change of publication.changes) {
|
|
3538
|
+
if (change.type !== "document" || change.kind !== "change" || change.record.kind !== TitleDoc.definition.kind)
|
|
3539
|
+
continue;
|
|
3540
|
+
const id = change.conversationId;
|
|
3541
|
+
if (id === undefined)
|
|
3542
|
+
continue;
|
|
3543
|
+
const title = change.value.title;
|
|
3544
|
+
if (typeof title !== "string" || title === "")
|
|
3545
|
+
continue;
|
|
3546
|
+
const waiters = titleWaiters.get(id);
|
|
3547
|
+
titleWaiters.delete(id);
|
|
3548
|
+
for (const settle of waiters ?? [])
|
|
3549
|
+
settle(title);
|
|
3550
|
+
}
|
|
3551
|
+
},
|
|
3552
|
+
end: () => {}
|
|
3204
3553
|
});
|
|
3205
|
-
indexWatch?.start(async (
|
|
3206
|
-
|
|
3554
|
+
indexWatch?.start(async (call) => {
|
|
3555
|
+
if (call.kind !== "value")
|
|
3556
|
+
return;
|
|
3557
|
+
index = call.value;
|
|
3207
3558
|
update();
|
|
3208
3559
|
});
|
|
3209
3560
|
update();
|
|
@@ -3212,7 +3563,7 @@ async function attachPushSender(options) {
|
|
|
3212
3563
|
async close() {
|
|
3213
3564
|
disposed = true;
|
|
3214
3565
|
unsubscribeConversations();
|
|
3215
|
-
|
|
3566
|
+
commits.detach();
|
|
3216
3567
|
await indexWatch?.stop();
|
|
3217
3568
|
for (const item of pending)
|
|
3218
3569
|
drop(item);
|
|
@@ -3351,6 +3702,7 @@ async function startGateway(options) {
|
|
|
3351
3702
|
const clients = new Set;
|
|
3352
3703
|
let rtc;
|
|
3353
3704
|
const rtcHello = () => rtc?.hello();
|
|
3705
|
+
const terminals = await startTerminals(options.terminals);
|
|
3354
3706
|
const broadcast = (level, message) => {
|
|
3355
3707
|
for (const client of clients)
|
|
3356
3708
|
client.send({ type: "notice", level, message });
|
|
@@ -3365,10 +3717,12 @@ async function startGateway(options) {
|
|
|
3365
3717
|
}
|
|
3366
3718
|
rtc?.closeDevice(publicKey);
|
|
3367
3719
|
};
|
|
3720
|
+
const reachableRelay = () => relay?.state().status === "removed" ? undefined : relay;
|
|
3368
3721
|
const currentAdvertisement = () => {
|
|
3369
3722
|
const advertiseUrl = remote?.advertiseUrl;
|
|
3723
|
+
const relay2 = reachableRelay();
|
|
3370
3724
|
const endpoints = remoteEndpointCandidates({
|
|
3371
|
-
...
|
|
3725
|
+
...relay2 === undefined ? {} : { relay: { origin: relay2.origin, hostId: relay2.hostId } },
|
|
3372
3726
|
...advertiseUrl === undefined ? {} : { direct: { advertiseUrl } }
|
|
3373
3727
|
});
|
|
3374
3728
|
return { endpoints, ...advertiseUrl === undefined ? {} : { advertiseUrl } };
|
|
@@ -3385,6 +3739,7 @@ async function startGateway(options) {
|
|
|
3385
3739
|
const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, imageVariants, {
|
|
3386
3740
|
devicePublicKey,
|
|
3387
3741
|
rtcHello,
|
|
3742
|
+
terminals,
|
|
3388
3743
|
closed: (self) => {
|
|
3389
3744
|
clients.delete(self);
|
|
3390
3745
|
connections.delete(self);
|
|
@@ -3405,11 +3760,12 @@ async function startGateway(options) {
|
|
|
3405
3760
|
const fragment = (secret2, target2, endpointSet2) => `${pair(secret2)}&url=${encodeURIComponent(target2)}${endpointSet2 === undefined ? "" : `&endpointSet=${encodeEndpointSetParam(endpointSet2)}`}`;
|
|
3406
3761
|
const advertisement = currentAdvertisement();
|
|
3407
3762
|
const endpointSet = checkedEndpointSet(advertisement.endpoints);
|
|
3763
|
+
const relay2 = reachableRelay();
|
|
3408
3764
|
const relayReachable = endpointSet.endpoints.some((endpoint) => endpoint.role === "relay");
|
|
3409
|
-
if (relayReachable &&
|
|
3765
|
+
if (relayReachable && relay2 !== undefined) {
|
|
3410
3766
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3411
3767
|
return {
|
|
3412
|
-
url: `${
|
|
3768
|
+
url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId), endpointSet)}`,
|
|
3413
3769
|
expiresAt: expiresAt2
|
|
3414
3770
|
};
|
|
3415
3771
|
}
|
|
@@ -3422,10 +3778,10 @@ async function startGateway(options) {
|
|
|
3422
3778
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3423
3779
|
return { url: `${advertise}/#${fragment(secret2, direct.url, endpointSet)}`, expiresAt: expiresAt2 };
|
|
3424
3780
|
}
|
|
3425
|
-
if (
|
|
3781
|
+
if (relay2 !== undefined) {
|
|
3426
3782
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3427
3783
|
return {
|
|
3428
|
-
url: `${
|
|
3784
|
+
url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId))}`,
|
|
3429
3785
|
expiresAt: expiresAt2
|
|
3430
3786
|
};
|
|
3431
3787
|
}
|
|
@@ -3519,6 +3875,10 @@ async function startGateway(options) {
|
|
|
3519
3875
|
});
|
|
3520
3876
|
if (secure?.relay !== undefined) {
|
|
3521
3877
|
const relayOrigin = secure.relay.origin;
|
|
3878
|
+
const startConfig = await readServerConfig(options.dataDir).catch(() => {
|
|
3879
|
+
return;
|
|
3880
|
+
});
|
|
3881
|
+
const linkJoin = startConfig !== undefined && normalizeOrigin(startConfig.origin) === normalizeOrigin(relayOrigin) ? startConfig : undefined;
|
|
3522
3882
|
const currentBinding = async () => {
|
|
3523
3883
|
const config = await readServerConfig(options.dataDir).catch(() => {
|
|
3524
3884
|
return;
|
|
@@ -3572,6 +3932,16 @@ async function startGateway(options) {
|
|
|
3572
3932
|
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId, push }));
|
|
3573
3933
|
} else if (state.status === "connecting") {
|
|
3574
3934
|
console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
|
|
3935
|
+
} else if (state.status === "removed") {
|
|
3936
|
+
broadcast("warning", state.reason);
|
|
3937
|
+
console.error(state.reason);
|
|
3938
|
+
markRemoved(options.dataDir, {
|
|
3939
|
+
origin: relayOrigin,
|
|
3940
|
+
...linkJoin?.binding === undefined ? {} : { binding: linkJoin.binding },
|
|
3941
|
+
...linkJoin?.username === undefined ? {} : { username: linkJoin.username }
|
|
3942
|
+
}).catch((error) => {
|
|
3943
|
+
console.error(`could not write ${REMOVAL_NOTICE_FILE}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3944
|
+
});
|
|
3575
3945
|
} else {
|
|
3576
3946
|
broadcast("warning", state.reason);
|
|
3577
3947
|
console.error(state.reason);
|
|
@@ -3606,6 +3976,7 @@ async function startGateway(options) {
|
|
|
3606
3976
|
detachSettingsWatch();
|
|
3607
3977
|
await detachPush?.close();
|
|
3608
3978
|
conversations.dispose();
|
|
3979
|
+
terminals?.closeAll();
|
|
3609
3980
|
await rtc?.close();
|
|
3610
3981
|
await Promise.all([...connections].map((connection) => connection.dispose()));
|
|
3611
3982
|
for (const socket of server.clients)
|
|
@@ -3908,12 +4279,16 @@ class ConversationList {
|
|
|
3908
4279
|
#list = [];
|
|
3909
4280
|
#unsubscribe = () => {};
|
|
3910
4281
|
#notifying = false;
|
|
4282
|
+
#harness;
|
|
4283
|
+
#live = new Map;
|
|
4284
|
+
#disposed = false;
|
|
3911
4285
|
constructor(docs) {
|
|
3912
4286
|
this.#questionTokens = (docs ?? []).filter((doc) => doc.presentation === "pinomad.question").map((doc) => doc.token);
|
|
3913
4287
|
this.#questionKinds = new Set(this.#questionTokens.map((token) => token.definition.kind));
|
|
3914
4288
|
}
|
|
3915
4289
|
static async open(harness, docs) {
|
|
3916
4290
|
const list = new ConversationList(docs);
|
|
4291
|
+
list.#harness = harness;
|
|
3917
4292
|
const summaries = [];
|
|
3918
4293
|
let cursor;
|
|
3919
4294
|
const records = [];
|
|
@@ -3932,10 +4307,13 @@ class ConversationList {
|
|
|
3932
4307
|
summaries.push(list.#summaryOf(record));
|
|
3933
4308
|
}
|
|
3934
4309
|
list.#list = summaries;
|
|
3935
|
-
|
|
4310
|
+
const observation = harness.observeCommits({ advance: (publication) => list.#observe(publication), end: () => {} });
|
|
4311
|
+
list.#unsubscribe = () => observation.detach();
|
|
3936
4312
|
const touched = new Set;
|
|
3937
4313
|
list.#reindex(await harness.snapshot(IndexDoc, context3), touched);
|
|
3938
4314
|
list.#refreshSummaries(touched);
|
|
4315
|
+
for (const [id, facts] of list.#facts)
|
|
4316
|
+
list.#followLive(id, facts.running === true);
|
|
3939
4317
|
return list;
|
|
3940
4318
|
}
|
|
3941
4319
|
async#load(harness, record) {
|
|
@@ -3956,6 +4334,7 @@ class ConversationList {
|
|
|
3956
4334
|
facts.label = subagent.label;
|
|
3957
4335
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
3958
4336
|
facts.role = subagent.role;
|
|
4337
|
+
facts.model = modelRefOf(subagent?.model);
|
|
3959
4338
|
}
|
|
3960
4339
|
facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
|
|
3961
4340
|
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
@@ -3990,22 +4369,23 @@ class ConversationList {
|
|
|
3990
4369
|
facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
|
|
3991
4370
|
} else if (change.type === "document") {
|
|
3992
4371
|
const id = change.conversationId;
|
|
4372
|
+
const value = change.kind === "change" ? change.value : null;
|
|
3993
4373
|
if (id === undefined) {
|
|
3994
4374
|
if (change.record.kind === IndexDoc.definition.kind)
|
|
3995
|
-
this.#reindex(
|
|
4375
|
+
this.#reindex(value, touched);
|
|
3996
4376
|
continue;
|
|
3997
4377
|
}
|
|
3998
4378
|
const facts = this.#fact(id);
|
|
3999
4379
|
if (change.record.kind === "pi.live") {
|
|
4000
|
-
const live =
|
|
4380
|
+
const live = value;
|
|
4001
4381
|
const wasRunning = facts.running === true;
|
|
4002
4382
|
facts.running = live?.run !== undefined;
|
|
4003
4383
|
facts.tools = live?.tools;
|
|
4004
4384
|
touched.add(id);
|
|
4385
|
+
this.#followLive(id, facts.running);
|
|
4005
4386
|
if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
|
|
4006
4387
|
ended.add(id);
|
|
4007
4388
|
} else if (this.#questionKinds.has(change.record.kind)) {
|
|
4008
|
-
const value = change.value;
|
|
4009
4389
|
const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
|
|
4010
4390
|
const next = new Set(facts.pending);
|
|
4011
4391
|
if (pending)
|
|
@@ -4015,15 +4395,16 @@ class ConversationList {
|
|
|
4015
4395
|
facts.pending = next;
|
|
4016
4396
|
touched.add(id);
|
|
4017
4397
|
} else if (change.record.kind === RoleDoc.definition.kind) {
|
|
4018
|
-
facts.agent = roleNameOf(
|
|
4398
|
+
facts.agent = roleNameOf(value);
|
|
4019
4399
|
touched.add(id);
|
|
4020
4400
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
4021
|
-
const
|
|
4022
|
-
facts.label = typeof
|
|
4023
|
-
facts.role = typeof
|
|
4401
|
+
const subagent = value;
|
|
4402
|
+
facts.label = typeof subagent?.label === "string" && subagent.label !== "" ? subagent.label : undefined;
|
|
4403
|
+
facts.role = typeof subagent?.role === "string" && subagent.role !== "" ? subagent.role : undefined;
|
|
4404
|
+
facts.model = modelRefOf(subagent?.model);
|
|
4024
4405
|
touched.add(id);
|
|
4025
4406
|
} else if (change.record.kind === TitleDoc.definition.kind) {
|
|
4026
|
-
const named =
|
|
4407
|
+
const named = value?.title;
|
|
4027
4408
|
facts.named = typeof named === "string" && named !== "" ? named : undefined;
|
|
4028
4409
|
touched.add(id);
|
|
4029
4410
|
}
|
|
@@ -4098,9 +4479,51 @@ class ConversationList {
|
|
|
4098
4479
|
listener(this.#list);
|
|
4099
4480
|
});
|
|
4100
4481
|
}
|
|
4482
|
+
#followLive(id, running) {
|
|
4483
|
+
if (!running || this.#disposed) {
|
|
4484
|
+
const watch = this.#live.get(id);
|
|
4485
|
+
this.#live.delete(id);
|
|
4486
|
+
watch?.stop();
|
|
4487
|
+
return;
|
|
4488
|
+
}
|
|
4489
|
+
if (this.#live.has(id))
|
|
4490
|
+
return;
|
|
4491
|
+
this.#live.set(id, undefined);
|
|
4492
|
+
setImmediate(() => {
|
|
4493
|
+
this.#harness.watchDoc(LiveDoc, id, context3).then(async (watch) => {
|
|
4494
|
+
if (watch === undefined)
|
|
4495
|
+
return;
|
|
4496
|
+
if (this.#live.get(id) !== undefined || !this.#live.has(id)) {
|
|
4497
|
+
await watch.stop();
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4500
|
+
this.#live.set(id, watch);
|
|
4501
|
+
const apply = (live) => {
|
|
4502
|
+
const facts = this.#fact(id);
|
|
4503
|
+
if (live.run === undefined)
|
|
4504
|
+
return;
|
|
4505
|
+
facts.tools = live.tools;
|
|
4506
|
+
this.#refreshSummaries(new Set([id]));
|
|
4507
|
+
};
|
|
4508
|
+
apply(watch.value);
|
|
4509
|
+
watch.start(async (call) => {
|
|
4510
|
+
if (call.kind === "value")
|
|
4511
|
+
apply(call.value);
|
|
4512
|
+
});
|
|
4513
|
+
}, (error) => {
|
|
4514
|
+
if (this.#live.get(id) === undefined)
|
|
4515
|
+
this.#live.delete(id);
|
|
4516
|
+
console.error(`conversation ${id}: could not follow pi.live: ${error instanceof Error ? error.message : String(error)}`);
|
|
4517
|
+
});
|
|
4518
|
+
});
|
|
4519
|
+
}
|
|
4101
4520
|
dispose() {
|
|
4521
|
+
this.#disposed = true;
|
|
4102
4522
|
this.#unsubscribe();
|
|
4103
4523
|
this.#listeners.clear();
|
|
4524
|
+
for (const watch of this.#live.values())
|
|
4525
|
+
watch?.stop();
|
|
4526
|
+
this.#live.clear();
|
|
4104
4527
|
}
|
|
4105
4528
|
}
|
|
4106
4529
|
function roleNameOf(value) {
|
|
@@ -4108,6 +4531,14 @@ function roleNameOf(value) {
|
|
|
4108
4531
|
return;
|
|
4109
4532
|
return typeof value.name === "string" ? value.name : value.name === null ? null : undefined;
|
|
4110
4533
|
}
|
|
4534
|
+
function modelRefOf(value) {
|
|
4535
|
+
if (typeof value !== "string")
|
|
4536
|
+
return;
|
|
4537
|
+
const slash = value.indexOf("/");
|
|
4538
|
+
if (slash <= 0 || slash === value.length - 1)
|
|
4539
|
+
return;
|
|
4540
|
+
return { provider: value.slice(0, slash), modelId: value.slice(slash + 1) };
|
|
4541
|
+
}
|
|
4111
4542
|
function rolesOf(index) {
|
|
4112
4543
|
const roles = new Map;
|
|
4113
4544
|
for (const entry of index?.conversations ?? []) {
|
|
@@ -4138,6 +4569,8 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4138
4569
|
return {
|
|
4139
4570
|
id: record.id,
|
|
4140
4571
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
4572
|
+
...record.owner === undefined ? {} : { lifecycle: "foreground" },
|
|
4573
|
+
...facts.model === undefined ? {} : { model: facts.model },
|
|
4141
4574
|
...parent === undefined ? {} : { parent },
|
|
4142
4575
|
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
4143
4576
|
...(facts.named ?? facts.title) === undefined ? {} : { title: facts.named ?? facts.title },
|
|
@@ -4150,7 +4583,7 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4150
4583
|
};
|
|
4151
4584
|
}
|
|
4152
4585
|
function sameSummary(a, b) {
|
|
4153
|
-
return a.id === b.id && a.kind === b.kind && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
4586
|
+
return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
4154
4587
|
}
|
|
4155
4588
|
function titleOf(entry) {
|
|
4156
4589
|
const message = entry?.model?.[0];
|
|
@@ -4159,6 +4592,38 @@ function titleOf(entry) {
|
|
|
4159
4592
|
const title = stripUserBlocks(message.content).text.replace(/\s+/g, " ").trim();
|
|
4160
4593
|
return title === "" ? {} : { title };
|
|
4161
4594
|
}
|
|
4595
|
+
var WHOLE_TRANSCRIPT = { min: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
|
|
4596
|
+
var TURN_STARTS = new Set(["pi.user", "pi.compaction", "pi.reset"]);
|
|
4597
|
+
async function turnPage(conversation, before, limit) {
|
|
4598
|
+
const want = Math.min(limit, MAX_TRANSCRIPT_PAGE);
|
|
4599
|
+
const done = (taken2) => taken2.length >= MAX_TRANSCRIPT_PAGE || taken2.length >= want && TURN_STARTS.has(taken2.at(-1).kind);
|
|
4600
|
+
const query = { order: "descending", ...before === undefined ? {} : { maxEntryId: before } };
|
|
4601
|
+
const taken = [];
|
|
4602
|
+
let cursor;
|
|
4603
|
+
for (;; ) {
|
|
4604
|
+
const page = await conversation.entries(query, Math.max(want + 1 - taken.length, 64), cursor, context3);
|
|
4605
|
+
for (const entry of page.items) {
|
|
4606
|
+
if (done(taken))
|
|
4607
|
+
return { entries: taken.reverse(), before: entry.id };
|
|
4608
|
+
taken.push(entry);
|
|
4609
|
+
}
|
|
4610
|
+
if (page.next === undefined)
|
|
4611
|
+
return { entries: taken.reverse() };
|
|
4612
|
+
cursor = page.next;
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
async function viewWindow(conversation, window) {
|
|
4616
|
+
if (window === undefined)
|
|
4617
|
+
return WHOLE_TRANSCRIPT;
|
|
4618
|
+
const from = window.from ?? await startOf(conversation, window.limit);
|
|
4619
|
+
return from === undefined ? WHOLE_TRANSCRIPT : { ...WHOLE_TRANSCRIPT, from };
|
|
4620
|
+
}
|
|
4621
|
+
async function startOf(conversation, limit) {
|
|
4622
|
+
const page = await turnPage(conversation, undefined, limit);
|
|
4623
|
+
return page.before === undefined ? undefined : page.entries[0].id;
|
|
4624
|
+
}
|
|
4625
|
+
var shownOps = (call) => isGone(call) ? [["r", null]] : call.ops;
|
|
4626
|
+
var isGone = (call) => call.retired && !call.ops.some((op) => op[0] === "r");
|
|
4162
4627
|
async function historyFacts(harness, id) {
|
|
4163
4628
|
const conversation = await harness.conversation(id, context3);
|
|
4164
4629
|
let first;
|
|
@@ -4293,6 +4758,7 @@ class GatewayClient {
|
|
|
4293
4758
|
}
|
|
4294
4759
|
#hostValues;
|
|
4295
4760
|
#subscriptions = new Map;
|
|
4761
|
+
#terminalSizes = new Map;
|
|
4296
4762
|
#disposed = false;
|
|
4297
4763
|
constructor(connection, options, conversations, files, skills, imageVariants, hooks) {
|
|
4298
4764
|
this.#connection = connection;
|
|
@@ -4355,7 +4821,8 @@ class GatewayClient {
|
|
|
4355
4821
|
})),
|
|
4356
4822
|
toolPresentations: options.toolPresentations ?? {},
|
|
4357
4823
|
endpointSet: this.#hooks.endpointSet(),
|
|
4358
|
-
...rtc === undefined ? {} : { rtc }
|
|
4824
|
+
...rtc === undefined ? {} : { rtc },
|
|
4825
|
+
...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } }
|
|
4359
4826
|
});
|
|
4360
4827
|
}
|
|
4361
4828
|
send(frame) {
|
|
@@ -4368,15 +4835,19 @@ class GatewayClient {
|
|
|
4368
4835
|
}
|
|
4369
4836
|
async#receive(frame) {
|
|
4370
4837
|
if (frame.type === "subscribe")
|
|
4371
|
-
await this.#subscribe(frame.stream, frame.images === "ref");
|
|
4838
|
+
await this.#subscribe(frame.stream, frame.images === "ref", frame.since, frame.window);
|
|
4372
4839
|
else if (frame.type === "unsubscribe")
|
|
4373
4840
|
await this.#unsubscribe(frame.stream);
|
|
4374
4841
|
else
|
|
4375
4842
|
await this.#answer(frame.id, frame.method, frame.args);
|
|
4376
4843
|
}
|
|
4377
|
-
async#subscribe(stream, imageRefs) {
|
|
4844
|
+
async#subscribe(stream, imageRefs, since, window) {
|
|
4378
4845
|
const previous = this.#subscriptions.get(stream);
|
|
4379
|
-
const state =
|
|
4846
|
+
const state = {
|
|
4847
|
+
...imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {},
|
|
4848
|
+
...since === undefined ? {} : { since },
|
|
4849
|
+
...window === undefined ? {} : { window }
|
|
4850
|
+
};
|
|
4380
4851
|
this.#subscriptions.set(stream, state);
|
|
4381
4852
|
const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
|
|
4382
4853
|
const send = (frame) => current() ? this.send(frame) : Promise.resolve();
|
|
@@ -4420,12 +4891,30 @@ class GatewayClient {
|
|
|
4420
4891
|
}
|
|
4421
4892
|
if (stream === "tasks")
|
|
4422
4893
|
return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
|
|
4894
|
+
if (stream.startsWith("terminals:")) {
|
|
4895
|
+
const terminals = this.#terminals();
|
|
4896
|
+
const conversation2 = await this.#conversation(Number(stream.slice("terminals:".length)));
|
|
4897
|
+
const { owner } = await this.#terminalOwner(conversation2);
|
|
4898
|
+
return { stop: terminals.watch(owner, (value) => void send({ type: "snapshot", stream, value })) };
|
|
4899
|
+
}
|
|
4900
|
+
if (stream.startsWith("terminal:")) {
|
|
4901
|
+
const terminalStream = stream;
|
|
4902
|
+
const detach = this.#terminals().attach(terminalStream.slice("terminal:".length), state?.since, {
|
|
4903
|
+
output: (frame) => send({ type: "output", stream: terminalStream, ...frame }),
|
|
4904
|
+
ended: (reason) => {
|
|
4905
|
+
send({ type: "ended", stream, reason });
|
|
4906
|
+
if (this.#subscriptions.get(stream) === state)
|
|
4907
|
+
this.#subscriptions.delete(stream);
|
|
4908
|
+
}
|
|
4909
|
+
});
|
|
4910
|
+
return { stop: detach };
|
|
4911
|
+
}
|
|
4423
4912
|
if (stream.startsWith("doc:")) {
|
|
4424
4913
|
const at = stream.lastIndexOf(":");
|
|
4425
4914
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
4426
4915
|
}
|
|
4427
4916
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
4428
|
-
return this.#forward(stream, await conversation.watch(context3), send, state);
|
|
4917
|
+
return this.#forward(stream, await conversation.watch(context3, await viewWindow(conversation, state?.window)), send, state);
|
|
4429
4918
|
}
|
|
4430
4919
|
#hostValue(stream) {
|
|
4431
4920
|
return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
|
|
@@ -4443,13 +4932,17 @@ class GatewayClient {
|
|
|
4443
4932
|
});
|
|
4444
4933
|
let inner;
|
|
4445
4934
|
let stopped = false;
|
|
4446
|
-
const
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4935
|
+
const observation = harness.observeCommits({
|
|
4936
|
+
advance: (publication) => {
|
|
4937
|
+
const matches = publication.changes.some((change) => (change.type === "document" || change.type === "document.copy") && change.record.kind === kind && change.conversationId === id);
|
|
4938
|
+
if (!matches)
|
|
4939
|
+
return;
|
|
4940
|
+
unsubscribe();
|
|
4941
|
+
setImmediate(() => signalCreation());
|
|
4942
|
+
},
|
|
4943
|
+
end: () => {}
|
|
4452
4944
|
});
|
|
4945
|
+
const unsubscribe = () => observation.detach();
|
|
4453
4946
|
try {
|
|
4454
4947
|
const existing = await harness.watchDoc(token, id, context3);
|
|
4455
4948
|
if (existing !== undefined) {
|
|
@@ -4490,16 +4983,19 @@ class GatewayClient {
|
|
|
4490
4983
|
async#forward(stream, watch, send, state) {
|
|
4491
4984
|
if (state?.images === undefined) {
|
|
4492
4985
|
await send({ type: "snapshot", stream, value: watch.value });
|
|
4493
|
-
watch.start((
|
|
4986
|
+
watch.start((call) => call.kind === "value" ? send({ type: "ops", stream, ops: shownOps(call) }) : Promise.resolve());
|
|
4494
4987
|
return watch;
|
|
4495
4988
|
}
|
|
4496
4989
|
const images = state.images;
|
|
4497
4990
|
let before = watch.value;
|
|
4498
4991
|
state.view = before;
|
|
4499
4992
|
await send({ type: "snapshot", stream, value: await images.value(before) });
|
|
4500
|
-
watch.start(async (
|
|
4993
|
+
watch.start(async (call) => {
|
|
4994
|
+
if (call.kind !== "value")
|
|
4995
|
+
return;
|
|
4996
|
+
const ops = shownOps(call);
|
|
4501
4997
|
const sent = await images.ops(ops, before);
|
|
4502
|
-
before = value;
|
|
4998
|
+
before = isGone(call) ? null : call.value;
|
|
4503
4999
|
state.view = before;
|
|
4504
5000
|
return send({ type: "ops", stream, ops: sent });
|
|
4505
5001
|
});
|
|
@@ -4509,7 +5005,7 @@ class GatewayClient {
|
|
|
4509
5005
|
for (const state of this.#subscriptions.values()) {
|
|
4510
5006
|
if (state.images === undefined || state.view === undefined)
|
|
4511
5007
|
continue;
|
|
4512
|
-
const image = state.images.image(state.view, ref);
|
|
5008
|
+
const image = state.images.image(state.view, ref) ?? (state.earlier === undefined ? undefined : state.images.image({ entries: state.earlier }, ref));
|
|
4513
5009
|
if (image !== undefined)
|
|
4514
5010
|
return image;
|
|
4515
5011
|
}
|
|
@@ -4569,10 +5065,43 @@ class GatewayClient {
|
|
|
4569
5065
|
await this.send({ type: "result", id, ok: true, value: value ?? null });
|
|
4570
5066
|
} catch (error) {
|
|
4571
5067
|
await this.send({ type: "result", id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
5068
|
+
return;
|
|
5069
|
+
}
|
|
5070
|
+
const device = this.devicePublicKey;
|
|
5071
|
+
if (method === "revokeDevice" && device !== undefined && args.publicKey === device) {
|
|
5072
|
+
this.#hooks.closeDevice(device);
|
|
4572
5073
|
}
|
|
4573
5074
|
}
|
|
4574
5075
|
async#call(method, args) {
|
|
4575
5076
|
switch (method) {
|
|
5077
|
+
case "terminalInput": {
|
|
5078
|
+
const input = args;
|
|
5079
|
+
this.#terminals().input(input.terminalId, input.data, this.#terminalSizes.get(input.terminalId));
|
|
5080
|
+
return null;
|
|
5081
|
+
}
|
|
5082
|
+
case "resizeTerminal": {
|
|
5083
|
+
const input = args;
|
|
5084
|
+
const size = { cols: input.cols, rows: input.rows };
|
|
5085
|
+
this.#terminals().resize(input.terminalId, size);
|
|
5086
|
+
this.#terminalSizes.set(input.terminalId, size);
|
|
5087
|
+
return null;
|
|
5088
|
+
}
|
|
5089
|
+
case "renameTerminal": {
|
|
5090
|
+
const input = args;
|
|
5091
|
+
this.#terminals().rename(input.terminalId, input.name);
|
|
5092
|
+
return null;
|
|
5093
|
+
}
|
|
5094
|
+
case "restartTerminal": {
|
|
5095
|
+
const input = args;
|
|
5096
|
+
const size = { cols: input.cols, rows: input.rows };
|
|
5097
|
+
this.#terminals().restart(input.terminalId, size);
|
|
5098
|
+
this.#terminalSizes.set(input.terminalId, size);
|
|
5099
|
+
return null;
|
|
5100
|
+
}
|
|
5101
|
+
case "closeTerminal":
|
|
5102
|
+
this.#terminals().close(args.terminalId);
|
|
5103
|
+
this.#terminalSizes.delete(args.terminalId);
|
|
5104
|
+
return null;
|
|
4576
5105
|
case "addProject":
|
|
4577
5106
|
return addProject(this.#options.harness, args.path, context3);
|
|
4578
5107
|
case "removeProject":
|
|
@@ -4636,6 +5165,7 @@ class GatewayClient {
|
|
|
4636
5165
|
const input = args;
|
|
4637
5166
|
await archive(this.#options.harness, input.conversationId, input.archived, context3);
|
|
4638
5167
|
if (input.archived) {
|
|
5168
|
+
this.#hooks.terminals?.closeOwners(await conversationSubtree(this.#options.harness, input.conversationId, context3));
|
|
4639
5169
|
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
|
|
4640
5170
|
for (const { record, reason } of kept) {
|
|
4641
5171
|
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
@@ -4652,7 +5182,8 @@ class GatewayClient {
|
|
|
4652
5182
|
case "revokeDevice": {
|
|
4653
5183
|
const { publicKey } = args;
|
|
4654
5184
|
await revokeDevice(this.#options.harness, publicKey, context3);
|
|
4655
|
-
this
|
|
5185
|
+
if (publicKey !== this.devicePublicKey)
|
|
5186
|
+
this.#hooks.closeDevice(publicKey);
|
|
4656
5187
|
return null;
|
|
4657
5188
|
}
|
|
4658
5189
|
case "rtcOffer": {
|
|
@@ -4738,6 +5269,26 @@ class GatewayClient {
|
|
|
4738
5269
|
const input = args;
|
|
4739
5270
|
return listSkills(this.#options.harness, this.#skills, input.scope, context3);
|
|
4740
5271
|
}
|
|
5272
|
+
case "earlierEntries": {
|
|
5273
|
+
const { conversationId, before, limit } = args;
|
|
5274
|
+
const stream = `conversation:${conversationId}`;
|
|
5275
|
+
const state = this.#subscriptions.get(stream);
|
|
5276
|
+
if (state === undefined)
|
|
5277
|
+
throw new Error(`Not subscribed to conversation ${conversationId}`);
|
|
5278
|
+
const replaced = () => this.#subscriptions.get(stream) !== state;
|
|
5279
|
+
const resubscribed = () => new Error(`Conversation ${conversationId} was resubscribed while its page loaded`);
|
|
5280
|
+
const page = await turnPage(await this.#conversation(conversationId), before, limit);
|
|
5281
|
+
if (replaced())
|
|
5282
|
+
throw resubscribed();
|
|
5283
|
+
if (state.images === undefined)
|
|
5284
|
+
return page;
|
|
5285
|
+
const raw = page.entries;
|
|
5286
|
+
const { entries } = await state.images.value({ entries: raw });
|
|
5287
|
+
if (replaced())
|
|
5288
|
+
throw resubscribed();
|
|
5289
|
+
state.earlier = [...state.earlier ?? [], ...raw];
|
|
5290
|
+
return { ...page, entries };
|
|
5291
|
+
}
|
|
4741
5292
|
case "image": {
|
|
4742
5293
|
const input = args;
|
|
4743
5294
|
const image = this.#shownImage(input.ref);
|
|
@@ -4799,6 +5350,37 @@ class GatewayClient {
|
|
|
4799
5350
|
return { outcome: "steered" };
|
|
4800
5351
|
}, context3);
|
|
4801
5352
|
}
|
|
5353
|
+
case "reorderQueued": {
|
|
5354
|
+
const { conversationId, submissionId, before } = args;
|
|
5355
|
+
return await this.#options.harness.commit(async (tx) => {
|
|
5356
|
+
const queuedState = async (id) => {
|
|
5357
|
+
const record = await tx.submission(id);
|
|
5358
|
+
if (record === undefined)
|
|
5359
|
+
throw new Error(`Submission ${id} does not exist`);
|
|
5360
|
+
assertQueuedInput(record, conversationId);
|
|
5361
|
+
if (record.entry !== undefined)
|
|
5362
|
+
return "sent";
|
|
5363
|
+
return record.status === "queued" ? "queued" : "unavailable";
|
|
5364
|
+
};
|
|
5365
|
+
const moved = await queuedState(submissionId);
|
|
5366
|
+
if (moved !== "queued")
|
|
5367
|
+
return { outcome: moved };
|
|
5368
|
+
if (before !== undefined && await queuedState(before) !== "queued") {
|
|
5369
|
+
return { outcome: "unavailable" };
|
|
5370
|
+
}
|
|
5371
|
+
if (before === submissionId)
|
|
5372
|
+
return { outcome: "moved" };
|
|
5373
|
+
const items = (await tx.doc(InboxDoc, conversationId)).items;
|
|
5374
|
+
const from = items.findIndex((candidate) => candidate.id === submissionId);
|
|
5375
|
+
if (from < 0 || before !== undefined && !items.some((candidate) => candidate.id === before)) {
|
|
5376
|
+
return { outcome: "unavailable" };
|
|
5377
|
+
}
|
|
5378
|
+
const [item] = items.splice(from, 1);
|
|
5379
|
+
const to = before === undefined ? items.findLastIndex((candidate) => candidate.mode !== "write") + 1 : items.findIndex((candidate) => candidate.id === before);
|
|
5380
|
+
items.splice(to, 0, item);
|
|
5381
|
+
return { outcome: "moved" };
|
|
5382
|
+
}, context3);
|
|
5383
|
+
}
|
|
4802
5384
|
case "renameConversation": {
|
|
4803
5385
|
const { title } = args;
|
|
4804
5386
|
const trimmed = title.trim();
|
|
@@ -4935,8 +5517,34 @@ class GatewayClient {
|
|
|
4935
5517
|
case "changes": {
|
|
4936
5518
|
return await this.#changes(conversation, context3);
|
|
4937
5519
|
}
|
|
5520
|
+
case "openTerminal": {
|
|
5521
|
+
const input = args;
|
|
5522
|
+
const terminals = this.#terminals();
|
|
5523
|
+
const { owner, cwd, record } = await this.#terminalOwner(conversation);
|
|
5524
|
+
const ready = record === undefined || await worktreeSettled(record);
|
|
5525
|
+
if (!ready || !await stat6(cwd).then((info2) => info2.isDirectory(), () => false)) {
|
|
5526
|
+
throw new Error(`The checkout is unavailable: ${cwd}`);
|
|
5527
|
+
}
|
|
5528
|
+
const size = { cols: input.cols, rows: input.rows };
|
|
5529
|
+
const terminalId = terminals.open(owner, cwd, size);
|
|
5530
|
+
this.#terminalSizes.set(terminalId, size);
|
|
5531
|
+
return { terminalId };
|
|
5532
|
+
}
|
|
4938
5533
|
}
|
|
4939
5534
|
}
|
|
5535
|
+
#terminals() {
|
|
5536
|
+
if (this.#hooks.terminals === undefined)
|
|
5537
|
+
throw new Error("Terminals are unavailable on this host");
|
|
5538
|
+
return this.#hooks.terminals;
|
|
5539
|
+
}
|
|
5540
|
+
async#terminalOwner(conversation) {
|
|
5541
|
+
const cwd = (await conversation.agent(context3)).cwd;
|
|
5542
|
+
if (cwd === undefined)
|
|
5543
|
+
throw new Error("No working directory");
|
|
5544
|
+
const record = checkoutAt((await this.#options.harness.snapshot(IndexDoc, context3))?.checkouts, cwd);
|
|
5545
|
+
const owner = record?.conversationId ?? await familyRoot(this.#options.harness, conversation.id, context3);
|
|
5546
|
+
return { owner, cwd, record };
|
|
5547
|
+
}
|
|
4940
5548
|
async#expandSkills(text, scope, bound) {
|
|
4941
5549
|
if (bound === undefined ? !text.includes("$") : bound.length === 0)
|
|
4942
5550
|
return text;
|
|
@@ -5524,7 +6132,7 @@ import {
|
|
|
5524
6132
|
readdir as readdir3,
|
|
5525
6133
|
rename as rename2,
|
|
5526
6134
|
rm as rm3,
|
|
5527
|
-
stat as
|
|
6135
|
+
stat as stat7,
|
|
5528
6136
|
writeFile as writeFile2
|
|
5529
6137
|
} from "node:fs/promises";
|
|
5530
6138
|
import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
|
|
@@ -5713,7 +6321,7 @@ class AttachmentStore {
|
|
|
5713
6321
|
throw new Error(problem);
|
|
5714
6322
|
const decoded = base64DecodedLength(data);
|
|
5715
6323
|
const path = this.#dataPath(meta);
|
|
5716
|
-
const received = (await
|
|
6324
|
+
const received = (await stat7(path)).size;
|
|
5717
6325
|
if (received === meta.size) {
|
|
5718
6326
|
await chmod(path, 292);
|
|
5719
6327
|
await this.#writeMeta({ ...meta, state: "complete" });
|
|
@@ -5756,13 +6364,13 @@ class AttachmentStore {
|
|
|
5756
6364
|
if (meta.state === "uploading")
|
|
5757
6365
|
throw new Error(`Attachment ${index + 1} is still uploading`);
|
|
5758
6366
|
const path = this.#dataPath(meta);
|
|
5759
|
-
let
|
|
6367
|
+
let info2;
|
|
5760
6368
|
try {
|
|
5761
|
-
|
|
6369
|
+
info2 = await lstat(path);
|
|
5762
6370
|
} catch {
|
|
5763
6371
|
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
5764
6372
|
}
|
|
5765
|
-
if (!
|
|
6373
|
+
if (!info2.isFile() || info2.size !== meta.size) {
|
|
5766
6374
|
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
5767
6375
|
}
|
|
5768
6376
|
checked.push({ meta, path });
|
|
@@ -5805,7 +6413,7 @@ class AttachmentStore {
|
|
|
5805
6413
|
} else {
|
|
5806
6414
|
this.#pending.delete(entry.name);
|
|
5807
6415
|
try {
|
|
5808
|
-
if (now - (await
|
|
6416
|
+
if (now - (await stat7(directory)).mtimeMs <= this.#ttlMs)
|
|
5809
6417
|
continue;
|
|
5810
6418
|
} catch {
|
|
5811
6419
|
continue;
|
|
@@ -6004,6 +6612,7 @@ async function openHost(options) {
|
|
|
6004
6612
|
}
|
|
6005
6613
|
},
|
|
6006
6614
|
...options.rtc === undefined ? {} : { rtc: options.rtc },
|
|
6615
|
+
...options.terminals === undefined ? {} : { terminals: options.terminals },
|
|
6007
6616
|
secure: {
|
|
6008
6617
|
hostKey,
|
|
6009
6618
|
offers,
|
|
@@ -6085,7 +6694,7 @@ async function openHost(options) {
|
|
|
6085
6694
|
}
|
|
6086
6695
|
|
|
6087
6696
|
// apps/host/src/service/log-rotation.ts
|
|
6088
|
-
import { copyFile, rename as rename3, rm as rm4, stat as
|
|
6697
|
+
import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
|
|
6089
6698
|
import { join as join11 } from "node:path";
|
|
6090
6699
|
var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
6091
6700
|
var HOST_LOG_HISTORY = 3;
|
|
@@ -6099,7 +6708,7 @@ async function rotateHostLog(dataDir, copy = copyFile) {
|
|
|
6099
6708
|
await rm4(temp, { force: true });
|
|
6100
6709
|
let size;
|
|
6101
6710
|
try {
|
|
6102
|
-
size = (await
|
|
6711
|
+
size = (await stat8(log)).size;
|
|
6103
6712
|
} catch (error) {
|
|
6104
6713
|
if (isMissing(error))
|
|
6105
6714
|
return false;
|
|
@@ -6147,7 +6756,7 @@ function startHostLogRotation(dataDir) {
|
|
|
6147
6756
|
}
|
|
6148
6757
|
|
|
6149
6758
|
// apps/host/src/model-auth.ts
|
|
6150
|
-
import { randomUUID as
|
|
6759
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
6151
6760
|
import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6152
6761
|
import { dirname as dirname3 } from "node:path";
|
|
6153
6762
|
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
@@ -6430,6 +7039,45 @@ async function openModelAuth(runtime, options) {
|
|
|
6430
7039
|
tail = next.catch(() => {});
|
|
6431
7040
|
return next;
|
|
6432
7041
|
};
|
|
7042
|
+
const opened = await readModelsFile(options.modelsPath);
|
|
7043
|
+
Promise.all([...custom].flatMap(([id, endpoint]) => {
|
|
7044
|
+
const entries = opened.providers[id]?.models;
|
|
7045
|
+
if (!Array.isArray(entries) || !entries.some(lacksLimits))
|
|
7046
|
+
return [];
|
|
7047
|
+
return [
|
|
7048
|
+
(async () => {
|
|
7049
|
+
const key = (await runtime.getAuth(id))?.auth.apiKey;
|
|
7050
|
+
const listed = await fetchEndpointModels(endpoint.api, endpoint.baseUrl, key).catch(() => []);
|
|
7051
|
+
return { id, endpoint, listed: new Map(listed.map((model) => [model.id, model])) };
|
|
7052
|
+
})()
|
|
7053
|
+
];
|
|
7054
|
+
})).then((answers) => serial(async () => {
|
|
7055
|
+
if (closed)
|
|
7056
|
+
return;
|
|
7057
|
+
const filled = new Set;
|
|
7058
|
+
const fill = (target) => {
|
|
7059
|
+
for (const { id, endpoint, listed } of answers) {
|
|
7060
|
+
const entry = target.providers[id];
|
|
7061
|
+
if (entry?.api !== endpoint.api || entry.baseUrl !== endpoint.baseUrl || !Array.isArray(entry.models))
|
|
7062
|
+
continue;
|
|
7063
|
+
for (const model of entry.models) {
|
|
7064
|
+
const limits = listed.get(model.id);
|
|
7065
|
+
for (const field of LIMITS) {
|
|
7066
|
+
if (field in model || limits?.[field] === undefined)
|
|
7067
|
+
continue;
|
|
7068
|
+
model[field] = limits[field];
|
|
7069
|
+
filled.add(id);
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
7073
|
+
};
|
|
7074
|
+
fill(await readModelsFile(options.modelsPath));
|
|
7075
|
+
if (filled.size === 0)
|
|
7076
|
+
return;
|
|
7077
|
+
await updateModelsFile(fill);
|
|
7078
|
+
await runtime.refresh({ providers: [...filled], allowNetwork: false });
|
|
7079
|
+
await republish();
|
|
7080
|
+
}));
|
|
6433
7081
|
return {
|
|
6434
7082
|
models,
|
|
6435
7083
|
auth,
|
|
@@ -6451,7 +7099,7 @@ async function openModelAuth(runtime, options) {
|
|
|
6451
7099
|
if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
|
|
6452
7100
|
throw new Error(`A login for ${provider.name} is already in progress`);
|
|
6453
7101
|
}
|
|
6454
|
-
const id =
|
|
7102
|
+
const id = randomUUID3();
|
|
6455
7103
|
const login = {
|
|
6456
7104
|
flow: { id, providerId, method, events: [], state: "running" },
|
|
6457
7105
|
controller: new AbortController,
|
|
@@ -6635,6 +7283,8 @@ async function fetchEndpointModels(api, baseUrl, key) {
|
|
|
6635
7283
|
throw new Error(`${url.host} listed no models`);
|
|
6636
7284
|
return models;
|
|
6637
7285
|
}
|
|
7286
|
+
var LIMITS = ["contextWindow", "maxTokens"];
|
|
7287
|
+
var lacksLimits = (model) => typeof model === "object" && model !== null && LIMITS.some((field) => !(field in model));
|
|
6638
7288
|
var positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
6639
7289
|
function endpointModel(id, reported) {
|
|
6640
7290
|
return {
|
|
@@ -6795,7 +7445,7 @@ async function main(argv) {
|
|
|
6795
7445
|
process.on("SIGINT", stop);
|
|
6796
7446
|
process.on("SIGTERM", stop);
|
|
6797
7447
|
host.closed.then((end) => {
|
|
6798
|
-
if (end.
|
|
7448
|
+
if (end.kind !== "failed" || stopping)
|
|
6799
7449
|
return;
|
|
6800
7450
|
stopping = true;
|
|
6801
7451
|
stopLogRotation();
|
|
@@ -6826,7 +7476,7 @@ async function installId(dataDir) {
|
|
|
6826
7476
|
const path = join12(dataDir, "install-id");
|
|
6827
7477
|
let id = (await readFile7(path, "utf8").catch(() => "")).trim();
|
|
6828
7478
|
if (id === "") {
|
|
6829
|
-
id =
|
|
7479
|
+
id = randomUUID4();
|
|
6830
7480
|
await mkdir6(dataDir, { recursive: true });
|
|
6831
7481
|
await writeFile5(path, `${id}
|
|
6832
7482
|
`, { mode: 384 });
|