pinomad 0.12.0 → 0.14.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.
Files changed (47) hide show
  1. package/dist/chunk-0dh2k3er.js +43 -0
  2. package/dist/{chunk-47ff1d1t.js → chunk-1mjbt2z2.js} +191 -11
  3. package/dist/{chunk-7wj0jh3t.js → chunk-1rdbnvy6.js} +1 -1
  4. package/dist/{chunk-ksfxqd8s.js → chunk-35szq73k.js} +2682 -459
  5. package/dist/{chunk-mnmqyq7d.js → chunk-5j7ybhjj.js} +1984 -333
  6. package/dist/{chunk-agq4tn9r.js → chunk-5pv0g1nj.js} +12 -10
  7. package/dist/{chunk-7mn38n4f.js → chunk-63ymn5c9.js} +2 -2
  8. package/dist/{chunk-nppgxh6c.js → chunk-6j6hkfbj.js} +8 -4
  9. package/dist/{chunk-pqe7q1s7.js → chunk-6vbp1vva.js} +2 -1
  10. package/dist/{chunk-g3ahp7b9.js → chunk-9trbbz4r.js} +1 -1
  11. package/dist/{chunk-gg2dcqbg.js → chunk-b0nwq1dt.js} +29 -12
  12. package/dist/chunk-dsr19q5h.js +23 -0
  13. package/dist/{chunk-41q0saz0.js → chunk-e53c0s8z.js} +9 -7
  14. package/dist/{chunk-np4jd070.js → chunk-e59k3bmz.js} +307 -56
  15. package/dist/chunk-eerpgvy9.js +315 -0
  16. package/dist/chunk-eewxy4cp.js +240 -0
  17. package/dist/{chunk-vqkb70d7.js → chunk-evbv1fkn.js} +3 -3
  18. package/dist/{chunk-vx7dy07y.js → chunk-ex5yr09h.js} +11 -6
  19. package/dist/{chunk-t91zt5f0.js → chunk-hg9gs396.js} +1 -1
  20. package/dist/{chunk-aqj9wfmb.js → chunk-mtkvft88.js} +1 -1
  21. package/dist/{chunk-t5abqsva.js → chunk-mxwcnnkg.js} +15 -305
  22. package/dist/{chunk-5qf1grkk.js → chunk-w927q9m1.js} +52 -16
  23. package/dist/pinomad.js +12 -12
  24. package/package.json +16 -11
  25. package/web/assets/{core-DnpzcQzs.js → core-BWHGsFpV.js} +1 -1
  26. package/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  27. package/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  28. package/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  29. package/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  30. package/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  31. package/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  32. package/web/assets/{highlighted-diff-Dn5ifsvK.js → highlighted-diff-DJ7KZCpS.js} +2 -2
  33. package/web/assets/{highlights-DnHv6NBM.js → highlights-CyEmo6OI.js} +1 -1
  34. package/web/assets/{highlights-themes-CUdrlZk3.js → highlights-themes-CSNkBojy.js} +1 -1
  35. package/web/assets/index-C_qkbyL7.css +1 -0
  36. package/web/assets/index-DefQtCI2.js +396 -0
  37. package/web/assets/{shiki-BuPP5HCg.js → shiki-BzCmRG-v.js} +2 -2
  38. package/web/assets/{shiki-langs-FJk0v0Ga.js → shiki-langs-DOkjyV4d.js} +1 -1
  39. package/web/assets/{stream-tokenizer-Bl6bisIa.js → stream-tokenizer-CBPqFe4Z.js} +1 -1
  40. package/web/assets/{theming-themes-BL_CC-Mm.js → theming-themes-wKVca0aB.js} +1 -1
  41. package/web/assets/{xterm-view-ZsuzJb2C.js → xterm-view-CKa1-QCM.js} +12 -12
  42. package/web/index.html +3 -2
  43. package/web/theme-boot.js +11 -0
  44. package/dist/chunk-42nqtx5r.js +0 -22
  45. package/dist/chunk-jeqsdcj1.js +0 -96
  46. package/web/assets/index-BV0ecolo.css +0 -1
  47. package/web/assets/index-DNsTTgPE.js +0 -396
@@ -5,7 +5,7 @@ import {
5
5
  openHostSettings,
6
6
  sameList,
7
7
  settingsDefaults
8
- } from "./chunk-nppgxh6c.js";
8
+ } from "./chunk-6j6hkfbj.js";
9
9
  import {
10
10
  DevicesDoc,
11
11
  RevocationsDoc,
@@ -14,7 +14,7 @@ import {
14
14
  loadHostKey,
15
15
  loadRelayKey,
16
16
  pairingOffers
17
- } from "./chunk-t91zt5f0.js";
17
+ } from "./chunk-hg9gs396.js";
18
18
  import {
19
19
  REMOVAL_NOTICE_FILE,
20
20
  markRemoved,
@@ -30,18 +30,34 @@ import {
30
30
  relayHostId,
31
31
  relayWsBase,
32
32
  startRelayLink
33
- } from "./chunk-g3ahp7b9.js";
33
+ } from "./chunk-9trbbz4r.js";
34
+ import {
35
+ installCommand,
36
+ npmBeside,
37
+ outputTail,
38
+ packagePrefix,
39
+ parseRelease,
40
+ viewCommand
41
+ } from "./chunk-0dh2k3er.js";
42
+ import {
43
+ serviceManager
44
+ } from "./chunk-mxwcnnkg.js";
34
45
  import {
35
46
  ATTACHMENT_TTL_MS,
47
+ HOST_UPGRADING_ERROR,
36
48
  INVALID_FRAME_CLOSE_CODE,
37
49
  MAX_ATTACHMENTS_PER_MESSAGE,
38
50
  MAX_ATTACHMENT_NAME_LENGTH,
39
51
  MAX_PENDING_ATTACHMENTS,
40
52
  MAX_PENDING_ATTACHMENT_BYTES,
53
+ MAX_TRANSCRIPT_PAGE,
41
54
  PROTOCOL,
55
+ QuestionItemSchema,
56
+ QuestionSchema,
42
57
  RTC_DATA_CHANNEL_LABEL,
43
58
  RTC_DEFAULT_STUN,
44
59
  RTC_MAX_RECORD_BYTES,
60
+ TodoItemSchema,
45
61
  UNAUTHORIZED_CLOSE_CODE,
46
62
  attachmentBeginProblem,
47
63
  attachmentBlock,
@@ -49,6 +65,7 @@ import {
49
65
  base64DecodedLength,
50
66
  isAttachmentId,
51
67
  isCustomProviderApi,
68
+ isProcessActive,
52
69
  organize,
53
70
  parseAttachmentBlock,
54
71
  parseClientFrame,
@@ -56,7 +73,7 @@ import {
56
73
  rtcCandidateType,
57
74
  sdpCandidates,
58
75
  withRtcCandidates
59
- } from "./chunk-47ff1d1t.js";
76
+ } from "./chunk-1mjbt2z2.js";
60
77
  import {
61
78
  ENDPOINT_SET_SCHEMA_VERSION,
62
79
  MAX_HOST_ENDPOINT_URL_LENGTH,
@@ -72,29 +89,348 @@ import {
72
89
  parseEndpointSet,
73
90
  respondIK,
74
91
  toBase64Url
75
- } from "./chunk-pqe7q1s7.js";
92
+ } from "./chunk-6vbp1vva.js";
76
93
  import {
77
94
  DEFAULT_DATA_DIR
78
95
  } from "./chunk-ngf1zatm.js";
79
96
  import {
80
97
  packagedVersion,
81
98
  webRoot
82
- } from "./chunk-7wj0jh3t.js";
99
+ } from "./chunk-1rdbnvy6.js";
83
100
  import {
84
101
  __require
85
102
  } from "./chunk-df32avw6.js";
86
103
 
87
104
  // apps/host/src/main.ts
88
- import { randomUUID as randomUUID4 } from "node:crypto";
105
+ import { randomUUID as randomUUID6 } from "node:crypto";
89
106
  import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
90
107
  import { homedir as homedir3 } from "node:os";
91
- import { join as join12, resolve as resolve7 } from "node:path";
108
+ import { join as join13, resolve as resolve7 } from "node:path";
92
109
  import { parseArgs } from "node:util";
93
110
  import { createModels } from "@earendil-works/pi-ai/models";
94
111
  import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
95
112
  import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
96
113
  import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
97
114
 
115
+ // apps/host/src/upgrade/host-upgrader.ts
116
+ import { execFile, spawn } from "node:child_process";
117
+ import { promisify } from "node:util";
118
+
119
+ // apps/host/src/upgrade/upgrader.ts
120
+ var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
121
+ var AUTO_TICK_MS = 60 * 1000;
122
+ var AUTO_MIN_AGE_MS = 3 * 24 * 60 * 60 * 1000;
123
+ var AUTO_QUIET_MS = 15 * 60 * 1000;
124
+ var sameUpgrade = (a, b) => JSON.stringify(a) === JSON.stringify(b);
125
+ var message = (error) => error instanceof Error ? error.message : String(error);
126
+ function compareVersions(a, b) {
127
+ const parse = (version) => {
128
+ const [core = "", pre] = version.split("-", 2);
129
+ return { parts: core.split(".").map((part) => Number.parseInt(part, 10) || 0), pre };
130
+ };
131
+ const x = parse(a);
132
+ const y = parse(b);
133
+ for (let i = 0;i < Math.max(x.parts.length, y.parts.length); i++) {
134
+ const diff = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
135
+ if (diff !== 0)
136
+ return diff;
137
+ }
138
+ if (x.pre === y.pre)
139
+ return 0;
140
+ if (x.pre === undefined)
141
+ return 1;
142
+ if (y.pre === undefined)
143
+ return -1;
144
+ return x.pre < y.pre ? -1 : 1;
145
+ }
146
+
147
+ class HostUpgrader {
148
+ #options;
149
+ #published;
150
+ #stops = [];
151
+ #check;
152
+ #lastActivity;
153
+ #admitting = 0;
154
+ constructor(options) {
155
+ this.#options = options;
156
+ this.#lastActivity = options.now();
157
+ this.#published = new Published({
158
+ current: options.current,
159
+ available: null,
160
+ blocked: null,
161
+ phase: "idle",
162
+ target: null,
163
+ attempt: 0,
164
+ checking: false,
165
+ checkedAt: null,
166
+ checkError: null,
167
+ failure: null
168
+ }, sameUpgrade);
169
+ }
170
+ get value() {
171
+ return this.#published.value;
172
+ }
173
+ subscribe(listener) {
174
+ return this.#published.subscribe(listener);
175
+ }
176
+ get installing() {
177
+ return this.value.phase === "installing";
178
+ }
179
+ async begin() {
180
+ const { busy, mode, every } = this.#options;
181
+ this.#stops.push(busy.subscribe((value) => {
182
+ this.touch();
183
+ if (!value)
184
+ this.#installIfIdle();
185
+ }), mode.subscribe((value) => {
186
+ if (value !== "off")
187
+ this.check().catch(() => {});
188
+ }), every(CHECK_INTERVAL_MS, () => void this.check().catch(() => {})), every(AUTO_TICK_MS, () => this.#autoTick()));
189
+ await this.#refreshBlocked();
190
+ if (mode.value !== "off")
191
+ this.check().catch(() => {});
192
+ }
193
+ close() {
194
+ for (const stop of this.#stops.splice(0))
195
+ stop();
196
+ }
197
+ touch() {
198
+ this.#lastActivity = this.#options.now();
199
+ }
200
+ admit() {
201
+ if (this.installing)
202
+ throw new Error(HOST_UPGRADING_ERROR);
203
+ this.touch();
204
+ this.#admitting++;
205
+ let released = false;
206
+ return () => {
207
+ if (released)
208
+ return;
209
+ released = true;
210
+ this.#admitting--;
211
+ this.touch();
212
+ };
213
+ }
214
+ check() {
215
+ if (this.#options.mode.value === "off")
216
+ return Promise.reject(new Error("Upgrade checks are off on this host"));
217
+ this.#check ??= this.#runCheck().finally(() => {
218
+ this.#check = undefined;
219
+ });
220
+ return this.#check;
221
+ }
222
+ async start(when) {
223
+ await this.#refreshBlocked();
224
+ const { blocked, available, phase } = this.value;
225
+ if (blocked !== null)
226
+ throw new Error(`This host can't upgrade itself (${blocked})`);
227
+ if (phase !== "idle")
228
+ throw new Error("An upgrade is already in progress");
229
+ if (available === null)
230
+ throw new Error("No newer release");
231
+ this.#begin(when, available.version);
232
+ }
233
+ #begin(when, version) {
234
+ this.#set({ attempt: this.value.attempt + 1 });
235
+ if (when === "now" || !this.#busy())
236
+ this.#install(version);
237
+ else
238
+ this.#set({ phase: "waiting", target: version });
239
+ }
240
+ #busy() {
241
+ return this.#options.busy.value || this.#admitting > 0;
242
+ }
243
+ #installIfIdle() {
244
+ if (this.value.phase === "waiting" && !this.#busy())
245
+ this.#install(this.value.target);
246
+ }
247
+ cancel() {
248
+ if (this.value.phase === "installing")
249
+ throw new Error("The upgrade is installing and can't be cancelled");
250
+ if (this.value.phase === "waiting")
251
+ this.#set({ phase: "idle", target: null });
252
+ }
253
+ async#runCheck() {
254
+ this.#set({ checking: true });
255
+ try {
256
+ const release = await this.#options.latest();
257
+ const newer = compareVersions(release.version, this.#options.current) > 0;
258
+ this.#set({
259
+ checking: false,
260
+ checkedAt: this.#options.now(),
261
+ checkError: null,
262
+ available: newer ? {
263
+ version: release.version,
264
+ publishedAt: release.publishedAt,
265
+ crossesMajor: release.protocolMajor !== this.#options.protocolMajor
266
+ } : null
267
+ });
268
+ } catch (error) {
269
+ this.#set({ checking: false, checkedAt: this.#options.now(), checkError: message(error) });
270
+ throw error;
271
+ }
272
+ }
273
+ async#refreshBlocked() {
274
+ this.#set({ blocked: await this.#options.blocked() });
275
+ }
276
+ #autoTick() {
277
+ if (this.#busy())
278
+ this.touch();
279
+ this.#installIfIdle();
280
+ const candidate = this.#autoCandidate();
281
+ if (candidate === undefined)
282
+ return;
283
+ this.#refreshBlocked().then(() => {
284
+ if (this.#autoCandidate() === candidate)
285
+ this.#begin("idle", candidate);
286
+ }, () => {});
287
+ }
288
+ #autoCandidate() {
289
+ const { mode, now } = this.#options;
290
+ const { available, blocked, phase, failure } = this.value;
291
+ if (mode.value !== "auto" || phase !== "idle" || blocked !== null || available === null)
292
+ return;
293
+ if (available.crossesMajor || failure?.version === available.version)
294
+ return;
295
+ if (now() - available.publishedAt < AUTO_MIN_AGE_MS || this.#busy() || now() - this.#lastActivity < AUTO_QUIET_MS) {
296
+ return;
297
+ }
298
+ return available.version;
299
+ }
300
+ async#install(version) {
301
+ this.#set({ phase: "installing", target: version });
302
+ try {
303
+ await this.#options.drain?.();
304
+ } catch (error) {
305
+ this.#set({ phase: "idle", target: null, failure: { version, message: message(error), attempt: this.value.attempt } });
306
+ return;
307
+ }
308
+ let result;
309
+ try {
310
+ result = await this.#options.install(version);
311
+ } catch (error) {
312
+ result = { ok: false, message: message(error) };
313
+ }
314
+ if (result.ok) {
315
+ this.#options.restart();
316
+ return;
317
+ }
318
+ this.#set({ phase: "idle", target: null, failure: { version, message: result.message, attempt: this.value.attempt } });
319
+ }
320
+ #set(patch) {
321
+ this.#published.set({ ...this.value, ...patch });
322
+ }
323
+ }
324
+
325
+ // apps/host/src/upgrade/host-upgrader.ts
326
+ var exec = promisify(execFile);
327
+ var withPath = (npm) => npm.binDir === undefined ? process.env : { ...process.env, PATH: `${npm.binDir}:${process.env.PATH ?? ""}` };
328
+ var UPGRADE_EXIT_CODE = 75;
329
+ async function upgradeBlocker(dataDir, {
330
+ packaged = packagedVersion !== undefined && packagePrefix() !== undefined,
331
+ manager = serviceManager
332
+ } = {}) {
333
+ if (!packaged)
334
+ return "source";
335
+ try {
336
+ const service = manager();
337
+ const spec = await service.read();
338
+ if (spec === undefined || spec.dataDir !== dataDir)
339
+ return "no-service";
340
+ const state = await service.state();
341
+ if (!state.installed)
342
+ return "no-service";
343
+ if (state.stopped)
344
+ return "stopped";
345
+ return state.running ? null : "no-service";
346
+ } catch {
347
+ return "no-service";
348
+ }
349
+ }
350
+ async function latestRelease() {
351
+ const npm = viewCommand(npmBeside(process.execPath));
352
+ const { stdout } = await exec(npm.command, [...npm.args], { env: withPath(npm), timeout: 60000 });
353
+ return parseRelease(stdout);
354
+ }
355
+ function installRelease(version) {
356
+ const prefix = packagePrefix();
357
+ if (prefix === undefined)
358
+ return Promise.resolve({ ok: false, message: "pinomad isn't a global npm install" });
359
+ const npm = installCommand(npmBeside(process.execPath), prefix, `pinomad@${version}`);
360
+ return new Promise((resolve) => {
361
+ let output = "";
362
+ const child = spawn(npm.command, [...npm.args], { env: withPath(npm), stdio: ["ignore", "pipe", "pipe"] });
363
+ const collect = (chunk) => {
364
+ output = (output + chunk.toString("utf8")).slice(-64 * 1024);
365
+ };
366
+ child.stdout.on("data", collect);
367
+ child.stderr.on("data", collect);
368
+ child.once("error", (error) => resolve({ ok: false, message: error.message }));
369
+ child.once("exit", (code, signal) => resolve(code === 0 ? { ok: true } : { ok: false, message: outputTail(output) || `npm exited with ${signal ?? code}` }));
370
+ });
371
+ }
372
+ async function busyValue(harness, context) {
373
+ const watch = await harness.watchTaskGraph(context);
374
+ const busy = new Published(Object.keys(watch.value.tasks).length > 0, (a, b) => a === b);
375
+ watch.start((call) => {
376
+ if (call.kind === "value")
377
+ busy.set(Object.keys(call.value.tasks).length > 0);
378
+ return Promise.resolve();
379
+ });
380
+ return Object.assign(busy, { stop: () => watch.stop() });
381
+ }
382
+ function mergedBusy(tasks, processes) {
383
+ if (processes === undefined)
384
+ return tasks;
385
+ const busy = new Published(tasks.value || processes.value, (a, b) => a === b);
386
+ const stopTasks = tasks.subscribe((value) => busy.set(value || processes.value));
387
+ const stopProcesses = processes.subscribe((value) => busy.set(tasks.value || value));
388
+ return Object.assign(busy, {
389
+ stop: () => {
390
+ stopTasks();
391
+ stopProcesses();
392
+ tasks.stop();
393
+ }
394
+ });
395
+ }
396
+ function modeValue(settings) {
397
+ const mode = new Published(settings.value.autoUpgrade ?? "notify", (a, b) => a === b);
398
+ const stop = settings.subscribe((value) => mode.set(value.autoUpgrade ?? "notify"));
399
+ return Object.assign(mode, { stop });
400
+ }
401
+ var every = (ms, run) => {
402
+ const timer = setInterval(run, ms);
403
+ timer.unref();
404
+ return () => clearInterval(timer);
405
+ };
406
+ async function startHostUpgrader(options) {
407
+ const taskBusy = await busyValue(options.harness, options.context);
408
+ const busy = mergedBusy(taskBusy, options.processesBusy);
409
+ const mode = modeValue(options.settings);
410
+ const { seams } = options;
411
+ const upgrader = new HostUpgrader({
412
+ current: options.current,
413
+ protocolMajor: PROTOCOL.major,
414
+ blocked: seams.blocked ?? (() => upgradeBlocker(options.dataDir)),
415
+ latest: seams.latest ?? latestRelease,
416
+ install: seams.install ?? installRelease,
417
+ restart: seams.restart,
418
+ ...options.drain === undefined ? {} : { drain: options.drain },
419
+ busy,
420
+ mode,
421
+ now: Date.now,
422
+ every: seams.every ?? every
423
+ });
424
+ const close = upgrader.close.bind(upgrader);
425
+ upgrader.close = () => {
426
+ close();
427
+ busy.stop();
428
+ mode.stop();
429
+ };
430
+ await upgrader.begin();
431
+ return upgrader;
432
+ }
433
+
98
434
  // apps/host/src/extensions/question.ts
99
435
  import {
100
436
  defineDoc,
@@ -102,55 +438,7 @@ import {
102
438
  defineTask,
103
439
  defineTool
104
440
  } from "@earendil-works/pi-durable";
105
- import { Type as Type2 } from "typebox";
106
-
107
- // packages/protocol/src/presentation.ts
108
441
  import { Type } from "typebox";
109
- import { Value } from "typebox/value";
110
- var TodoItemSchema = Type.Object({
111
- text: Type.String({ minLength: 1 }),
112
- status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")])
113
- });
114
- var TodoSchema = Type.Object({ items: Type.Array(TodoItemSchema) });
115
- var QuestionOptionSchema = Type.Object({
116
- label: Type.String({ minLength: 1 }),
117
- description: Type.Optional(Type.String())
118
- });
119
- var QuestionItemSchema = Type.Object({
120
- question: Type.String({ minLength: 1 }),
121
- header: Type.String({ minLength: 1, maxLength: 16 }),
122
- options: Type.Array(QuestionOptionSchema, { minItems: 2, maxItems: 4 }),
123
- multiSelect: Type.Optional(Type.Boolean())
124
- });
125
- var QuestionAnswerSchema = Type.Object({
126
- selected: Type.Array(Type.String()),
127
- other: Type.Optional(Type.String())
128
- });
129
- var QuestionResolutionSchema = Type.Union([
130
- Type.Object({ outcome: Type.Literal("answered"), answers: Type.Array(QuestionAnswerSchema), at: Type.Number() }),
131
- Type.Object({ outcome: Type.Literal("dismissed"), at: Type.Number() }),
132
- Type.Object({ outcome: Type.Literal("cancelled"), at: Type.Number() })
133
- ]);
134
- var QuestionRequestSchema = Type.Object({
135
- id: Type.String({ minLength: 1 }),
136
- questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 }),
137
- askedAt: Type.Number(),
138
- resolution: Type.Optional(QuestionResolutionSchema)
139
- });
140
- var QuestionSchema = Type.Object({ requests: Type.Array(QuestionRequestSchema) });
141
- var DiffSchema = Type.Object({ patch: Type.String() });
142
- var CodemodeCallSchema = Type.Object({
143
- id: Type.Optional(Type.Number()),
144
- name: Type.String(),
145
- args: Type.String(),
146
- status: Type.Union([Type.Literal("running"), Type.Literal("ok"), Type.Literal("error"), Type.Literal("cancelled")]),
147
- durationMs: Type.Optional(Type.Number()),
148
- error: Type.Optional(Type.String()),
149
- details: Type.Optional(Type.Unknown())
150
- });
151
- var CodemodeSchema = Type.Object({ code: Type.Optional(Type.String()), calls: Type.Array(CodemodeCallSchema) });
152
-
153
- // apps/host/src/extensions/question.ts
154
442
  var QuestionDoc = defineDoc({
155
443
  kind: "question.requests",
156
444
  version: 1,
@@ -178,8 +466,10 @@ var QuestionWait = defineTask({
178
466
  resolve(initial);
179
467
  return;
180
468
  }
181
- watch.start(async (value) => {
182
- const found = resolutionOf(value, task.input.requestId);
469
+ watch.start(async (call) => {
470
+ if (call.kind !== "value")
471
+ return;
472
+ const found = resolutionOf(call.value, task.input.requestId);
183
473
  if (found !== undefined)
184
474
  resolve(found);
185
475
  });
@@ -224,8 +514,8 @@ var askUserQuestion = defineTool({
224
514
  name: "ask_user_question",
225
515
  callers: ["model"],
226
516
  description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep everything short: headers a word or two, option labels a few words. Usually leave option descriptions out; add one only when the label alone is ambiguous, as one line of at most ~60 characters.",
227
- parameters: Type2.Object({
228
- questions: Type2.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
517
+ parameters: Type.Object({
518
+ questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
229
519
  }),
230
520
  replay: "safe",
231
521
  execute: async (args, api, context) => {
@@ -294,31 +584,31 @@ function createCodemode() {
294
584
  }
295
585
 
296
586
  // apps/host/src/extensions/context.ts
297
- import { dirname as dirname2, sep as sep4 } from "node:path";
587
+ import { dirname as dirname2, sep as sep5 } from "node:path";
298
588
  import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
299
589
  import { formatSkillsForPrompt, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
300
590
 
301
591
  // apps/host/src/skills.ts
302
- import { execFile as execFile2 } from "node:child_process";
592
+ import { execFile as execFile3 } from "node:child_process";
303
593
  import { existsSync } from "node:fs";
304
594
  import { realpath as realpath4, stat as stat4 } from "node:fs/promises";
305
- import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
306
- import { promisify as promisify2 } from "node:util";
595
+ import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as sep4 } from "node:path";
596
+ import { promisify as promisify3 } from "node:util";
307
597
  import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
308
598
  import { loadSkills } from "@earendil-works/pi-coding-agent";
309
599
 
310
600
  // apps/host/src/checkout.ts
311
- import { execFile, spawn } from "node:child_process";
601
+ import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
312
602
  import { randomBytes } from "node:crypto";
313
603
  import { realpath, rm, stat } from "node:fs/promises";
314
604
  import { tmpdir } from "node:os";
315
605
  import { join, relative, sep } from "node:path";
316
- import { promisify } from "node:util";
317
- var run = promisify(execFile);
606
+ import { promisify as promisify2 } from "node:util";
607
+ var run = promisify2(execFile2);
318
608
  var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
319
609
  function gitCapped(dir, args, maxBytes, env) {
320
610
  return new Promise((resolve, reject) => {
321
- const child = spawn("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
611
+ const child = spawn2("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
322
612
  const chunks = [];
323
613
  let size = 0;
324
614
  let killed = false;
@@ -443,14 +733,14 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
443
733
  }
444
734
 
445
735
  // apps/host/src/files.ts
446
- import { spawn as spawn2 } from "node:child_process";
736
+ import { spawn as spawn3 } from "node:child_process";
447
737
  import { opendir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
448
- import { join as join3, resolve as resolve2, sep as sep2 } from "node:path";
738
+ import { join as join3, resolve as resolve2, sep as sep3 } from "node:path";
449
739
  import { AgentDoc as AgentDoc2 } from "@earendil-works/pi-durable";
450
740
 
451
741
  // apps/host/src/organization.ts
452
742
  import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
453
- import { basename, join as join2, resolve } from "node:path";
743
+ import { basename, join as join2, resolve, sep as sep2 } from "node:path";
454
744
  import { clampThinkingLevel } from "@earendil-works/pi-ai";
455
745
  import {
456
746
  AgentDoc,
@@ -756,6 +1046,17 @@ async function familyRoot(harness, id, context) {
756
1046
  }
757
1047
  return current;
758
1048
  }
1049
+ async function subagentWorktreeBase(harness, dataDir, dispatcher, context) {
1050
+ const unavailable = (why) => new Error(`checkout "worktree" needs ${why}; use "shared"`);
1051
+ const cwd = (await harness.snapshot(AgentDoc, dispatcher, context))?.cwd;
1052
+ if (cwd === undefined || cwd.startsWith(join2(dataDir, "chats") + sep2))
1053
+ throw unavailable("a project conversation, not a Chat");
1054
+ const head = await gitBase(cwd);
1055
+ if (head === undefined)
1056
+ throw unavailable("a Git repository with at least one commit");
1057
+ const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
1058
+ return record === undefined ? head : { repo: record.repo, subdir: record.subdir, base: head.base };
1059
+ }
759
1060
  async function cleanupArchivedWorktrees(harness, rootId, context) {
760
1061
  const index = await harness.snapshot(IndexDoc, context);
761
1062
  const checkouts = index?.checkouts ?? [];
@@ -827,7 +1128,7 @@ function createCandidateLister(enumerate, options = {}) {
827
1128
  }
828
1129
  function gitLines(cwd, args, limits) {
829
1130
  return new Promise((resolvePromise, rejectPromise) => {
830
- const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
1131
+ const child = spawn3("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
831
1132
  const chunks = [];
832
1133
  let bytes = 0;
833
1134
  let truncated = false;
@@ -875,7 +1176,7 @@ function gitLines(cwd, args, limits) {
875
1176
  }
876
1177
  async function inWorkTree(root) {
877
1178
  return await new Promise((resolvePromise) => {
878
- const child = spawn2("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
1179
+ const child = spawn3("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
879
1180
  let out = "";
880
1181
  child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
881
1182
  child.on("error", () => resolvePromise(false));
@@ -884,7 +1185,7 @@ async function inWorkTree(root) {
884
1185
  }
885
1186
  async function hasHead(root) {
886
1187
  return await new Promise((resolvePromise) => {
887
- const child = spawn2("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
1188
+ const child = spawn3("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
888
1189
  child.on("error", () => resolvePromise(false));
889
1190
  child.on("close", (code) => resolvePromise(code === 0));
890
1191
  });
@@ -1005,7 +1306,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
1005
1306
  const cwd = agent.cwd;
1006
1307
  if (cwd === undefined)
1007
1308
  throw new Error(`Conversation ${scope.conversationId} has no checkout`);
1008
- if (cwd.startsWith(join3(dataDir, "chats") + sep2))
1309
+ if (cwd.startsWith(join3(dataDir, "chats") + sep3))
1009
1310
  return { files: [], truncated: false };
1010
1311
  const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
1011
1312
  const ready = record === undefined || await worktreeSettled(record);
@@ -1037,7 +1338,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
1037
1338
  }
1038
1339
 
1039
1340
  // apps/host/src/skills.ts
1040
- var exec = promisify2(execFile2);
1341
+ var exec2 = promisify3(execFile3);
1041
1342
  function invocableSkillName(name) {
1042
1343
  return name.length <= 64 && /^[a-z0-9-]+$/.test(name) && !name.startsWith("-") && !name.endsWith("-") && !name.includes("--");
1043
1344
  }
@@ -1101,10 +1402,10 @@ async function committedSkillFiles(gitRoot, layers) {
1101
1402
  if (!await inWorkTree(gitRoot) || !await hasHead(gitRoot))
1102
1403
  return null;
1103
1404
  const pathspecs = layers.map((dir) => {
1104
- const rel = relative2(gitRoot, dir).split(sep3).join("/");
1405
+ const rel = relative2(gitRoot, dir).split(sep4).join("/");
1105
1406
  return rel === "" ? ".agents/skills" : `${rel}/.agents/skills`;
1106
1407
  });
1107
- const { stdout } = await exec("git", ["-C", gitRoot, "ls-tree", "-r", "-z", "--full-name", "--name-only", "HEAD", "--", ...pathspecs], { maxBuffer: 4 * 1024 * 1024 });
1408
+ const { stdout } = await exec2("git", ["-C", gitRoot, "ls-tree", "-r", "-z", "--full-name", "--name-only", "HEAD", "--", ...pathspecs], { maxBuffer: 4 * 1024 * 1024 });
1108
1409
  return new Set(stdout.split("\x00").filter((path) => path !== ""));
1109
1410
  }
1110
1411
  async function enumerateSkills(root, mode, agentsHome) {
@@ -1116,7 +1417,7 @@ async function enumerateSkills(root, mode, agentsHome) {
1116
1417
  if (mode === "head" && gitRoot !== undefined) {
1117
1418
  const committed = await committedSkillFiles(gitRoot, layers.dirs);
1118
1419
  if (committed !== null) {
1119
- project = project.filter((scoped) => committed.has(relative2(gitRoot, scoped.skill.filePath).split(sep3).join("/")));
1420
+ project = project.filter((scoped) => committed.has(relative2(gitRoot, scoped.skill.filePath).split(sep4).join("/")));
1120
1421
  }
1121
1422
  }
1122
1423
  }
@@ -1250,7 +1551,7 @@ function createContext(options) {
1250
1551
  const seen = new Set;
1251
1552
  files = [
1252
1553
  ...loadProjectContextFiles({ cwd: dirname2(checkout.repo), agentDir: agentsHome }),
1253
- ...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep4))
1554
+ ...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep5))
1254
1555
  ].filter((file) => {
1255
1556
  if (seen.has(file.path))
1256
1557
  return false;
@@ -1277,21 +1578,336 @@ ${file.content}`).join(`
1277
1578
  };
1278
1579
  }
1279
1580
 
1581
+ // apps/host/src/extensions/context-breakdown.ts
1582
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
1583
+ import { defineDoc as defineDoc4, defineExtension as defineExtension3, GenerationTask, hook } from "@earendil-works/pi-durable";
1584
+
1585
+ // packages/client/src/context-usage.ts
1586
+ var contextTokens = (usage) => usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
1587
+
1588
+ // apps/host/src/context-breakdown.ts
1589
+ var CHARS_PER_TOKEN = 4;
1590
+ var tokensOf = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
1591
+ var MCP_TOOL = /^mcp__/;
1592
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1593
+ var arrayOf = (value) => Array.isArray(value) ? value : [];
1594
+ function textOf(value) {
1595
+ if (typeof value === "string")
1596
+ return value;
1597
+ if (isObject(value) && "parts" in value)
1598
+ return textOf(value.parts);
1599
+ return arrayOf(value).map((block) => isObject(block) && typeof block.text === "string" ? block.text : "").join(`
1600
+ `);
1601
+ }
1602
+ var sourcesOf = (payload) => [payload, payload.config, payload.context].filter(isObject);
1603
+ var systemItemsOf = (source) => [...arrayOf(source.messages), ...arrayOf(source.input)].filter((item) => isObject(item) && (item.role === "system" || item.role === "developer"));
1604
+ function systemTextOf(payload) {
1605
+ return sourcesOf(payload).flatMap((source) => [
1606
+ textOf(source.system),
1607
+ textOf(source.instructions),
1608
+ textOf(source.systemInstruction),
1609
+ ...systemItemsOf(source).flatMap((item) => [
1610
+ textOf(item.content),
1611
+ ...isObject(item.sections) ? Object.values(item.sections).filter((section2) => typeof section2 === "string") : []
1612
+ ])
1613
+ ]).filter((text) => text !== "").join(`
1614
+ `);
1615
+ }
1616
+ function toolsOf(payload) {
1617
+ const listed = sourcesOf(payload).flatMap((source) => [
1618
+ ...arrayOf(source.tools),
1619
+ ...arrayOf(isObject(source.toolConfig) ? source.toolConfig.tools : undefined),
1620
+ ...systemItemsOf(source).flatMap((item) => [...arrayOf(item.tools), ...arrayOf(item.toolsAdded)])
1621
+ ]);
1622
+ return listed.flatMap((tool) => isObject(tool) && Array.isArray(tool.functionDeclarations) ? tool.functionDeclarations.filter(isObject) : isObject(tool) ? [tool] : []);
1623
+ }
1624
+ function toolName(tool) {
1625
+ for (const holder of [tool, tool.function, tool.toolSpec]) {
1626
+ if (isObject(holder) && typeof holder.name === "string")
1627
+ return holder.name;
1628
+ }
1629
+ return;
1630
+ }
1631
+ function cut(text, tag) {
1632
+ const open = text.indexOf(`<${tag}>`);
1633
+ const close = open < 0 ? -1 : text.indexOf(`</${tag}>`, open);
1634
+ if (close < 0)
1635
+ return { inside: "", rest: text };
1636
+ return { inside: text.slice(open, close + tag.length + 3), rest: text.slice(0, open) + text.slice(close + tag.length + 3) };
1637
+ }
1638
+ function splitCodemode(section2, namespaces) {
1639
+ const starts = namespaces.map(({ name }) => section2.search(new RegExp(`\\n## ${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\n| \\(|$)`))).filter((index) => index >= 0);
1640
+ if (starts.length === 0)
1641
+ return { builtin: section2, mcp: "" };
1642
+ const first = Math.min(...starts);
1643
+ return { builtin: section2.slice(0, first), mcp: section2.slice(first) };
1644
+ }
1645
+ function estimateRequest(payload, namespaces) {
1646
+ if (!isObject(payload))
1647
+ return;
1648
+ const system = systemTextOf(payload);
1649
+ const tools = toolsOf(payload);
1650
+ if (system === "" && tools.length === 0)
1651
+ return;
1652
+ const skills = cut(system, "available_skills");
1653
+ const codemode = cut(skills.rest, "codemode");
1654
+ const { builtin, mcp } = splitCodemode(codemode.inside, namespaces);
1655
+ let toolTokens = 0;
1656
+ let mcpTokens = tokensOf(mcp);
1657
+ for (const tool of tools) {
1658
+ const cost = tokensOf(JSON.stringify(tool));
1659
+ if (MCP_TOOL.test(toolName(tool) ?? ""))
1660
+ mcpTokens += cost;
1661
+ else
1662
+ toolTokens += cost;
1663
+ }
1664
+ return {
1665
+ system: tokensOf(codemode.rest) + tokensOf(builtin),
1666
+ skills: tokensOf(skills.inside),
1667
+ tools: toolTokens,
1668
+ mcp: mcpTokens
1669
+ };
1670
+ }
1671
+ function breakdownOf(estimate, used) {
1672
+ const fixed = estimate.system + estimate.skills + estimate.tools + estimate.mcp;
1673
+ const scale = fixed > used ? used / fixed : 1;
1674
+ const system = Math.floor(estimate.system * scale);
1675
+ const skills = Math.floor(estimate.skills * scale);
1676
+ const tools = Math.floor(estimate.tools * scale);
1677
+ const mcp = Math.floor(estimate.mcp * scale);
1678
+ return { used, system, skills, tools, mcp, messages: used - system - skills - tools - mcp };
1679
+ }
1680
+
1681
+ // apps/host/src/extensions/context-breakdown.ts
1682
+ var ContextBreakdownDoc = defineDoc4({
1683
+ kind: "context.breakdown",
1684
+ version: 1,
1685
+ scope: "conversation",
1686
+ history: "latest",
1687
+ fork: "current",
1688
+ initial: () => ({ used: 0, system: 0, skills: 0, tools: 0, mcp: 0, messages: 0 }),
1689
+ checkpointWhen: () => false
1690
+ });
1691
+ function createContextBreakdown(options) {
1692
+ let namespaces = [];
1693
+ const pending = new Map;
1694
+ const build = () => defineExtension3({
1695
+ name: "context-breakdown",
1696
+ hooks: [
1697
+ hook(GenerationTask, {
1698
+ beforeProviderRequest: (request, api) => {
1699
+ const estimate = estimateRequest(request.payload, namespaces);
1700
+ if (estimate === undefined)
1701
+ pending.delete(api.taskId);
1702
+ else
1703
+ pending.set(api.taskId, estimate);
1704
+ return;
1705
+ },
1706
+ afterResponse: (message2, api) => {
1707
+ const estimate = pending.get(api.taskId);
1708
+ pending.delete(api.taskId);
1709
+ if (estimate === undefined || message2.stopReason === "aborted" || message2.stopReason === "error")
1710
+ return;
1711
+ const used = message2.usage ? contextTokens(message2.usage) : 0;
1712
+ if (!(used > 0))
1713
+ return;
1714
+ const value = breakdownOf(estimate, used);
1715
+ options.harness().commit(async (tx) => {
1716
+ Object.assign(await tx.doc(ContextBreakdownDoc, api.conversationId), value);
1717
+ }, BACKGROUND_CONTEXT).catch((error) => {
1718
+ console.error(`context-breakdown: conversation ${api.conversationId}: ${error instanceof Error ? error.message : String(error)}`);
1719
+ });
1720
+ }
1721
+ })
1722
+ ]
1723
+ });
1724
+ return {
1725
+ extension: build(),
1726
+ docs: [{ token: ContextBreakdownDoc, presentation: "pinomad.context" }],
1727
+ namespaced: (next) => {
1728
+ namespaces = next;
1729
+ return build();
1730
+ }
1731
+ };
1732
+ }
1733
+
1734
+ // apps/host/src/extensions/processes.ts
1735
+ import { randomUUID } from "node:crypto";
1736
+ import { defineExtension as defineExtension4, defineTool as defineTool2 } from "@earendil-works/pi-durable";
1737
+ import { Type as Type2 } from "typebox";
1738
+
1739
+ // packages/client/src/processes.ts
1740
+ function processStatusText(state) {
1741
+ if (state === "starting")
1742
+ return "Starting";
1743
+ if (state === "running")
1744
+ return "Running";
1745
+ if (state === "lost")
1746
+ return "Lost track after a host restart; it may still be running";
1747
+ if (typeof state === "object" && "stopped" in state) {
1748
+ switch (state.stopped) {
1749
+ case "agent":
1750
+ return "Stopped by the agent";
1751
+ case "user":
1752
+ return "Stopped by the user";
1753
+ case "archive":
1754
+ return "Stopped when the conversation was archived";
1755
+ case "shutdown":
1756
+ return "Stopped when the host shut down";
1757
+ }
1758
+ }
1759
+ if (typeof state === "object" && "failed" in state)
1760
+ return `Failed to start: ${state.failed}`;
1761
+ if (typeof state === "object" && "exited" in state) {
1762
+ if (state.signal !== undefined)
1763
+ return `Exited after signal ${state.signal}`;
1764
+ if (state.exited === null)
1765
+ return "Exited";
1766
+ return `Exited with code ${state.exited}`;
1767
+ }
1768
+ return "Unknown";
1769
+ }
1770
+
1771
+ // apps/host/src/process-doc.ts
1772
+ import { defineDoc as defineDoc5 } from "@earendil-works/pi-durable";
1773
+ var ProcessDoc = defineDoc5({
1774
+ kind: "pinomad.processes",
1775
+ version: 1,
1776
+ scope: "conversation",
1777
+ history: "latest",
1778
+ fork: "initial",
1779
+ initial: () => ({ processes: [] }),
1780
+ checkpointWhen: () => false
1781
+ });
1782
+
1783
+ // apps/host/src/extensions/processes.ts
1784
+ var ID = Type2.String({ minLength: 1, description: "The process id returned by process_start." });
1785
+ function createProcesses(options) {
1786
+ const { runtime } = options;
1787
+ if (runtime === undefined) {
1788
+ return {
1789
+ extension: defineExtension4({ name: "processes", tools: [] }),
1790
+ docs: [{ token: ProcessDoc, presentation: "pinomad.processes" }]
1791
+ };
1792
+ }
1793
+ const start = defineTool2({
1794
+ name: "process_start",
1795
+ callers: ["model"],
1796
+ description: "Start a long-running command in the background and return its id. The command runs in your conversation's working directory under a pty, so dev servers and monitors keep running after the call returns. " + "It cannot receive input. Use process_output to read it, process_stop to end it, and process_list to recover ids after a context reset. " + "The host keeps at most 8 unfinished processes per conversation family. Write monitors as commands that exit when the event happens (for example a one-shot grep) so the exit report wakes you; line-by-line callbacks are not supported.",
1797
+ parameters: Type2.Object({
1798
+ command: Type2.String({ minLength: 1, description: "Shell command; it is run with the login shell's -c." }),
1799
+ name: Type2.Optional(Type2.String({ description: `Short label for lists, e.g. "dev server". Defaults to the command's first line.` }))
1800
+ }),
1801
+ replay: "unsafe",
1802
+ execute: async (args, api, context) => {
1803
+ const agent = await api.agent(context);
1804
+ const cwd = agent.cwd;
1805
+ if (cwd === undefined)
1806
+ throw new Error("This conversation has no working directory");
1807
+ const name = cleanName(args.name) ?? defaultName(args.command);
1808
+ const memo = await api.memo("process.start", { id: randomUUID(), name }, context);
1809
+ const process2 = await runtime.start({
1810
+ conversationId: api.conversationId,
1811
+ command: args.command,
1812
+ name: memo.name,
1813
+ cwd,
1814
+ id: memo.id
1815
+ }, context);
1816
+ return result2(processResult("Started", process2), { id: process2.id, name: process2.name });
1817
+ }
1818
+ });
1819
+ const output = defineTool2({
1820
+ name: "process_output",
1821
+ callers: ["model"],
1822
+ description: `Read a background process's output after a raw character position. Omit "since" for the retained output; pass the previous result's "next" to read only what is new. ` + 'Returns at most 16 KiB of text with ANSI escapes removed; "next" stays a raw output position. "truncated" is true when the start of the retained output was already dropped. ' + "If the process ended, status and exitCode say how; output may be unavailable after it falls out of history or the host restarts.",
1823
+ parameters: Type2.Object({
1824
+ id: ID,
1825
+ since: Type2.Optional(Type2.Integer({ minimum: 0, description: "The previous result's next position." }))
1826
+ }),
1827
+ replay: "safe",
1828
+ execute: async (args, api, context) => {
1829
+ const value = await runtime.output({
1830
+ conversationId: api.conversationId,
1831
+ id: args.id,
1832
+ ...args.since === undefined ? {} : { since: args.since }
1833
+ }, context);
1834
+ const heading = statusLine(value.status);
1835
+ const body = value.released === true ? "Output is no longer available." : value.text === "" ? "(no new output)" : value.text;
1836
+ const truncated = value.truncated && value.released !== true ? `
1837
+ (the beginning of the retained output was truncated)` : "";
1838
+ return result2(`${heading}
1839
+ ${body}${truncated}`, value);
1840
+ }
1841
+ });
1842
+ const stop = defineTool2({
1843
+ name: "process_stop",
1844
+ callers: ["model"],
1845
+ description: "Stop a background process and wait until it has exited. The host sends SIGTERM to the whole process group, then SIGKILL after 5 seconds. " + "A process that exits after this call is treated as stopped, so it will not report back to you; the result carries its final state. Stopping an already ended process returns that state.",
1846
+ parameters: Type2.Object({ id: ID }),
1847
+ replay: "safe",
1848
+ execute: async (args, api, context) => {
1849
+ const process2 = await runtime.stop({
1850
+ conversationId: api.conversationId,
1851
+ id: args.id,
1852
+ reason: "agent"
1853
+ }, context);
1854
+ return result2(processResult("Stopped", process2), { state: process2.state });
1855
+ }
1856
+ });
1857
+ const list = defineTool2({
1858
+ name: "process_list",
1859
+ callers: ["model"],
1860
+ description: "List this conversation family's background processes, running and recently ended, with ids and states. Use it to recover ids after a context reset. Output and stop work only while the host still has the process's pty.",
1861
+ parameters: Type2.Object({}),
1862
+ replay: "safe",
1863
+ execute: async (_args, api, context) => {
1864
+ const processes = await runtime.list(api.conversationId, context);
1865
+ const text = processes.length === 0 ? "No background processes." : processes.map((process2) => `${process2.id} ${process2.name} [${statusLine(process2.state)}] ${process2.command}`).join(`
1866
+ `);
1867
+ return result2(text, { processes });
1868
+ }
1869
+ });
1870
+ return {
1871
+ extension: defineExtension4({ name: "processes", tools: [start, output, stop, list] }),
1872
+ docs: [{ token: ProcessDoc, presentation: "pinomad.processes" }]
1873
+ };
1874
+ }
1875
+ function result2(text, details) {
1876
+ return { output: [{ type: "text", text }], details };
1877
+ }
1878
+ function processResult(verb, process2) {
1879
+ return `${verb} ${process2.name} (${process2.id}): ${statusLine(process2.state)}`;
1880
+ }
1881
+ function statusLine(state) {
1882
+ return processStatusText(state);
1883
+ }
1884
+ function cleanName(name) {
1885
+ const trimmed = name?.trim();
1886
+ return trimmed === undefined || trimmed === "" ? undefined : trimmed;
1887
+ }
1888
+ function defaultName(command) {
1889
+ const line = command.trim().split(`
1890
+ `, 1)[0].trim();
1891
+ if (line === "")
1892
+ return "Background process";
1893
+ return line.length <= 60 ? line : `${line.slice(0, 59)}…`;
1894
+ }
1895
+
1280
1896
  // apps/host/src/extensions/subagent.ts
1897
+ import { join as join6 } from "node:path";
1281
1898
  import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
1282
1899
  import {
1283
1900
  AgentDoc as AgentDoc4,
1284
- AssistantEntry,
1285
1901
  configure as configure2,
1286
- defineExtension as defineExtension3,
1287
- defineTool as defineTool2,
1902
+ defineExtension as defineExtension5,
1903
+ defineTool as defineTool3,
1288
1904
  section as section2
1289
1905
  } from "@earendil-works/pi-durable";
1290
1906
  import { Type as Type3 } from "typebox";
1291
1907
 
1292
1908
  // apps/host/src/subagent-doc.ts
1293
- import { defineDoc as defineDoc4 } from "@earendil-works/pi-durable";
1294
- var SubagentDoc = defineDoc4({
1909
+ import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
1910
+ var SubagentDoc = defineDoc6({
1295
1911
  kind: "pinomad.subagent",
1296
1912
  version: 1,
1297
1913
  scope: "conversation",
@@ -1301,8 +1917,273 @@ var SubagentDoc = defineDoc4({
1301
1917
  checkpointWhen: () => false
1302
1918
  });
1303
1919
 
1920
+ // apps/host/src/subagents.ts
1921
+ import {
1922
+ AssistantEntry,
1923
+ defineDoc as defineDoc7,
1924
+ defineTask as defineTask2
1925
+ } from "@earendil-works/pi-durable";
1926
+ var SubagentsDoc = defineDoc7({
1927
+ kind: "pinomad.subagents",
1928
+ version: 1,
1929
+ scope: "conversation",
1930
+ history: "latest",
1931
+ fork: "initial",
1932
+ initial: () => ({ agents: {} }),
1933
+ checkpointWhen: () => false
1934
+ });
1935
+ var MAX_WORKING_SUBAGENTS = 8;
1936
+ var NAME = /^[\p{L}\p{N}][\p{L}\p{N}_-]{0,39}$/u;
1937
+ async function familyRootIn(tx, id) {
1938
+ let current = id;
1939
+ for (let depth = 0;depth < 256; depth++) {
1940
+ const record = await tx.conversation(current);
1941
+ const up = record?.parent?.conversationId ?? record?.owner?.conversationId;
1942
+ if (up === undefined)
1943
+ return current;
1944
+ current = up;
1945
+ }
1946
+ return current;
1947
+ }
1948
+ function nameFromDescription(description) {
1949
+ const base = (description ?? "").trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+/, "").slice(0, 32).replace(/-+$/, "");
1950
+ return base === "" ? "subagent" : base;
1951
+ }
1952
+ function uniqueName(base, taken) {
1953
+ if (!taken(base))
1954
+ return base;
1955
+ for (let n = 2;; n++) {
1956
+ const candidate = `${base}-${n}`;
1957
+ if (!taken(candidate))
1958
+ return candidate;
1959
+ }
1960
+ }
1961
+ function assertValidName(name) {
1962
+ if (!NAME.test(name)) {
1963
+ throw new Error(`Invalid subagent name "${name}": use 1-40 letters, digits, "-" or "_", starting with a letter or digit`);
1964
+ }
1965
+ }
1966
+ async function isLive(tx, id) {
1967
+ if (id === undefined)
1968
+ return false;
1969
+ const task = await tx.task(id);
1970
+ return task !== undefined && task.state.status !== "terminal";
1971
+ }
1972
+ async function inCall(tx, record) {
1973
+ return await isLive(tx, record.call);
1974
+ }
1975
+ async function statusOf(tx, record) {
1976
+ if (record.status !== "working" || record.reporters.length > 0 || await inCall(tx, record))
1977
+ return record.status;
1978
+ return "stopped";
1979
+ }
1980
+ async function workingCount(tx, root) {
1981
+ let count = 0;
1982
+ for (const record of Object.values((await tx.doc(SubagentsDoc, root)).agents)) {
1983
+ if (record.reporters.length > 0 || await inCall(tx, record))
1984
+ count++;
1985
+ }
1986
+ return count;
1987
+ }
1988
+ async function assertCapacity(tx, root) {
1989
+ if (await workingCount(tx, root) >= MAX_WORKING_SUBAGENTS) {
1990
+ throw new Error(`At most ${MAX_WORKING_SUBAGENTS} subagents can work at once in this conversation; wait for one to answer or stop one with subagent_stop`);
1991
+ }
1992
+ }
1993
+ function recordNamed(agents, name) {
1994
+ const found = Object.hasOwn(agents, name) ? agents[name] : undefined;
1995
+ if (found !== undefined)
1996
+ return found;
1997
+ const names = Object.keys(agents);
1998
+ throw new Error(names.length === 0 ? `No subagent named "${name}"` : `No subagent named "${name}". Subagents: ${names.join(", ")}`);
1999
+ }
2000
+ async function recordOf(tx, childId) {
2001
+ if ((await tx.conversation(childId))?.owner === undefined)
2002
+ return;
2003
+ const root = await familyRootIn(tx, childId);
2004
+ const agents = (await tx.doc(SubagentsDoc, root)).agents;
2005
+ for (const [name, record] of Object.entries(agents)) {
2006
+ if (record.conversationId === childId)
2007
+ return { root, name, record };
2008
+ }
2009
+ return;
2010
+ }
2011
+ var SubagentAnchor = defineTask2({
2012
+ name: "pinomad.subagent-anchor",
2013
+ version: 1,
2014
+ initial: () => ({ phase: "done" }),
2015
+ phases: {
2016
+ done: (_anchor, runtime, context) => runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: null } }), context)
2017
+ },
2018
+ abort: (_anchor, runtime, context) => runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), context)
2019
+ });
2020
+ var SubagentReporter = defineTask2({
2021
+ name: "pinomad.subagent-reporter",
2022
+ version: 1,
2023
+ initial: () => ({ phase: "deliver" }),
2024
+ phases: {
2025
+ deliver: async (reporter, runtime, context) => {
2026
+ const input = reporter.input;
2027
+ const child = await runtime.conversation(input.conversationId, context);
2028
+ const sender = await runtime.conversation(runtime.conversationId, context);
2029
+ await runtime.commit(async (tx) => {
2030
+ const record = (await tx.doc(SubagentsDoc, input.root)).agents[input.name];
2031
+ if (input.message === undefined) {
2032
+ if (record !== undefined)
2033
+ release(record, reporter.id, "idle");
2034
+ return { status: "terminal", outcome: { status: "completed", result: null } };
2035
+ }
2036
+ const message2 = input.message;
2037
+ if (child !== undefined) {
2038
+ if (record?.cancelled?.includes(reporter.id) === true) {
2039
+ await settle(tx, sender, reporter.id, input, "cancelled", runtime.now());
2040
+ return { status: "terminal", outcome: { status: "completed", result: null } };
2041
+ }
2042
+ await child.submit(tx, draftOf(reporter.id, message2));
2043
+ }
2044
+ return { status: "running", checkpoint: { phase: "wait" } };
2045
+ }, context);
2046
+ },
2047
+ wait: async (reporter, runtime, context) => {
2048
+ const input = reporter.input;
2049
+ const child = await runtime.conversation(input.conversationId, context);
2050
+ const message2 = input.message;
2051
+ const settled = child === undefined || message2 === undefined ? undefined : await (await child.submit(draftOf(reporter.id, message2), context)).wait(context);
2052
+ const sender = await runtime.conversation(runtime.conversationId, context);
2053
+ await runtime.commit(async (tx) => {
2054
+ if (message2 === undefined) {
2055
+ const record = (await tx.doc(SubagentsDoc, input.root)).agents[input.name];
2056
+ if (record !== undefined)
2057
+ release(record, reporter.id, "idle");
2058
+ } else {
2059
+ await settle(tx, sender, reporter.id, input, settled, runtime.now());
2060
+ }
2061
+ return { status: "terminal", outcome: { status: "completed", result: null } };
2062
+ }, context);
2063
+ }
2064
+ },
2065
+ abort: (reporter, runtime, context) => runtime.commit(async (tx) => {
2066
+ const record = (await tx.doc(SubagentsDoc, reporter.input.root)).agents[reporter.input.name];
2067
+ if (record !== undefined)
2068
+ release(record, reporter.id, "stopped");
2069
+ return { status: "terminal", outcome: { status: "aborted" } };
2070
+ }, context)
2071
+ });
2072
+ function draftOf(reporter, message2) {
2073
+ return { type: "input", content: message2, whenBusy: "steer", requestId: `subagent:${reporter}` };
2074
+ }
2075
+ function release(record, reporter, status) {
2076
+ record.reporters = record.reporters.filter((id) => id !== reporter);
2077
+ if (record.cancelled !== undefined)
2078
+ record.cancelled = record.cancelled.filter((id) => id !== reporter);
2079
+ if (record.reporters.length === 0)
2080
+ record.status = status;
2081
+ }
2082
+ function stopRecord(record, by) {
2083
+ record.stop = { by, noted: false };
2084
+ record.cancelled = [...record.reporters];
2085
+ }
2086
+ function steer(text) {
2087
+ return { type: "input", content: text, whenBusy: "steer" };
2088
+ }
2089
+ function note(text, now) {
2090
+ return { type: "write", entry: { kind: "pi.user", model: [{ role: "user", content: text, timestamp: now }] } };
2091
+ }
2092
+ async function settle(tx, sender, reporter, input, settled, now) {
2093
+ const record = (await tx.doc(SubagentsDoc, input.root)).agents[input.name];
2094
+ if (record === undefined)
2095
+ return;
2096
+ const name = input.name;
2097
+ let report;
2098
+ let status;
2099
+ if (settled !== "cancelled" && settled?.status === "done" && settled.type === "input") {
2100
+ status = "idle";
2101
+ const key = `${sender.id}:${settled.answer}`;
2102
+ if (record.reported[key] !== "steer") {
2103
+ report = steer(`${answeredHeader(name, record)}
2104
+
2105
+ ${await answerText(tx, settled.answer)}`);
2106
+ record.reported[key] = "steer";
2107
+ }
2108
+ } else if (settled === "cancelled" || settled?.status === "unanswered" && settled.reason === "aborted") {
2109
+ status = "stopped";
2110
+ const stop = record.stop ?? { by: "user", noted: false };
2111
+ if (stop.by === "user" && !stop.noted) {
2112
+ report = note(`[The user stopped subagent ${name}.]`, now);
2113
+ stop.noted = true;
2114
+ }
2115
+ record.stop = stop;
2116
+ } else {
2117
+ status = "failed";
2118
+ const reason = settled === undefined ? "its conversation no longer exists" : settled.status === "unanswered" ? settled.reason : "no answer";
2119
+ report = steer(`[subagent ${name} failed: ${reason}]`);
2120
+ }
2121
+ if (report !== undefined)
2122
+ await sender.submit(tx, { ...report, requestId: `subagent-report:${reporter}` });
2123
+ release(record, reporter, status);
2124
+ }
2125
+ function answeredHeader(name, record) {
2126
+ return record?.branch === undefined ? `[subagent ${name} answered]` : `[subagent ${name} answered on branch ${record.branch} (from ${record.base?.slice(0, 12) ?? "?"})]`;
2127
+ }
2128
+ async function answerText(tx, id) {
2129
+ const message2 = (await tx.entry(AssistantEntry, id))?.model?.[0];
2130
+ const text = message2?.role === "assistant" ? message2.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
2131
+ return text === "" ? "(The subagent returned no text.)" : text;
2132
+ }
2133
+ async function entriesSince(tx, record) {
2134
+ const entries = [];
2135
+ let cursor;
2136
+ do {
2137
+ const page = await tx.scanEntries({
2138
+ conversationId: record.conversationId,
2139
+ order: "ascending",
2140
+ ...record.readUpTo === undefined ? {} : { minEntryId: record.readUpTo + 1 }
2141
+ }, 256, cursor);
2142
+ entries.push(...page.items);
2143
+ cursor = page.next;
2144
+ } while (cursor !== undefined);
2145
+ return entries;
2146
+ }
2147
+ async function assertDispatchable(tx, root, name) {
2148
+ const record = recordNamed((await tx.doc(SubagentsDoc, root)).agents, name);
2149
+ if (await inCall(tx, record)) {
2150
+ throw new Error(`Subagent "${name}" is still answering the subagent call that dispatched it`);
2151
+ }
2152
+ if (record.reporters.length === 0)
2153
+ await assertCapacity(tx, root);
2154
+ return record;
2155
+ }
2156
+ async function dispatch(tx, options) {
2157
+ const record = recordNamed((await tx.doc(SubagentsDoc, options.root)).agents, options.name);
2158
+ const reporter = await tx.createTask(SubagentReporter, {
2159
+ root: options.root,
2160
+ name: options.name,
2161
+ conversationId: record.conversationId,
2162
+ message: options.message
2163
+ }, { ownership: { kind: "conversation" }, conversationId: options.sender, background: true });
2164
+ record.reporters.push(reporter);
2165
+ record.status = "working";
2166
+ delete record.stop;
2167
+ delete record.call;
2168
+ const doc = await tx.doc(SubagentDoc, record.conversationId);
2169
+ if (doc.lifecycle !== "background")
2170
+ doc.lifecycle = "background";
2171
+ return reporter;
2172
+ }
2173
+ async function markStop(tx, childId, by) {
2174
+ const found = await recordOf(tx, childId);
2175
+ if (found === undefined)
2176
+ return false;
2177
+ stopRecord(found.record, by);
2178
+ return true;
2179
+ }
2180
+
1304
2181
  // apps/host/src/extensions/subagent.ts
1305
- var INSTRUCTIONS = "You are a subagent working on one delegated task. You cannot ask the user questions; if you need a decision or clarification, stop and state exactly what you need in your final answer. Your final message is returned to the delegating agent as the result, so make it a complete, self-contained report.";
2182
+ var instructions = (name) => `You are a subagent named "${name}", working on tasks delegated to you. You cannot ask the user questions; if you need a decision or clarification, stop and state exactly what you need in your final answer. Each final answer is returned to whoever sent the message, so make it a complete, self-contained report. You may get more messages later from the delegating agent.`;
2183
+ var READ_LIMIT = 16000;
2184
+ function contentText(content) {
2185
+ return typeof content === "string" ? content : content.flatMap((block) => block.type === "text" && block.text !== undefined ? [block.text] : []).join("");
2186
+ }
1306
2187
  var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
1307
2188
  function createSubagent(options) {
1308
2189
  const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
@@ -1314,15 +2195,22 @@ function createSubagent(options) {
1314
2195
  const defined = roles();
1315
2196
  throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
1316
2197
  };
1317
- const tool = defineTool2({
2198
+ const tool = defineTool3({
1318
2199
  name: "subagent",
1319
2200
  callers: ["model"],
1320
- description: "Delegate a self-contained task to a subagent that runs in its own conversation and returns its final answer. " + "The subagent starts with none of this conversation's context, so put everything it needs in `task`. " + "It works in the same files as you: subagents called in the same turn run concurrently, so give parallel subagents read-only work or non-overlapping files. " + "It cannot ask the user questions. `model` and `thinkingLevel` override what it inherits from you; `role` delegates to a named agent; a model or thinking level it pins cannot be overridden; `description` only labels the call. " + `Available models: ${available()}.`,
2201
+ description: "Delegate a self-contained task to a named subagent that runs in its own conversation. " + "Without `background` the call blocks until the subagent answers and returns its answer; with `background: true` it returns at once, you keep working, and the answer is reported to you when it arrives. " + "Either way the subagent stays available afterwards: continue with subagent_send, stop it with subagent_stop, read its answers again with subagent_read, and list subagents with subagent_list. " + "The subagent starts with none of this conversation's context, so put everything it needs in `task`. " + '`checkout` is required: "shared" works in your files (use it for read-only work; parallel subagents must not edit the same files), "worktree" gets its own Git worktree for work that edits files. ' + "It cannot ask the user questions. `model` and `thinkingLevel` override what it inherits from you; `role` delegates to a named agent; a model or thinking level it pins cannot be overridden. " + `Available models: ${available()}.`,
1321
2202
  parameters: Type3.Object({
1322
2203
  task: Type3.String({
1323
2204
  minLength: 1,
1324
2205
  description: "Complete, self-contained instructions. The subagent sees none of this conversation."
1325
2206
  }),
2207
+ checkout: Type3.Union([Type3.Literal("shared"), Type3.Literal("worktree")], {
2208
+ description: '"shared": your working directory. "worktree": its own Git worktree from your HEAD; uncommitted changes are not carried over.'
2209
+ }),
2210
+ name: Type3.Optional(Type3.String({
2211
+ description: 'Optional unique name, e.g. "reviewer"; derived from `description` when omitted. Use it with the other subagent tools.'
2212
+ })),
2213
+ background: Type3.Optional(Type3.Boolean({ description: "Return at once instead of waiting; the answer is reported to you when it arrives." })),
1326
2214
  role: Type3.Optional(Type3.String({
1327
2215
  description: "Optional named agent to delegate to, from the Named agents list in your instructions. Its prompt applies, plus any model or thinking level it pins."
1328
2216
  })),
@@ -1332,16 +2220,39 @@ function createSubagent(options) {
1332
2220
  }),
1333
2221
  replay: "safe",
1334
2222
  execute: async (args, api, context) => {
2223
+ const background = args.background === true;
2224
+ let repo;
2225
+ if (args.checkout === "worktree") {
2226
+ if (options.worktree === undefined)
2227
+ throw new Error('checkout "worktree" is not available on this host; use "shared"');
2228
+ repo = await options.worktree.base(api.conversationId, context);
2229
+ }
1335
2230
  const child = await api.commit(async (tx) => {
1336
- const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
1337
- if (existing !== undefined) {
1338
- const doc2 = await tx.doc(SubagentDoc, existing.id);
2231
+ const root = await familyRootIn(tx, api.conversationId);
2232
+ const registry = await tx.doc(SubagentsDoc, root);
2233
+ const recorded = Object.entries(registry.agents).find(([, record]) => record.createdBy === api.taskId);
2234
+ const existingId = recorded?.[1].conversationId ?? (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0]?.id;
2235
+ if (existingId !== undefined) {
2236
+ const doc2 = await tx.doc(SubagentDoc, existingId);
1339
2237
  return {
1340
- id: existing.id,
2238
+ id: existingId,
2239
+ name: recorded?.[0] ?? doc2?.name,
2240
+ ...recorded?.[1].branch === undefined ? {} : { branch: recorded[1].branch, base: recorded[1].base },
1341
2241
  ...doc2?.model === undefined ? {} : { model: doc2.model },
1342
2242
  ...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
1343
2243
  };
1344
2244
  }
2245
+ const taken = (name3) => Object.hasOwn(registry.agents, name3);
2246
+ let name2;
2247
+ if (args.name !== undefined) {
2248
+ name2 = args.name.trim();
2249
+ assertValidName(name2);
2250
+ if (taken(name2))
2251
+ throw new Error(`A subagent named "${name2}" already exists; message it with subagent_send or pick another name`);
2252
+ } else {
2253
+ const words = args.description?.trim() || args.task.trim().split(/\s+/).slice(0, 4).join(" ");
2254
+ name2 = uniqueName(nameFromDescription(words), taken);
2255
+ }
1345
2256
  const role = args.role === undefined ? undefined : roleNamed(args.role);
1346
2257
  if (role?.model !== undefined && args.model !== undefined) {
1347
2258
  throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
@@ -1374,21 +2285,38 @@ function createSubagent(options) {
1374
2285
  thinkingLevel = clampThinkingLevel2(override, parent.thinkingLevel);
1375
2286
  }
1376
2287
  }
2288
+ await assertCapacity(tx, root);
1377
2289
  const change = {
1378
2290
  extensions: { remove: [extension, ...options.exclude] },
1379
- instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
2291
+ instructions: role === undefined || role.prompt === "" ? instructions(name2) : `${instructions(name2)}
1380
2292
 
1381
2293
  ${role.prompt}`,
1382
2294
  ...model === undefined ? {} : { model },
1383
2295
  ...thinkingLevel === undefined ? {} : { thinkingLevel }
1384
2296
  };
1385
- const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
1386
- await configure2(tx, created.id, change);
2297
+ const owner = background ? await tx.createTask(SubagentAnchor, null, {
2298
+ ownership: { kind: "conversation" },
2299
+ conversationId: api.conversationId,
2300
+ background: true
2301
+ }) : api.taskId;
2302
+ const created = await tx.createConversation({ ownership: { kind: "task", taskId: owner } });
2303
+ let worktree;
2304
+ if (repo !== undefined) {
2305
+ const path = options.worktree.path(created.id);
2306
+ worktree = { branch: worktreeBranch(created.id, branchSuffix()), base: repo.base, cwd: join6(path, repo.subdir) };
2307
+ const index = await tx.doc(IndexDoc);
2308
+ if (index.checkouts === undefined)
2309
+ index.checkouts = [];
2310
+ index.checkouts.push({ conversationId: created.id, path, repo: repo.repo, subdir: repo.subdir, branch: worktree.branch, base: repo.base });
2311
+ }
2312
+ await configure2(tx, created.id, worktree === undefined ? change : { ...change, cwd: worktree.cwd });
1387
2313
  const childAgent = await tx.doc(AgentDoc4, created.id);
1388
2314
  const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc4, api.conversationId)).model;
1389
2315
  const startedAt = Date.now();
1390
2316
  const doc = await tx.doc(SubagentDoc, created.id);
1391
2317
  doc.startedAt = startedAt;
2318
+ doc.name = name2;
2319
+ doc.lifecycle = background ? "background" : "foreground";
1392
2320
  if (ref !== undefined)
1393
2321
  doc.model = `${ref.provider}/${ref.modelId}`;
1394
2322
  if (role !== undefined)
@@ -1396,33 +2324,183 @@ ${role.prompt}`,
1396
2324
  const label = args.description?.trim();
1397
2325
  if (label !== undefined && label !== "")
1398
2326
  doc.label = label;
1399
- return { id: created.id, ...ref === undefined ? {} : { model: doc.model }, startedAt };
2327
+ registry.agents[name2] = {
2328
+ conversationId: created.id,
2329
+ dispatcher: api.conversationId,
2330
+ createdBy: api.taskId,
2331
+ ...background ? {} : { call: api.taskId },
2332
+ checkout: worktree === undefined ? "shared" : "worktree",
2333
+ ...worktree === undefined ? {} : { branch: worktree.branch, base: worktree.base },
2334
+ ...label === undefined || label === "" ? {} : { description: label },
2335
+ status: "working",
2336
+ reporters: [],
2337
+ reported: {}
2338
+ };
2339
+ if (background) {
2340
+ await dispatch(tx, { root, name: name2, sender: api.conversationId, message: args.task });
2341
+ }
2342
+ return {
2343
+ id: created.id,
2344
+ name: name2,
2345
+ ...worktree === undefined ? {} : { branch: worktree.branch, base: worktree.base },
2346
+ ...ref === undefined ? {} : { model: doc.model },
2347
+ startedAt
2348
+ };
1400
2349
  }, context);
1401
- await api.details({ conversationId: child.id, ...child.model === undefined ? {} : { model: child.model }, ...child.startedAt === undefined ? {} : { startedAt: child.startedAt } }, context);
2350
+ const details = {
2351
+ conversationId: child.id,
2352
+ ...child.name === undefined ? {} : { name: child.name },
2353
+ ...background ? { background: true } : {},
2354
+ ...child.branch === undefined ? {} : { branch: child.branch, base: child.base },
2355
+ ...child.model === undefined ? {} : { model: child.model },
2356
+ ...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
2357
+ };
2358
+ const name = child.name ?? "subagent";
2359
+ if (background) {
2360
+ return {
2361
+ output: [
2362
+ {
2363
+ type: "text",
2364
+ text: `Started subagent "${name}" in the background` + (child.branch === undefined ? "" : ` on its own worktree branch ${child.branch} (from ${child.base?.slice(0, 12)})`) + ". Its answer will be reported to you when it arrives; message it with subagent_send."
2365
+ }
2366
+ ],
2367
+ details
2368
+ };
2369
+ }
2370
+ await api.details(details, context);
1402
2371
  const handle = await api.conversation(child.id, context);
1403
2372
  if (handle === undefined)
1404
2373
  throw new Error(`Subagent conversation ${child.id} does not exist`);
1405
2374
  const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
1406
2375
  const settled = await (await handle.submit(request, context)).wait(context);
1407
- if (settled.status !== "done" || settled.type !== "input")
1408
- throw new Error(`Subagent failed: ${settled.status}`);
1409
- const answer = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
1410
- const message = answer?.model?.[0];
1411
- const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
1412
- const result2 = {
1413
- output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
1414
- details: {
1415
- conversationId: child.id,
1416
- ...child.model === undefined ? {} : { model: child.model },
1417
- ...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
2376
+ const answered = settled.status === "done" && settled.type === "input";
2377
+ const text = await api.commit(async (tx) => {
2378
+ const found = await recordOf(tx, child.id);
2379
+ if (found !== undefined) {
2380
+ delete found.record.call;
2381
+ if (found.record.reporters.length === 0) {
2382
+ found.record.status = answered ? "idle" : settled.status === "unanswered" && settled.reason === "aborted" ? "stopped" : "failed";
2383
+ }
2384
+ if (answered)
2385
+ found.record.reported[`${api.conversationId}:${settled.answer}`] = "steer";
1418
2386
  }
2387
+ return answered ? await answerText(tx, settled.answer) : undefined;
2388
+ }, context);
2389
+ if (text === undefined)
2390
+ throw new Error(`Subagent failed: ${settled.status === "unanswered" ? settled.reason : settled.status}`);
2391
+ return {
2392
+ output: [{ type: "text", text: `${answeredHeader(name, child)}
2393
+
2394
+ ${text}` }],
2395
+ details
2396
+ };
2397
+ }
2398
+ });
2399
+ const send = defineTool3({
2400
+ name: "subagent_send",
2401
+ callers: ["model"],
2402
+ description: "Send a message to one of your subagents (started with `subagent`), for example a follow-up question or new instructions. Returns at once; its answer is reported to you when it arrives. A message to a subagent that is still working steers its current answer.",
2403
+ parameters: Type3.Object({
2404
+ name: Type3.String({ description: "The subagent's name." }),
2405
+ message: Type3.String({ minLength: 1, description: "Self-contained message; it sees only its own conversation." })
2406
+ }),
2407
+ replay: "unsafe",
2408
+ execute: async (args, api, context) => {
2409
+ const conversationId = await api.commit(async (tx) => {
2410
+ const root = await familyRootIn(tx, api.conversationId);
2411
+ const record = await assertDispatchable(tx, root, args.name);
2412
+ await dispatch(tx, { root, name: args.name, sender: api.conversationId, message: args.message });
2413
+ return record.conversationId;
2414
+ }, context);
2415
+ return {
2416
+ output: [{ type: "text", text: `Sent to subagent "${args.name}". Its answer will be reported to you when it arrives.` }],
2417
+ details: { conversationId, name: args.name }
1419
2418
  };
1420
- return result2;
1421
2419
  }
1422
2420
  });
1423
- const extension = defineExtension3({
2421
+ const stop = defineTool3({
2422
+ name: "subagent_stop",
2423
+ callers: ["model"],
2424
+ description: "Stop what one of your subagents is doing now and drop its queued messages. It stays available for subagent_send.",
2425
+ parameters: Type3.Object({ name: Type3.String({ description: "The subagent's name." }) }),
2426
+ replay: "unsafe",
2427
+ execute: async (args, api, context) => {
2428
+ const conversationId = await api.commit(async (tx) => {
2429
+ const root = await familyRootIn(tx, api.conversationId);
2430
+ const record = recordNamed((await tx.doc(SubagentsDoc, root)).agents, args.name);
2431
+ stopRecord(record, "model");
2432
+ return record.conversationId;
2433
+ }, context);
2434
+ const handle = await api.conversation(conversationId, context);
2435
+ if (handle === undefined)
2436
+ throw new Error(`Subagent conversation ${conversationId} does not exist`);
2437
+ await handle.abort(context);
2438
+ return {
2439
+ output: [{ type: "text", text: `Stopped subagent "${args.name}". It keeps its conversation; message it again with subagent_send.` }],
2440
+ details: { conversationId, name: args.name }
2441
+ };
2442
+ }
2443
+ });
2444
+ const read = defineTool3({
2445
+ name: "subagent_read",
2446
+ callers: ["model"],
2447
+ description: "Read the answers one of your subagents gave since you last read it, without its tool calls — for example after your own context was compacted.",
2448
+ parameters: Type3.Object({ name: Type3.String({ description: "The subagent's name." }) }),
2449
+ replay: "unsafe",
2450
+ execute: async (args, api, context) => {
2451
+ const text = await api.commit(async (tx) => {
2452
+ const root = await familyRootIn(tx, api.conversationId);
2453
+ const record = recordNamed((await tx.doc(SubagentsDoc, root)).agents, args.name);
2454
+ const entries = await entriesSince(tx, record);
2455
+ const lines = [];
2456
+ for (const entry of entries) {
2457
+ const message2 = entry.model?.[0];
2458
+ if (entry.kind !== "pi.assistant" || message2?.role !== "assistant")
2459
+ continue;
2460
+ const said = contentText(message2.content);
2461
+ if (said !== "")
2462
+ lines.push(`${args.name}: ${said}`);
2463
+ }
2464
+ const last = entries.at(-1);
2465
+ if (last !== undefined)
2466
+ record.readUpTo = last.id;
2467
+ if (lines.length === 0)
2468
+ return `Nothing new in subagent "${args.name}" since you last read it.`;
2469
+ const joined = lines.join(`
2470
+
2471
+ `);
2472
+ return joined.length <= READ_LIMIT ? joined : `(Earlier answers omitted.)
2473
+
2474
+ ${joined.slice(-READ_LIMIT)}`;
2475
+ }, context);
2476
+ return { output: [{ type: "text", text }] };
2477
+ }
2478
+ });
2479
+ const list = defineTool3({
2480
+ name: "subagent_list",
2481
+ callers: ["model"],
2482
+ description: "List the subagents of this conversation with their status: working, idle, stopped, or failed.",
2483
+ parameters: Type3.Object({}),
2484
+ replay: "safe",
2485
+ execute: async (_args, api, context) => {
2486
+ const text = await api.commit(async (tx) => {
2487
+ const root = await familyRootIn(tx, api.conversationId);
2488
+ const lines = [];
2489
+ for (const [name, record] of Object.entries((await tx.doc(SubagentsDoc, root)).agents)) {
2490
+ const label = record.description === undefined ? "" : ` — ${record.description}`;
2491
+ const checkout = record.branch === undefined ? record.checkout : `worktree, branch ${record.branch}`;
2492
+ lines.push(`- ${name}: ${await statusOf(tx, record)}${label} (checkout: ${checkout})`);
2493
+ }
2494
+ return lines.length === 0 ? "No subagents." : lines.join(`
2495
+ `);
2496
+ }, context);
2497
+ return { output: [{ type: "text", text }] };
2498
+ }
2499
+ });
2500
+ const extension = defineExtension5({
1424
2501
  name: "subagent",
1425
- tools: [tool],
2502
+ tools: [tool, send, stop, read, list],
2503
+ tasks: [SubagentAnchor, SubagentReporter],
1426
2504
  sections: [
1427
2505
  section2("agent_roles", () => {
1428
2506
  const defined = roles();
@@ -1440,9 +2518,9 @@ ${role.prompt}`,
1440
2518
  }
1441
2519
 
1442
2520
  // apps/host/src/extensions/todo.ts
1443
- import { defineDoc as defineDoc5, defineExtension as defineExtension4, defineTool as defineTool3 } from "@earendil-works/pi-durable";
2521
+ import { defineDoc as defineDoc8, defineExtension as defineExtension6, defineTool as defineTool4 } from "@earendil-works/pi-durable";
1444
2522
  import { Type as Type4 } from "typebox";
1445
- var TodoDoc = defineDoc5({
2523
+ var TodoDoc = defineDoc8({
1446
2524
  kind: "todo.list",
1447
2525
  version: 1,
1448
2526
  scope: "conversation",
@@ -1451,7 +2529,7 @@ var TodoDoc = defineDoc5({
1451
2529
  initial: () => ({ items: [] }),
1452
2530
  checkpointWhen: () => false
1453
2531
  });
1454
- var todoWrite = defineTool3({
2532
+ var todoWrite = defineTool4({
1455
2533
  name: "todo_write",
1456
2534
  description: "Replace the todo list shown to the user with the given items.",
1457
2535
  parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
@@ -1465,7 +2543,7 @@ var todoWrite = defineTool3({
1465
2543
  }
1466
2544
  });
1467
2545
  var todo = {
1468
- extension: defineExtension4({ name: "todo", tools: [todoWrite] }),
2546
+ extension: defineExtension6({ name: "todo", tools: [todoWrite] }),
1469
2547
  docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
1470
2548
  };
1471
2549
 
@@ -1473,8 +2551,8 @@ var todo = {
1473
2551
  import { stat as stat6 } from "node:fs/promises";
1474
2552
  import { createServer } from "node:http";
1475
2553
  import { homedir, networkInterfaces } from "node:os";
1476
- import { join as join7, sep as sep5 } from "node:path";
1477
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
2554
+ import { join as join8, sep as sep6 } from "node:path";
2555
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
1478
2556
  import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
1479
2557
  import {
1480
2558
  AgentDoc as AgentDoc5,
@@ -1483,7 +2561,7 @@ import {
1483
2561
  LiveDoc
1484
2562
  } from "@earendil-works/pi-durable";
1485
2563
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
1486
- import { Value as Value2 } from "typebox/value";
2564
+ import { Value } from "typebox/value";
1487
2565
 
1488
2566
  // apps/host/src/secure-carrier.ts
1489
2567
  import { WebSocket } from "ws";
@@ -1519,7 +2597,7 @@ function webSocketCarrier(socket) {
1519
2597
  }
1520
2598
 
1521
2599
  // apps/host/src/rtc.ts
1522
- import { randomUUID } from "node:crypto";
2600
+ import { randomUUID as randomUUID2 } from "node:crypto";
1523
2601
  var BUFFER_HIGH_WATER = 256 * 1024;
1524
2602
  var BUFFER_LOW_WATER = 64 * 1024;
1525
2603
  var PUMP_STALL_MS = 1e4;
@@ -1778,7 +2856,7 @@ function nodeRtcFactory(loaded) {
1778
2856
  const max = parseRtcMaxMessageSize(input.offerSdp);
1779
2857
  if (max.kind === "invalid")
1780
2858
  throw new Error(max.reason);
1781
- const pc = new ndc.PeerConnection(`pinomad-${randomUUID()}`, { iceServers: [...input.stun] });
2859
+ const pc = new ndc.PeerConnection(`pinomad-${randomUUID2()}`, { iceServers: [...input.stun] });
1782
2860
  const timers = input.timers;
1783
2861
  const candidates = new Set;
1784
2862
  let localSdp;
@@ -2037,17 +3115,17 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
2037
3115
  stallTimer = timers.setTimeout(stall, 1000);
2038
3116
  };
2039
3117
  channel.onBufferedAmountLow(() => pump());
2040
- channel.onMessage((message) => {
2041
- if (typeof message === "string")
3118
+ channel.onMessage((message2) => {
3119
+ if (typeof message2 === "string")
2042
3120
  handlers?.text();
2043
- else if (message instanceof ArrayBuffer)
2044
- handlers?.binary(new Uint8Array(message));
3121
+ else if (message2 instanceof ArrayBuffer)
3122
+ handlers?.binary(new Uint8Array(message2));
2045
3123
  else
2046
- handlers?.binary(message);
3124
+ handlers?.binary(message2);
2047
3125
  });
2048
3126
  channel.onClosed(finish);
2049
- channel.onError((message) => {
2050
- log("rtc-datachannel-error", { message });
3127
+ channel.onError((message2) => {
3128
+ log("rtc-datachannel-error", { message: message2 });
2051
3129
  finish();
2052
3130
  });
2053
3131
  return {
@@ -2077,12 +3155,218 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
2077
3155
  }
2078
3156
 
2079
3157
  // apps/host/src/terminals.ts
2080
- import { randomUUID as randomUUID2 } from "node:crypto";
3158
+ import { randomUUID as randomUUID3 } from "node:crypto";
2081
3159
  import { userInfo } from "node:os";
2082
- var TERMINAL_LIMIT = 8;
3160
+
3161
+ // apps/host/src/output-buffer.ts
2083
3162
  var BUFFER_SIZE = 512 * 1024;
2084
3163
  var FLUSH_MS = 8;
2085
3164
  var MAX_CHUNK = 16 * 1024;
3165
+
3166
+ class OutputBuffer {
3167
+ #bufferSize;
3168
+ #flushMs;
3169
+ #maxChunk;
3170
+ #onFlush;
3171
+ #viewers = new Set;
3172
+ #buffer = "";
3173
+ #start = 0;
3174
+ #pending = "";
3175
+ #timer;
3176
+ #ended = false;
3177
+ constructor(options = {}) {
3178
+ this.#bufferSize = options.bufferSize ?? BUFFER_SIZE;
3179
+ this.#flushMs = options.flushMs ?? FLUSH_MS;
3180
+ this.#maxChunk = options.maxChunk ?? MAX_CHUNK;
3181
+ this.#onFlush = options.onFlush;
3182
+ }
3183
+ get start() {
3184
+ return this.#start;
3185
+ }
3186
+ get end() {
3187
+ return this.#start + this.#buffer.length;
3188
+ }
3189
+ get length() {
3190
+ return this.#buffer.length;
3191
+ }
3192
+ get ended() {
3193
+ return this.#ended;
3194
+ }
3195
+ push(data) {
3196
+ if (this.#ended || data === "")
3197
+ return;
3198
+ this.#pending += data;
3199
+ if (this.#pending.length >= this.#maxChunk)
3200
+ this.flush();
3201
+ else
3202
+ this.#timer ??= setTimeout(() => this.flush(), this.#flushMs);
3203
+ }
3204
+ flush() {
3205
+ if (this.#timer !== undefined)
3206
+ clearTimeout(this.#timer);
3207
+ this.#timer = undefined;
3208
+ const data = this.#pending;
3209
+ if (data === "")
3210
+ return;
3211
+ this.#pending = "";
3212
+ this.#buffer += data;
3213
+ this.#trim();
3214
+ this.#onFlush?.(data);
3215
+ for (const viewer of this.#viewers)
3216
+ this.#pump(viewer);
3217
+ }
3218
+ discard() {
3219
+ if (this.#timer !== undefined)
3220
+ clearTimeout(this.#timer);
3221
+ this.#timer = undefined;
3222
+ const gap = this.#buffer.length + this.#pending.length + 1;
3223
+ this.#buffer = "";
3224
+ this.#pending = "";
3225
+ this.#start += gap;
3226
+ for (const viewer of this.#viewers)
3227
+ viewer.position = undefined;
3228
+ }
3229
+ close(reason) {
3230
+ if (this.#ended)
3231
+ return;
3232
+ if (this.#timer !== undefined)
3233
+ clearTimeout(this.#timer);
3234
+ this.#timer = undefined;
3235
+ this.#ended = true;
3236
+ for (const viewer of this.#viewers)
3237
+ viewer.sink.ended(reason);
3238
+ this.#viewers.clear();
3239
+ }
3240
+ attach(since, sink) {
3241
+ const position = since !== undefined && since >= this.#start && since <= this.end ? since : undefined;
3242
+ const viewer = { sink, position, sending: false };
3243
+ this.#viewers.add(viewer);
3244
+ this.#pump(viewer);
3245
+ return () => {
3246
+ this.#viewers.delete(viewer);
3247
+ };
3248
+ }
3249
+ snapshot(since, maxChunk = this.#maxChunk) {
3250
+ const end = this.end;
3251
+ const from = since === undefined ? this.#start : Math.min(Math.max(since, this.#start), end);
3252
+ let to = Math.min(end, from + maxChunk);
3253
+ if (to < end) {
3254
+ const partial = partialEscapeStart(this.#buffer, from - this.#start, to - this.#start);
3255
+ if (partial !== undefined && this.#start + partial > from)
3256
+ to = this.#start + partial;
3257
+ }
3258
+ if (to < end && isHighSurrogate(this.#buffer.charCodeAt(to - this.#start - 1)))
3259
+ to--;
3260
+ const text = this.#buffer.slice(from - this.#start, to - this.#start);
3261
+ return {
3262
+ text,
3263
+ next: to,
3264
+ truncated: since === undefined ? this.#start > 0 : since < this.#start
3265
+ };
3266
+ }
3267
+ #trim() {
3268
+ if (this.#buffer.length <= this.#bufferSize)
3269
+ return;
3270
+ let cut2 = this.#buffer.length - this.#bufferSize;
3271
+ const line = this.#buffer.indexOf(`
3272
+ `, cut2);
3273
+ if (line !== -1 && line - cut2 < 1024)
3274
+ cut2 = line + 1;
3275
+ this.#buffer = this.#buffer.slice(cut2);
3276
+ this.#start += cut2;
3277
+ }
3278
+ async#pump(viewer) {
3279
+ if (viewer.sending)
3280
+ return;
3281
+ viewer.sending = true;
3282
+ try {
3283
+ while (this.#viewers.has(viewer)) {
3284
+ const end = this.end;
3285
+ const reset = viewer.position === undefined || viewer.position < this.#start;
3286
+ const from = reset ? this.#start : viewer.position;
3287
+ if (reset && end === this.#start)
3288
+ return;
3289
+ if (!reset && from >= end)
3290
+ return;
3291
+ let to = reset ? end : Math.min(end, from + this.#maxChunk);
3292
+ if (to < end && isHighSurrogate(this.#buffer.charCodeAt(to - this.#start - 1)))
3293
+ to--;
3294
+ const data = this.#buffer.slice(from - this.#start, to - this.#start);
3295
+ viewer.position = to;
3296
+ await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
3297
+ }
3298
+ } finally {
3299
+ viewer.sending = false;
3300
+ }
3301
+ }
3302
+ }
3303
+ function isHighSurrogate(code) {
3304
+ return code >= 55296 && code <= 56319;
3305
+ }
3306
+ function partialEscapeStart(text, from, to) {
3307
+ let index = from;
3308
+ while (index < to) {
3309
+ if (text.charCodeAt(index) !== 27) {
3310
+ index++;
3311
+ continue;
3312
+ }
3313
+ const end = escapeEnd(text, index, to);
3314
+ if (end === -1)
3315
+ return index;
3316
+ index = end;
3317
+ }
3318
+ return;
3319
+ }
3320
+ function escapeEnd(text, start, limit) {
3321
+ let index = start + 1;
3322
+ if (index >= limit)
3323
+ return -1;
3324
+ const kind = text.charCodeAt(index);
3325
+ if (kind === 91) {
3326
+ index++;
3327
+ while (index < limit) {
3328
+ const code = text.charCodeAt(index);
3329
+ if (code >= 64 && code <= 126)
3330
+ return index + 1;
3331
+ index++;
3332
+ }
3333
+ return -1;
3334
+ }
3335
+ if (kind === 93) {
3336
+ index++;
3337
+ while (index < limit) {
3338
+ const code = text.charCodeAt(index);
3339
+ if (code === 7)
3340
+ return index + 1;
3341
+ if (code === 27 && index + 1 < limit && text.charCodeAt(index + 1) === 92)
3342
+ return index + 2;
3343
+ index++;
3344
+ }
3345
+ return -1;
3346
+ }
3347
+ if (kind === 80 || kind === 88 || kind === 94 || kind === 95) {
3348
+ index++;
3349
+ while (index < limit) {
3350
+ if (text.charCodeAt(index) === 27 && index + 1 < limit && text.charCodeAt(index + 1) === 92) {
3351
+ return index + 2;
3352
+ }
3353
+ index++;
3354
+ }
3355
+ return -1;
3356
+ }
3357
+ while (index < limit) {
3358
+ const code = text.charCodeAt(index);
3359
+ if (code >= 32 && code <= 47) {
3360
+ index++;
3361
+ continue;
3362
+ }
3363
+ return index + 1;
3364
+ }
3365
+ return -1;
3366
+ }
3367
+
3368
+ // apps/host/src/terminals.ts
3369
+ var TERMINAL_LIMIT = 8;
2086
3370
  var MAX_TITLE = 1024;
2087
3371
  var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
2088
3372
 
@@ -2122,7 +3406,7 @@ class Terminals {
2122
3406
  const number = (this.#numbers.get(owner) ?? 0) + 1;
2123
3407
  this.#numbers.set(owner, number);
2124
3408
  const terminal = {
2125
- id: randomUUID2(),
3409
+ id: randomUUID3(),
2126
3410
  owner,
2127
3411
  cwd,
2128
3412
  number,
@@ -2133,12 +3417,16 @@ class Terminals {
2133
3417
  title: undefined,
2134
3418
  rename: undefined,
2135
3419
  exit: undefined,
2136
- buffer: "",
2137
- start: 0,
2138
- pending: "",
2139
- flushTimer: undefined,
2140
- titleCarry: "",
2141
- viewers: new Set
3420
+ output: new OutputBuffer({
3421
+ ...this.#options.bufferSize === undefined ? {} : { bufferSize: this.#options.bufferSize },
3422
+ ...this.#options.flushMs === undefined ? {} : { flushMs: this.#options.flushMs },
3423
+ ...this.#options.maxChunk === undefined ? {} : { maxChunk: this.#options.maxChunk },
3424
+ onFlush: (data) => {
3425
+ if (this.#readTitle(terminal, data))
3426
+ this.#changed(owner);
3427
+ }
3428
+ }),
3429
+ titleCarry: ""
2142
3430
  };
2143
3431
  this.#terminals.set(terminal.id, terminal);
2144
3432
  try {
@@ -2168,16 +3456,12 @@ class Terminals {
2168
3456
  restart(id, size) {
2169
3457
  const terminal = this.#get(id);
2170
3458
  this.#stop(terminal);
2171
- terminal.start += terminal.buffer.length + terminal.pending.length + 1;
2172
- terminal.buffer = "";
2173
- terminal.pending = "";
3459
+ terminal.output.discard();
2174
3460
  terminal.titleCarry = "";
2175
3461
  terminal.title = undefined;
2176
3462
  terminal.exit = undefined;
2177
3463
  terminal.cols = size.cols;
2178
3464
  terminal.rows = size.rows;
2179
- for (const viewer of terminal.viewers)
2180
- viewer.position = undefined;
2181
3465
  try {
2182
3466
  this.#start(terminal);
2183
3467
  } catch (error) {
@@ -2206,15 +3490,7 @@ class Terminals {
2206
3490
  this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
2207
3491
  }
2208
3492
  attach(id, since, sink) {
2209
- const terminal = this.#get(id);
2210
- const end = terminal.start + terminal.buffer.length;
2211
- const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
2212
- const viewer = { sink, position, sending: false };
2213
- terminal.viewers.add(viewer);
2214
- this.#pump(terminal, viewer);
2215
- return () => {
2216
- terminal.viewers.delete(viewer);
2217
- };
3493
+ return this.#get(id).output.attach(since, sink);
2218
3494
  }
2219
3495
  #get(id) {
2220
3496
  const terminal = this.#terminals.get(id);
@@ -2236,25 +3512,19 @@ class Terminals {
2236
3512
  pty.onData((data) => {
2237
3513
  if (terminal.generation !== generation)
2238
3514
  return;
2239
- terminal.pending += data;
2240
- if (terminal.pending.length >= (this.#options.maxChunk ?? MAX_CHUNK))
2241
- this.#flush(terminal);
2242
- else
2243
- terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
3515
+ this.#options.onOutput?.();
3516
+ terminal.output.push(data);
2244
3517
  });
2245
3518
  pty.onExit(({ exitCode, signal }) => {
2246
3519
  if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
2247
3520
  return;
2248
- this.#flush(terminal);
3521
+ terminal.output.flush();
2249
3522
  terminal.pty = undefined;
2250
3523
  terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
2251
3524
  this.#changed(terminal.owner);
2252
3525
  });
2253
3526
  }
2254
3527
  #stop(terminal) {
2255
- if (terminal.flushTimer !== undefined)
2256
- clearTimeout(terminal.flushTimer);
2257
- terminal.flushTimer = undefined;
2258
3528
  terminal.generation++;
2259
3529
  const pty = terminal.pty;
2260
3530
  terminal.pty = undefined;
@@ -2264,10 +3534,8 @@ class Terminals {
2264
3534
  }
2265
3535
  #remove(terminal) {
2266
3536
  this.#stop(terminal);
3537
+ terminal.output.close("Terminal closed");
2267
3538
  this.#terminals.delete(terminal.id);
2268
- for (const viewer of terminal.viewers)
2269
- viewer.sink.ended("Terminal closed");
2270
- terminal.viewers.clear();
2271
3539
  }
2272
3540
  #resize(terminal, size) {
2273
3541
  if (terminal.cols === size.cols && terminal.rows === size.rows)
@@ -2279,30 +3547,6 @@ class Terminals {
2279
3547
  } catch {}
2280
3548
  this.#changed(terminal.owner);
2281
3549
  }
2282
- #flush(terminal) {
2283
- if (terminal.flushTimer !== undefined)
2284
- clearTimeout(terminal.flushTimer);
2285
- terminal.flushTimer = undefined;
2286
- const data = terminal.pending;
2287
- if (data === "")
2288
- return;
2289
- terminal.pending = "";
2290
- terminal.buffer += data;
2291
- const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
2292
- if (terminal.buffer.length > bufferSize) {
2293
- let cut = terminal.buffer.length - bufferSize;
2294
- const line = terminal.buffer.indexOf(`
2295
- `, cut);
2296
- if (line !== -1 && line - cut < 1024)
2297
- cut = line + 1;
2298
- terminal.buffer = terminal.buffer.slice(cut);
2299
- terminal.start += cut;
2300
- }
2301
- if (this.#readTitle(terminal, data))
2302
- this.#changed(terminal.owner);
2303
- for (const viewer of terminal.viewers)
2304
- this.#pump(terminal, viewer);
2305
- }
2306
3550
  #readTitle(terminal, data) {
2307
3551
  const text = terminal.titleCarry + data;
2308
3552
  let title;
@@ -2318,29 +3562,6 @@ class Terminals {
2318
3562
  terminal.title = title;
2319
3563
  return terminal.rename === undefined;
2320
3564
  }
2321
- async#pump(terminal, viewer) {
2322
- if (viewer.sending)
2323
- return;
2324
- viewer.sending = true;
2325
- try {
2326
- const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
2327
- while (terminal.viewers.has(viewer)) {
2328
- const end = terminal.start + terminal.buffer.length;
2329
- const reset = viewer.position === undefined || viewer.position < terminal.start;
2330
- const from = reset ? terminal.start : viewer.position;
2331
- if (!reset && from >= end)
2332
- return;
2333
- let to = reset ? end : Math.min(end, from + maxChunk);
2334
- if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
2335
- to--;
2336
- const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
2337
- viewer.position = to;
2338
- await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
2339
- }
2340
- } finally {
2341
- viewer.sending = false;
2342
- }
2343
- }
2344
3565
  #changed(owner) {
2345
3566
  const watchers = this.#watchers.get(owner);
2346
3567
  if (watchers === undefined)
@@ -2351,20 +3572,20 @@ class Terminals {
2351
3572
  }
2352
3573
  }
2353
3574
  async function startTerminals(options = {}) {
2354
- let spawn3 = options.spawn;
2355
- if (spawn3 === undefined) {
3575
+ let spawn4 = options.spawn;
3576
+ if (spawn4 === undefined) {
2356
3577
  try {
2357
3578
  const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
2358
3579
  if (typeof loaded.spawn !== "function")
2359
3580
  throw new Error("the module has no spawn()");
2360
- spawn3 = loaded.spawn;
3581
+ spawn4 = loaded.spawn;
2361
3582
  } catch (error) {
2362
3583
  const detail = error instanceof Error ? error.message : String(error);
2363
3584
  (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})`);
2364
3585
  return;
2365
3586
  }
2366
3587
  }
2367
- return new Terminals({ ...options, spawn: spawn3, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
3588
+ return new Terminals({ ...options, spawn: spawn4, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
2368
3589
  }
2369
3590
  function loginShell() {
2370
3591
  let file = process.env.SHELL;
@@ -2378,12 +3599,12 @@ function loginShell() {
2378
3599
  return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
2379
3600
  }
2380
3601
  function shellEnv(env) {
2381
- const result2 = {};
3602
+ const result3 = {};
2382
3603
  for (const [key, value] of Object.entries(env)) {
2383
3604
  if (value !== undefined && !key.startsWith("PINOMAD_"))
2384
- result2[key] = value;
3605
+ result3[key] = value;
2385
3606
  }
2386
- return result2;
3607
+ return result3;
2387
3608
  }
2388
3609
  function info(terminal) {
2389
3610
  return {
@@ -2395,13 +3616,10 @@ function info(terminal) {
2395
3616
  ...terminal.exit === undefined ? {} : { exit: terminal.exit }
2396
3617
  };
2397
3618
  }
2398
- function isHighSurrogate(code) {
2399
- return code >= 55296 && code <= 56319;
2400
- }
2401
3619
 
2402
3620
  // apps/host/src/folders.ts
2403
3621
  import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
2404
- import { isAbsolute, join as join6 } from "node:path";
3622
+ import { isAbsolute, join as join7 } from "node:path";
2405
3623
  var denied = (error) => {
2406
3624
  const code = error?.code;
2407
3625
  return code === "EACCES" || code === "EPERM";
@@ -2431,10 +3649,10 @@ async function listFolders(path, home) {
2431
3649
  continue;
2432
3650
  let isDirectory = entry.isDirectory();
2433
3651
  if (!isDirectory && entry.isSymbolicLink()) {
2434
- isDirectory = await stat5(join6(dir, entry.name)).then((target) => target.isDirectory(), () => false);
3652
+ isDirectory = await stat5(join7(dir, entry.name)).then((target) => target.isDirectory(), () => false);
2435
3653
  }
2436
3654
  if (isDirectory)
2437
- folders.push({ name: entry.name, path: join6(dir, entry.name) });
3655
+ folders.push({ name: entry.name, path: join7(dir, entry.name) });
2438
3656
  }
2439
3657
  folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
2440
3658
  return { path: dir, home: await realpath5(home).catch(() => home), folders };
@@ -2620,7 +3838,7 @@ function imageProblem(images) {
2620
3838
  // apps/host/src/image-refs.ts
2621
3839
  import { createHash } from "node:crypto";
2622
3840
  import { applyImmutable } from "@earendil-works/chord/delta";
2623
- var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3841
+ var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2624
3842
  var isImageBlock = (value) => value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
2625
3843
  var hashed = new WeakMap;
2626
3844
  function refOf(block) {
@@ -2712,7 +3930,7 @@ function rewriteEntries(value) {
2712
3930
  return out ?? value;
2713
3931
  }
2714
3932
  function rewriteEntry(value) {
2715
- if (!isObject(value) || value.model === undefined)
3933
+ if (!isObject2(value) || value.model === undefined)
2716
3934
  return value;
2717
3935
  const model = rewriteMessages(value.model);
2718
3936
  return model === value.model ? value : { ...value, model };
@@ -2729,7 +3947,7 @@ function rewriteMessages(value) {
2729
3947
  return out ?? value;
2730
3948
  }
2731
3949
  function rewriteMessage(value) {
2732
- if (!isObject(value) || value.content === undefined)
3950
+ if (!isObject2(value) || value.content === undefined)
2733
3951
  return value;
2734
3952
  const content = rewriteContent(value.content);
2735
3953
  return content === value.content ? value : { ...value, content };
@@ -2740,7 +3958,7 @@ function rewriteContent(value) {
2740
3958
  let out;
2741
3959
  for (let i = 0;i < value.length; i++) {
2742
3960
  const block = value[i];
2743
- const next = isObject(block) && isImageBlock(block) ? withRef(block) : block;
3961
+ const next = isObject2(block) && isImageBlock(block) ? withRef(block) : block;
2744
3962
  if (next !== block)
2745
3963
  (out ??= value.slice())[i] = next;
2746
3964
  }
@@ -2758,31 +3976,31 @@ function rewriteItems(value) {
2758
3976
  return out ?? value;
2759
3977
  }
2760
3978
  function rewriteItem(value) {
2761
- if (!isObject(value) || value.content === undefined)
3979
+ if (!isObject2(value) || value.content === undefined)
2762
3980
  return value;
2763
3981
  const content = rewriteContent(value.content);
2764
3982
  return content === value.content ? value : { ...value, content };
2765
3983
  }
2766
3984
  function rewriteInbox(value) {
2767
- if (!isObject(value) || value.items === undefined)
3985
+ if (!isObject2(value) || value.items === undefined)
2768
3986
  return value;
2769
3987
  const items = rewriteItems(value.items);
2770
3988
  return items === value.items ? value : { ...value, items };
2771
3989
  }
2772
3990
  function rewriteGeneration(value) {
2773
- if (!isObject(value) || value.message === undefined)
3991
+ if (!isObject2(value) || value.message === undefined)
2774
3992
  return value;
2775
- const message = rewriteMessage(value.message);
2776
- return message === value.message ? value : { ...value, message };
3993
+ const message2 = rewriteMessage(value.message);
3994
+ return message2 === value.message ? value : { ...value, message: message2 };
2777
3995
  }
2778
3996
  function rewriteLive(value) {
2779
- if (!isObject(value) || value.generation === undefined)
3997
+ if (!isObject2(value) || value.generation === undefined)
2780
3998
  return value;
2781
3999
  const generation = rewriteGeneration(value.generation);
2782
4000
  return generation === value.generation ? value : { ...value, generation };
2783
4001
  }
2784
4002
  function rewriteDocs(value) {
2785
- if (!isObject(value))
4003
+ if (!isObject2(value))
2786
4004
  return value;
2787
4005
  let out;
2788
4006
  for (const key of ["pi.inbox", "pi.live"]) {
@@ -2796,7 +4014,7 @@ function rewriteDocs(value) {
2796
4014
  return out ?? value;
2797
4015
  }
2798
4016
  function rewriteView(value) {
2799
- if (!isObject(value))
4017
+ if (!isObject2(value))
2800
4018
  return value;
2801
4019
  let out;
2802
4020
  if (value.entries !== undefined) {
@@ -2824,7 +4042,7 @@ function rewriteSlot(slot, value) {
2824
4042
  case "content":
2825
4043
  return rewriteContent(value);
2826
4044
  case "block":
2827
- return isObject(value) && isImageBlock(value) ? withRef(value) : value;
4045
+ return isObject2(value) && isImageBlock(value) ? withRef(value) : value;
2828
4046
  case "docs":
2829
4047
  return rewriteDocs(value);
2830
4048
  case "inbox":
@@ -2889,35 +4107,35 @@ function reachesIntoImage(op, root) {
2889
4107
  if (slotAt(path.slice(0, length)) !== "block")
2890
4108
  continue;
2891
4109
  const node = resolve4(root, path, length);
2892
- if (isObject(node) && isImageBlock(node))
4110
+ if (isObject2(node) && isImageBlock(node))
2893
4111
  return true;
2894
4112
  }
2895
4113
  return false;
2896
4114
  }
2897
4115
  function* contentArrays(view) {
2898
- if (!isObject(view))
4116
+ if (!isObject2(view))
2899
4117
  return;
2900
4118
  if (Array.isArray(view.entries)) {
2901
4119
  for (const entry of view.entries) {
2902
- if (!isObject(entry) || !Array.isArray(entry.model))
4120
+ if (!isObject2(entry) || !Array.isArray(entry.model))
2903
4121
  continue;
2904
- for (const message of entry.model) {
2905
- if (isObject(message) && Array.isArray(message.content))
2906
- yield message.content;
4122
+ for (const message2 of entry.model) {
4123
+ if (isObject2(message2) && Array.isArray(message2.content))
4124
+ yield message2.content;
2907
4125
  }
2908
4126
  }
2909
4127
  }
2910
- if (!isObject(view.docs))
4128
+ if (!isObject2(view.docs))
2911
4129
  return;
2912
4130
  const inbox = view.docs["pi.inbox"];
2913
- if (isObject(inbox) && Array.isArray(inbox.items)) {
4131
+ if (isObject2(inbox) && Array.isArray(inbox.items)) {
2914
4132
  for (const item of inbox.items) {
2915
- if (isObject(item) && Array.isArray(item.content))
4133
+ if (isObject2(item) && Array.isArray(item.content))
2916
4134
  yield item.content;
2917
4135
  }
2918
4136
  }
2919
4137
  const live = view.docs["pi.live"];
2920
- if (isObject(live) && isObject(live.generation) && isObject(live.generation.message) && Array.isArray(live.generation.message.content)) {
4138
+ if (isObject2(live) && isObject2(live.generation) && isObject2(live.generation.message) && Array.isArray(live.generation.message.content)) {
2921
4139
  yield live.generation.message.content;
2922
4140
  }
2923
4141
  }
@@ -2925,7 +4143,7 @@ function imageBlocks(view) {
2925
4143
  const blocks = [];
2926
4144
  for (const content of contentArrays(view)) {
2927
4145
  for (const block of content) {
2928
- if (isObject(block) && isImageBlock(block))
4146
+ if (isObject2(block) && isImageBlock(block))
2929
4147
  blocks.push(block);
2930
4148
  }
2931
4149
  }
@@ -3013,8 +4231,8 @@ class ImageVariants {
3013
4231
  if (job === undefined)
3014
4232
  return;
3015
4233
  this.#active += 1;
3016
- this.#derive(job.key, job.variant, job.original).then((result2) => {
3017
- job.resolve(result2);
4234
+ this.#derive(job.key, job.variant, job.original).then((result3) => {
4235
+ job.resolve(result3);
3018
4236
  this.#finish();
3019
4237
  }, (error) => {
3020
4238
  job.reject(error instanceof Error ? error : new Error(String(error)));
@@ -3035,13 +4253,13 @@ class ImageVariants {
3035
4253
  if (prepared === undefined || prepared.data === original.data && prepared.mimeType === original.mimeType) {
3036
4254
  return { ...original, variant: "original" };
3037
4255
  }
3038
- const result2 = {
4256
+ const result3 = {
3039
4257
  data: prepared.data,
3040
4258
  mimeType: prepared.mimeType,
3041
4259
  variant
3042
4260
  };
3043
- this.#write(key, result2);
3044
- return result2;
4261
+ this.#write(key, result3);
4262
+ return result3;
3045
4263
  } catch {
3046
4264
  return { ...original, variant: "original" };
3047
4265
  }
@@ -3054,8 +4272,8 @@ class ImageVariants {
3054
4272
  this.#cache.set(key, cached);
3055
4273
  return cached;
3056
4274
  }
3057
- #write(key, result2) {
3058
- const bytes = result2.data.length;
4275
+ #write(key, result3) {
4276
+ const bytes = result3.data.length;
3059
4277
  if (bytes > this.#maxCacheBytes)
3060
4278
  return;
3061
4279
  while (this.#cacheBytes + bytes > this.#maxCacheBytes) {
@@ -3066,17 +4284,17 @@ class ImageVariants {
3066
4284
  this.#cache.delete(oldest);
3067
4285
  this.#cacheBytes -= previous.data.length;
3068
4286
  }
3069
- this.#cache.set(key, result2);
4287
+ this.#cache.set(key, result3);
3070
4288
  this.#cacheBytes += bytes;
3071
4289
  }
3072
4290
  }
3073
4291
 
3074
4292
  // apps/host/src/auto-title.ts
3075
- import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
4293
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
3076
4294
 
3077
4295
  // apps/host/src/title-doc.ts
3078
- import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
3079
- var TitleDoc = defineDoc6({
4296
+ import { defineDoc as defineDoc9 } from "@earendil-works/pi-durable";
4297
+ var TitleDoc = defineDoc9({
3080
4298
  kind: "pinomad.title",
3081
4299
  version: 1,
3082
4300
  scope: "conversation",
@@ -3087,11 +4305,11 @@ var TitleDoc = defineDoc6({
3087
4305
  });
3088
4306
 
3089
4307
  // apps/host/src/auto-title.ts
3090
- var context = BACKGROUND_CONTEXT;
4308
+ var context = BACKGROUND_CONTEXT2;
3091
4309
  var TITLE_PROMPT = "You name conversations between a user and a coding agent. Reply with only the title: at most 6 words" + " (about 20 characters for Chinese or Japanese), in the same language as the user's message, no quotes," + " no trailing punctuation. Name the task, not the agent's answer.";
3092
4310
  var TITLE_INPUT_LIMIT = 2000;
3093
4311
  var TITLE_MAX_LENGTH = 80;
3094
- var textOf = (content) => {
4312
+ var textOf2 = (content) => {
3095
4313
  if (typeof content === "string")
3096
4314
  return content;
3097
4315
  if (!Array.isArray(content))
@@ -3148,7 +4366,7 @@ function attachAutoTitle(runs, deps) {
3148
4366
  return;
3149
4367
  const firstMessage = first.model?.[0];
3150
4368
  const firstText = (stripUserBlocks(firstMessage?.content ?? "").text || "(images or files only)").slice(0, TITLE_INPUT_LIMIT);
3151
- const answer = textOf(last.content).slice(0, TITLE_INPUT_LIMIT);
4369
+ const answer = textOf2(last.content).slice(0, TITLE_INPUT_LIMIT);
3152
4370
  const request = {
3153
4371
  systemPrompt: TITLE_PROMPT,
3154
4372
  messages: [
@@ -3195,8 +4413,8 @@ ${answer}`}`,
3195
4413
 
3196
4414
  // apps/host/src/push.ts
3197
4415
  import { createHash as createHash2 } from "node:crypto";
3198
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
3199
- import { defineDoc as defineDoc7 } from "@earendil-works/pi-durable";
4416
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
4417
+ import { defineDoc as defineDoc10 } from "@earendil-works/pi-durable";
3200
4418
 
3201
4419
  // packages/client/src/family.ts
3202
4420
  var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
@@ -3253,8 +4471,8 @@ function diffRunNotices(previous, organized) {
3253
4471
  }
3254
4472
 
3255
4473
  // apps/host/src/push.ts
3256
- var context2 = BACKGROUND_CONTEXT2;
3257
- var PushDoc = defineDoc7({
4474
+ var context2 = BACKGROUND_CONTEXT3;
4475
+ var PushDoc = defineDoc10({
3258
4476
  kind: "pinomad.push",
3259
4477
  version: 2,
3260
4478
  scope: "session",
@@ -3391,18 +4609,18 @@ async function attachPushSender(options) {
3391
4609
  return await new Promise((resolve5) => {
3392
4610
  const waiters = titleWaiters.get(notice.id) ?? new Set;
3393
4611
  titleWaiters.set(notice.id, waiters);
3394
- const settle = (title) => {
3395
- waiters.delete(settle);
4612
+ const settle2 = (title) => {
4613
+ waiters.delete(settle2);
3396
4614
  if (waiters.size === 0)
3397
4615
  titleWaiters.delete(notice.id);
3398
4616
  timers.clearTimeout(timer);
3399
4617
  resolve5(title);
3400
4618
  };
3401
- const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
3402
- waiters.add(settle);
4619
+ const timer = timers.setTimeout(() => settle2(FALLBACK_TITLE), TITLE_WAIT_MS);
4620
+ waiters.add(settle2);
3403
4621
  namedTitle(notice.id).then((title) => {
3404
4622
  if (title !== undefined)
3405
- settle(title);
4623
+ settle2(title);
3406
4624
  });
3407
4625
  });
3408
4626
  };
@@ -3529,24 +4747,29 @@ async function attachPushSender(options) {
3529
4747
  }
3530
4748
  };
3531
4749
  const unsubscribeConversations = conversations.subscribe(() => update());
3532
- const unsubscribeCommits = harness.subscribeCommits((publication) => {
3533
- for (const change of publication.changes) {
3534
- if (change.type !== "document" || change.record.kind !== TitleDoc.definition.kind)
3535
- continue;
3536
- const id = change.conversationId;
3537
- if (id === undefined)
3538
- continue;
3539
- const title = change.value?.title;
3540
- if (typeof title !== "string" || title === "")
3541
- continue;
3542
- const waiters = titleWaiters.get(id);
3543
- titleWaiters.delete(id);
3544
- for (const settle of waiters ?? [])
3545
- settle(title);
3546
- }
4750
+ const commits = harness.observeCommits({
4751
+ advance: (publication) => {
4752
+ for (const change of publication.changes) {
4753
+ if (change.type !== "document" || change.kind !== "change" || change.record.kind !== TitleDoc.definition.kind)
4754
+ continue;
4755
+ const id = change.conversationId;
4756
+ if (id === undefined)
4757
+ continue;
4758
+ const title = change.value.title;
4759
+ if (typeof title !== "string" || title === "")
4760
+ continue;
4761
+ const waiters = titleWaiters.get(id);
4762
+ titleWaiters.delete(id);
4763
+ for (const settle2 of waiters ?? [])
4764
+ settle2(title);
4765
+ }
4766
+ },
4767
+ end: () => {}
3547
4768
  });
3548
- indexWatch?.start(async (value) => {
3549
- index = value ?? undefined;
4769
+ indexWatch?.start(async (call) => {
4770
+ if (call.kind !== "value")
4771
+ return;
4772
+ index = call.value;
3550
4773
  update();
3551
4774
  });
3552
4775
  update();
@@ -3555,7 +4778,7 @@ async function attachPushSender(options) {
3555
4778
  async close() {
3556
4779
  disposed = true;
3557
4780
  unsubscribeConversations();
3558
- unsubscribeCommits();
4781
+ commits.detach();
3559
4782
  await indexWatch?.stop();
3560
4783
  for (const item of pending)
3561
4784
  drop(item);
@@ -3594,7 +4817,7 @@ function toolTarget(args) {
3594
4817
  }
3595
4818
 
3596
4819
  // apps/host/src/gateway.ts
3597
- var context3 = BACKGROUND_CONTEXT3;
4820
+ var context3 = BACKGROUND_CONTEXT4;
3598
4821
  var forkTails = new WeakMap;
3599
4822
  function assertQueuedInput(record, conversationId) {
3600
4823
  if (record.conversationId !== conversationId) {
@@ -3607,7 +4830,7 @@ async function dismissPendingQuestions(tx, docs, conversationId) {
3607
4830
  let dismissed = false;
3608
4831
  for (const doc of docs) {
3609
4832
  const value = await tx.doc(doc.token, conversationId);
3610
- if (!Value2.Check(QuestionSchema, value))
4833
+ if (!Value.Check(QuestionSchema, value))
3611
4834
  continue;
3612
4835
  for (const request of value.requests) {
3613
4836
  if (request.resolution === undefined) {
@@ -3659,6 +4882,19 @@ function submitResult(record) {
3659
4882
  ...record.entry === undefined ? {} : { entry: record.entry }
3660
4883
  };
3661
4884
  }
4885
+ var AGENT_WORK_CALLS = new Set(["submit", "createConversation", "runCommand", "compact", "answer"]);
4886
+ var NO_UPGRADES = constantValue({
4887
+ current: packagedVersion ?? null,
4888
+ available: null,
4889
+ blocked: "source",
4890
+ phase: "idle",
4891
+ target: null,
4892
+ attempt: 0,
4893
+ checking: false,
4894
+ checkedAt: null,
4895
+ checkError: null,
4896
+ failure: null
4897
+ });
3662
4898
  var LOOPBACK_SECURE_PATH = "/secure";
3663
4899
  async function startGateway(options) {
3664
4900
  if (options.secure?.lan?.publicUrl !== undefined)
@@ -3694,10 +4930,16 @@ async function startGateway(options) {
3694
4930
  const clients = new Set;
3695
4931
  let rtc;
3696
4932
  const rtcHello = () => rtc?.hello();
3697
- const terminals = await startTerminals(options.terminals);
3698
- const broadcast = (level, message) => {
4933
+ const terminals = await startTerminals({
4934
+ ...options.terminals,
4935
+ onOutput: () => {
4936
+ options.terminals?.onOutput?.();
4937
+ options.upgrade?.touch();
4938
+ }
4939
+ });
4940
+ const broadcast = (level, message2) => {
3699
4941
  for (const client of clients)
3700
- client.send({ type: "notice", level, message });
4942
+ client.send({ type: "notice", level, message: message2 });
3701
4943
  };
3702
4944
  let remote;
3703
4945
  let relay;
@@ -3732,6 +4974,7 @@ async function startGateway(options) {
3732
4974
  devicePublicKey,
3733
4975
  rtcHello,
3734
4976
  terminals,
4977
+ ...options.processes === undefined ? {} : { processes: options.processes },
3735
4978
  closed: (self) => {
3736
4979
  clients.delete(self);
3737
4980
  connections.delete(self);
@@ -3969,6 +5212,7 @@ async function startGateway(options) {
3969
5212
  await detachPush?.close();
3970
5213
  conversations.dispose();
3971
5214
  terminals?.closeAll();
5215
+ await options.processes?.close(context3);
3972
5216
  await rtc?.close();
3973
5217
  await Promise.all([...connections].map((connection) => connection.dispose()));
3974
5218
  for (const socket of server.clients)
@@ -4141,6 +5385,8 @@ function secureHandshake(carrier, secure, policy, handlers) {
4141
5385
  session = new SecureSession(admitted.result);
4142
5386
  stage = "settled";
4143
5387
  await carrier.send(admitted.message);
5388
+ if (closed)
5389
+ return;
4144
5390
  if (admitted.purpose === "signal")
4145
5391
  handlers.signal?.(connection, admitted.deviceKey);
4146
5392
  else
@@ -4201,10 +5447,10 @@ function secureHandshake(carrier, secure, policy, handlers) {
4201
5447
  if (decision.outcome === "admitted" && decision.requestId !== undefined) {
4202
5448
  policy.accountAdmitted(decision.requestId, deviceKey);
4203
5449
  }
4204
- const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
5450
+ const { message: message2, result: result3 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
4205
5451
  return {
4206
- result: result2,
4207
- message,
5452
+ result: result3,
5453
+ message: message2,
4208
5454
  deviceKey,
4209
5455
  purpose,
4210
5456
  ...decision.outcome === "admitted" ? { joined: decision.entry } : {}
@@ -4271,12 +5517,16 @@ class ConversationList {
4271
5517
  #list = [];
4272
5518
  #unsubscribe = () => {};
4273
5519
  #notifying = false;
5520
+ #harness;
5521
+ #live = new Map;
5522
+ #disposed = false;
4274
5523
  constructor(docs) {
4275
5524
  this.#questionTokens = (docs ?? []).filter((doc) => doc.presentation === "pinomad.question").map((doc) => doc.token);
4276
5525
  this.#questionKinds = new Set(this.#questionTokens.map((token) => token.definition.kind));
4277
5526
  }
4278
5527
  static async open(harness, docs) {
4279
5528
  const list = new ConversationList(docs);
5529
+ list.#harness = harness;
4280
5530
  const summaries = [];
4281
5531
  let cursor;
4282
5532
  const records = [];
@@ -4295,10 +5545,13 @@ class ConversationList {
4295
5545
  summaries.push(list.#summaryOf(record));
4296
5546
  }
4297
5547
  list.#list = summaries;
4298
- list.#unsubscribe = harness.subscribeCommits((publication) => list.#observe(publication));
5548
+ const observation = harness.observeCommits({ advance: (publication) => list.#observe(publication), end: () => {} });
5549
+ list.#unsubscribe = () => observation.detach();
4299
5550
  const touched = new Set;
4300
5551
  list.#reindex(await harness.snapshot(IndexDoc, context3), touched);
4301
5552
  list.#refreshSummaries(touched);
5553
+ for (const [id, facts] of list.#facts)
5554
+ list.#followLive(id, facts.running === true);
4302
5555
  return list;
4303
5556
  }
4304
5557
  async#load(harness, record) {
@@ -4309,7 +5562,7 @@ class ConversationList {
4309
5562
  facts.tools = live?.tools;
4310
5563
  for (const token of this.#questionTokens) {
4311
5564
  const value = await harness.snapshot(token, record.id, context3);
4312
- if (Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
5565
+ if (Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
4313
5566
  facts.pending.add(token.definition.kind);
4314
5567
  }
4315
5568
  }
@@ -4320,6 +5573,9 @@ class ConversationList {
4320
5573
  if (typeof subagent?.role === "string" && subagent.role !== "")
4321
5574
  facts.role = subagent.role;
4322
5575
  facts.model = modelRefOf(subagent?.model);
5576
+ if (typeof subagent?.name === "string" && subagent.name !== "")
5577
+ facts.name = subagent.name;
5578
+ facts.lifecycle = subagent?.lifecycle === "background" ? "background" : "foreground";
4323
5579
  }
4324
5580
  facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
4325
5581
  const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
@@ -4348,29 +5604,30 @@ class ConversationList {
4348
5604
  continue;
4349
5605
  touched.add(entry.conversationId);
4350
5606
  facts.newest = entry.id;
4351
- const message = entry.model?.[0];
4352
- facts.updatedAt = typeof message?.timestamp === "number" ? message.timestamp : undefined;
4353
- facts.failed = entry.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error";
4354
- facts.calls = entry.kind === "pi.assistant" ? callsOf(message) : undefined;
5607
+ const message2 = entry.model?.[0];
5608
+ facts.updatedAt = typeof message2?.timestamp === "number" ? message2.timestamp : undefined;
5609
+ facts.failed = entry.kind === "pi.assistant" && message2?.role === "assistant" && message2.stopReason === "error";
5610
+ facts.calls = entry.kind === "pi.assistant" ? callsOf(message2) : undefined;
4355
5611
  } else if (change.type === "document") {
4356
5612
  const id = change.conversationId;
5613
+ const value = change.kind === "change" ? change.value : null;
4357
5614
  if (id === undefined) {
4358
5615
  if (change.record.kind === IndexDoc.definition.kind)
4359
- this.#reindex(change.value, touched);
5616
+ this.#reindex(value, touched);
4360
5617
  continue;
4361
5618
  }
4362
5619
  const facts = this.#fact(id);
4363
5620
  if (change.record.kind === "pi.live") {
4364
- const live = change.value;
5621
+ const live = value;
4365
5622
  const wasRunning = facts.running === true;
4366
5623
  facts.running = live?.run !== undefined;
4367
5624
  facts.tools = live?.tools;
4368
5625
  touched.add(id);
5626
+ this.#followLive(id, facts.running);
4369
5627
  if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
4370
5628
  ended.add(id);
4371
5629
  } else if (this.#questionKinds.has(change.record.kind)) {
4372
- const value = change.value;
4373
- const pending = Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
5630
+ const pending = Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined);
4374
5631
  const next = new Set(facts.pending);
4375
5632
  if (pending)
4376
5633
  next.add(change.record.kind);
@@ -4379,16 +5636,18 @@ class ConversationList {
4379
5636
  facts.pending = next;
4380
5637
  touched.add(id);
4381
5638
  } else if (change.record.kind === RoleDoc.definition.kind) {
4382
- facts.agent = roleNameOf(change.value);
5639
+ facts.agent = roleNameOf(value);
4383
5640
  touched.add(id);
4384
5641
  } else if (change.record.kind === SubagentDoc.definition.kind) {
4385
- const value = change.value;
4386
- facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
4387
- facts.role = typeof value?.role === "string" && value.role !== "" ? value.role : undefined;
4388
- facts.model = modelRefOf(value?.model);
5642
+ const subagent = value;
5643
+ facts.label = typeof subagent?.label === "string" && subagent.label !== "" ? subagent.label : undefined;
5644
+ facts.role = typeof subagent?.role === "string" && subagent.role !== "" ? subagent.role : undefined;
5645
+ facts.model = modelRefOf(subagent?.model);
5646
+ facts.name = typeof subagent?.name === "string" && subagent.name !== "" ? subagent.name : undefined;
5647
+ facts.lifecycle = subagent?.lifecycle === "background" ? "background" : "foreground";
4389
5648
  touched.add(id);
4390
5649
  } else if (change.record.kind === TitleDoc.definition.kind) {
4391
- const named = change.value?.title;
5650
+ const named = value?.title;
4392
5651
  facts.named = typeof named === "string" && named !== "" ? named : undefined;
4393
5652
  touched.add(id);
4394
5653
  }
@@ -4463,9 +5722,51 @@ class ConversationList {
4463
5722
  listener(this.#list);
4464
5723
  });
4465
5724
  }
5725
+ #followLive(id, running) {
5726
+ if (!running || this.#disposed) {
5727
+ const watch = this.#live.get(id);
5728
+ this.#live.delete(id);
5729
+ watch?.stop();
5730
+ return;
5731
+ }
5732
+ if (this.#live.has(id))
5733
+ return;
5734
+ this.#live.set(id, undefined);
5735
+ setImmediate(() => {
5736
+ this.#harness.watchDoc(LiveDoc, id, context3).then(async (watch) => {
5737
+ if (watch === undefined)
5738
+ return;
5739
+ if (this.#live.get(id) !== undefined || !this.#live.has(id)) {
5740
+ await watch.stop();
5741
+ return;
5742
+ }
5743
+ this.#live.set(id, watch);
5744
+ const apply = (live) => {
5745
+ const facts = this.#fact(id);
5746
+ if (live.run === undefined)
5747
+ return;
5748
+ facts.tools = live.tools;
5749
+ this.#refreshSummaries(new Set([id]));
5750
+ };
5751
+ apply(watch.value);
5752
+ watch.start(async (call) => {
5753
+ if (call.kind === "value")
5754
+ apply(call.value);
5755
+ });
5756
+ }, (error) => {
5757
+ if (this.#live.get(id) === undefined)
5758
+ this.#live.delete(id);
5759
+ console.error(`conversation ${id}: could not follow pi.live: ${error instanceof Error ? error.message : String(error)}`);
5760
+ });
5761
+ });
5762
+ }
4466
5763
  dispose() {
5764
+ this.#disposed = true;
4467
5765
  this.#unsubscribe();
4468
5766
  this.#listeners.clear();
5767
+ for (const watch of this.#live.values())
5768
+ watch?.stop();
5769
+ this.#live.clear();
4469
5770
  }
4470
5771
  }
4471
5772
  function roleNameOf(value) {
@@ -4511,7 +5812,8 @@ function summaryOf(record, facts, role, currentAgent) {
4511
5812
  return {
4512
5813
  id: record.id,
4513
5814
  kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
4514
- ...record.owner === undefined ? {} : { lifecycle: "foreground" },
5815
+ ...record.owner === undefined ? {} : { lifecycle: facts.lifecycle ?? "foreground" },
5816
+ ...facts.name === undefined ? {} : { name: facts.name },
4515
5817
  ...facts.model === undefined ? {} : { model: facts.model },
4516
5818
  ...parent === undefined ? {} : { parent },
4517
5819
  ...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
@@ -4525,15 +5827,47 @@ function summaryOf(record, facts, role, currentAgent) {
4525
5827
  };
4526
5828
  }
4527
5829
  function sameSummary(a, b) {
4528
- 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;
5830
+ return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.name === b.name && 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;
4529
5831
  }
4530
5832
  function titleOf(entry) {
4531
- const message = entry?.model?.[0];
4532
- if (message?.role !== "user")
5833
+ const message2 = entry?.model?.[0];
5834
+ if (message2?.role !== "user")
4533
5835
  return {};
4534
- const title = stripUserBlocks(message.content).text.replace(/\s+/g, " ").trim();
5836
+ const title = stripUserBlocks(message2.content).text.replace(/\s+/g, " ").trim();
4535
5837
  return title === "" ? {} : { title };
4536
5838
  }
5839
+ var WHOLE_TRANSCRIPT = { min: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
5840
+ var TURN_STARTS = new Set(["pi.user", "pi.compaction", "pi.reset"]);
5841
+ async function turnPage(conversation, before, limit) {
5842
+ const want = Math.min(limit, MAX_TRANSCRIPT_PAGE);
5843
+ const done = (taken2) => taken2.length >= MAX_TRANSCRIPT_PAGE || taken2.length >= want && TURN_STARTS.has(taken2.at(-1).kind);
5844
+ const query = { order: "descending", ...before === undefined ? {} : { maxEntryId: before } };
5845
+ const taken = [];
5846
+ let cursor;
5847
+ for (;; ) {
5848
+ const page = await conversation.entries(query, Math.max(want + 1 - taken.length, 64), cursor, context3);
5849
+ for (const entry of page.items) {
5850
+ if (done(taken))
5851
+ return { entries: taken.reverse(), before: entry.id };
5852
+ taken.push(entry);
5853
+ }
5854
+ if (page.next === undefined)
5855
+ return { entries: taken.reverse() };
5856
+ cursor = page.next;
5857
+ }
5858
+ }
5859
+ async function viewWindow(conversation, window) {
5860
+ if (window === undefined)
5861
+ return WHOLE_TRANSCRIPT;
5862
+ const from = window.from ?? await startOf(conversation, window.limit);
5863
+ return from === undefined ? WHOLE_TRANSCRIPT : { ...WHOLE_TRANSCRIPT, from };
5864
+ }
5865
+ async function startOf(conversation, limit) {
5866
+ const page = await turnPage(conversation, undefined, limit);
5867
+ return page.before === undefined ? undefined : page.entries[0].id;
5868
+ }
5869
+ var shownOps = (call) => isGone(call) ? [["r", null]] : call.ops;
5870
+ var isGone = (call) => call.retired && !call.ops.some((op) => op[0] === "r");
4537
5871
  async function historyFacts(harness, id) {
4538
5872
  const conversation = await harness.conversation(id, context3);
4539
5873
  let first;
@@ -4545,21 +5879,21 @@ async function historyFacts(harness, id) {
4545
5879
  newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
4546
5880
  cursor = page.next;
4547
5881
  } while (cursor !== undefined);
4548
- const message = newest?.model?.[0];
5882
+ const message2 = newest?.model?.[0];
4549
5883
  return {
4550
5884
  ...titleOf(first),
4551
5885
  ...newest === undefined ? {} : {
4552
5886
  newest: newest.id,
4553
- ...typeof message?.timestamp === "number" ? { updatedAt: message.timestamp } : {},
4554
- failed: newest.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error",
4555
- ...newest.kind === "pi.assistant" ? { calls: callsOf(message) } : {}
5887
+ ...typeof message2?.timestamp === "number" ? { updatedAt: message2.timestamp } : {},
5888
+ failed: newest.kind === "pi.assistant" && message2?.role === "assistant" && message2.stopReason === "error",
5889
+ ...newest.kind === "pi.assistant" ? { calls: callsOf(message2) } : {}
4556
5890
  }
4557
5891
  };
4558
5892
  }
4559
- function callsOf(message) {
4560
- if (message?.role !== "assistant")
5893
+ function callsOf(message2) {
5894
+ if (message2?.role !== "assistant")
4561
5895
  return;
4562
- const content = message.content;
5896
+ const content = message2.content;
4563
5897
  if (!Array.isArray(content))
4564
5898
  return;
4565
5899
  const calls = new Map;
@@ -4666,6 +6000,12 @@ class GatewayClient {
4666
6000
  throw new Error("Provider setup is not available on this host");
4667
6001
  return modelAuth;
4668
6002
  }
6003
+ #upgrades() {
6004
+ const upgrade = this.#options.upgrade;
6005
+ if (upgrade === undefined)
6006
+ throw new Error("This host can't upgrade itself (source)");
6007
+ return upgrade;
6008
+ }
4669
6009
  #hostValues;
4670
6010
  #subscriptions = new Map;
4671
6011
  #terminalSizes = new Map;
@@ -4683,7 +6023,8 @@ class GatewayClient {
4683
6023
  auth: options.modelAuth?.auth ?? constantValue({ logins: [] }),
4684
6024
  providers: options.modelAuth?.providers ?? constantValue({ providers: [], logins: [] }),
4685
6025
  settings: options.settings,
4686
- agents: options.agents ?? constantValue({ roles: [], problems: [] })
6026
+ agents: options.agents ?? constantValue({ roles: [], problems: [] }),
6027
+ upgrade: options.upgrade ?? NO_UPGRADES
4687
6028
  };
4688
6029
  this.#hooks = hooks;
4689
6030
  this.devicePublicKey = hooks.devicePublicKey;
@@ -4732,7 +6073,8 @@ class GatewayClient {
4732
6073
  toolPresentations: options.toolPresentations ?? {},
4733
6074
  endpointSet: this.#hooks.endpointSet(),
4734
6075
  ...rtc === undefined ? {} : { rtc },
4735
- ...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } }
6076
+ ...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } },
6077
+ ...this.#hooks.processes === undefined ? {} : { processes: this.#hooks.processes.hello }
4736
6078
  });
4737
6079
  }
4738
6080
  send(frame) {
@@ -4745,15 +6087,19 @@ class GatewayClient {
4745
6087
  }
4746
6088
  async#receive(frame) {
4747
6089
  if (frame.type === "subscribe")
4748
- await this.#subscribe(frame.stream, frame.images === "ref", frame.since);
6090
+ await this.#subscribe(frame.stream, frame.images === "ref", frame.since, frame.window);
4749
6091
  else if (frame.type === "unsubscribe")
4750
6092
  await this.#unsubscribe(frame.stream);
4751
6093
  else
4752
6094
  await this.#answer(frame.id, frame.method, frame.args);
4753
6095
  }
4754
- async#subscribe(stream, imageRefs, since) {
6096
+ async#subscribe(stream, imageRefs, since, window) {
4755
6097
  const previous = this.#subscriptions.get(stream);
4756
- const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : since === undefined ? {} : { since };
6098
+ const state = {
6099
+ ...imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {},
6100
+ ...since === undefined ? {} : { since },
6101
+ ...window === undefined ? {} : { window }
6102
+ };
4757
6103
  this.#subscriptions.set(stream, state);
4758
6104
  const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
4759
6105
  const send = (frame) => current() ? this.send(frame) : Promise.resolve();
@@ -4815,12 +6161,24 @@ class GatewayClient {
4815
6161
  });
4816
6162
  return { stop: detach };
4817
6163
  }
6164
+ if (stream.startsWith("process:")) {
6165
+ const processStream = stream;
6166
+ const detach = this.#processes().attach(processStream.slice("process:".length), state?.since, {
6167
+ output: (frame) => send({ type: "output", stream: processStream, ...frame }),
6168
+ ended: (reason) => {
6169
+ send({ type: "ended", stream, reason });
6170
+ if (this.#subscriptions.get(stream) === state)
6171
+ this.#subscriptions.delete(stream);
6172
+ }
6173
+ });
6174
+ return { stop: detach };
6175
+ }
4818
6176
  if (stream.startsWith("doc:")) {
4819
6177
  const at = stream.lastIndexOf(":");
4820
6178
  return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
4821
6179
  }
4822
6180
  const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
4823
- return this.#forward(stream, await conversation.watch(context3), send, state);
6181
+ return this.#forward(stream, await conversation.watch(context3, await viewWindow(conversation, state?.window)), send, state);
4824
6182
  }
4825
6183
  #hostValue(stream) {
4826
6184
  return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
@@ -4838,13 +6196,17 @@ class GatewayClient {
4838
6196
  });
4839
6197
  let inner;
4840
6198
  let stopped = false;
4841
- const unsubscribe = harness.subscribeCommits((publication) => {
4842
- const matches = publication.changes.some((change) => (change.type === "document" || change.type === "document.copy") && change.record.kind === kind && change.conversationId === id);
4843
- if (!matches)
4844
- return;
4845
- unsubscribe();
4846
- setImmediate(() => signalCreation());
6199
+ const observation = harness.observeCommits({
6200
+ advance: (publication) => {
6201
+ const matches = publication.changes.some((change) => (change.type === "document" || change.type === "document.copy") && change.record.kind === kind && change.conversationId === id);
6202
+ if (!matches)
6203
+ return;
6204
+ unsubscribe();
6205
+ setImmediate(() => signalCreation());
6206
+ },
6207
+ end: () => {}
4847
6208
  });
6209
+ const unsubscribe = () => observation.detach();
4848
6210
  try {
4849
6211
  const existing = await harness.watchDoc(token, id, context3);
4850
6212
  if (existing !== undefined) {
@@ -4885,16 +6247,19 @@ class GatewayClient {
4885
6247
  async#forward(stream, watch, send, state) {
4886
6248
  if (state?.images === undefined) {
4887
6249
  await send({ type: "snapshot", stream, value: watch.value });
4888
- watch.start((_value, ops) => send({ type: "ops", stream, ops }));
6250
+ watch.start((call) => call.kind === "value" ? send({ type: "ops", stream, ops: shownOps(call) }) : Promise.resolve());
4889
6251
  return watch;
4890
6252
  }
4891
6253
  const images = state.images;
4892
6254
  let before = watch.value;
4893
6255
  state.view = before;
4894
6256
  await send({ type: "snapshot", stream, value: await images.value(before) });
4895
- watch.start(async (value, ops) => {
6257
+ watch.start(async (call) => {
6258
+ if (call.kind !== "value")
6259
+ return;
6260
+ const ops = shownOps(call);
4896
6261
  const sent = await images.ops(ops, before);
4897
- before = value;
6262
+ before = isGone(call) ? null : call.value;
4898
6263
  state.view = before;
4899
6264
  return send({ type: "ops", stream, ops: sent });
4900
6265
  });
@@ -4904,7 +6269,7 @@ class GatewayClient {
4904
6269
  for (const state of this.#subscriptions.values()) {
4905
6270
  if (state.images === undefined || state.view === undefined)
4906
6271
  continue;
4907
- const image = state.images.image(state.view, ref);
6272
+ const image = state.images.image(state.view, ref) ?? (state.earlier === undefined ? undefined : state.images.image({ entries: state.earlier }, ref));
4908
6273
  if (image !== undefined)
4909
6274
  return image;
4910
6275
  }
@@ -4953,8 +6318,11 @@ class GatewayClient {
4953
6318
  #submissionByRequest(conversationId, requestId) {
4954
6319
  return this.#options.harness.commit((tx) => tx.submissionByRequest(conversationId, requestId), context3);
4955
6320
  }
4956
- async#compact(conversation, instructions) {
4957
- const taskId = await conversation.compact(instructions, context3);
6321
+ async#isSubagent(id) {
6322
+ return await this.#options.harness.commit(async (tx) => (await tx.conversation(id))?.owner !== undefined, context3);
6323
+ }
6324
+ async#compact(conversation, instructions2) {
6325
+ const taskId = await conversation.compact(instructions2, context3);
4958
6326
  this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
4959
6327
  return taskId;
4960
6328
  }
@@ -4972,6 +6340,17 @@ class GatewayClient {
4972
6340
  }
4973
6341
  }
4974
6342
  async#call(method, args) {
6343
+ if (AGENT_WORK_CALLS.has(method) && this.#options.upgrade !== undefined) {
6344
+ const release2 = this.#options.upgrade.admit();
6345
+ try {
6346
+ return await this.#dispatch(method, args);
6347
+ } finally {
6348
+ release2();
6349
+ }
6350
+ }
6351
+ return this.#dispatch(method, args);
6352
+ }
6353
+ async#dispatch(method, args) {
4975
6354
  switch (method) {
4976
6355
  case "terminalInput": {
4977
6356
  const input = args;
@@ -5001,6 +6380,11 @@ class GatewayClient {
5001
6380
  this.#terminals().close(args.terminalId);
5002
6381
  this.#terminalSizes.delete(args.terminalId);
5003
6382
  return null;
6383
+ case "stopProcess": {
6384
+ const input = args;
6385
+ const process2 = await this.#processes().stop({ conversationId: input.conversationId, id: input.id, reason: "user" }, context3);
6386
+ return { state: process2.state };
6387
+ }
5004
6388
  case "addProject":
5005
6389
  return addProject(this.#options.harness, args.path, context3);
5006
6390
  case "removeProject":
@@ -5013,6 +6397,9 @@ class GatewayClient {
5013
6397
  const existing = await this.#submissionByRequest(input.conversationId, input.requestId);
5014
6398
  if (existing !== undefined)
5015
6399
  return submitResult(existing);
6400
+ if (await this.#isSubagent(input.conversationId)) {
6401
+ throw new Error("Subagents take no messages from the user: tell the conversation that dispatched it");
6402
+ }
5016
6403
  const resolved = images();
5017
6404
  const conversation2 = await this.#conversation(input.conversationId);
5018
6405
  const attachments = await this.#attachmentText(input.attachments);
@@ -5062,13 +6449,27 @@ class GatewayClient {
5062
6449
  }
5063
6450
  case "archive": {
5064
6451
  const input = args;
5065
- await archive(this.#options.harness, input.conversationId, input.archived, context3);
5066
- if (input.archived) {
5067
- this.#hooks.terminals?.closeOwners(await conversationSubtree(this.#options.harness, input.conversationId, context3));
5068
- const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
5069
- for (const { record, reason } of kept) {
5070
- await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
5071
- }
6452
+ if (!input.archived) {
6453
+ await archive(this.#options.harness, input.conversationId, false, context3);
6454
+ this.#hooks.processes?.unseal(await conversationSubtree(this.#options.harness, input.conversationId, context3));
6455
+ return null;
6456
+ }
6457
+ const subtree = await conversationSubtree(this.#options.harness, input.conversationId, context3);
6458
+ this.#hooks.processes?.seal(subtree);
6459
+ try {
6460
+ await archive(this.#options.harness, input.conversationId, true, context3);
6461
+ } catch (error) {
6462
+ this.#hooks.processes?.unseal(subtree);
6463
+ throw error;
6464
+ }
6465
+ for (const id of subtree) {
6466
+ await (await this.#options.harness.conversation(id, context3))?.abort(context3, { background: true });
6467
+ }
6468
+ await this.#hooks.processes?.closeOwners(subtree, "archive", context3);
6469
+ this.#hooks.terminals?.closeOwners(subtree);
6470
+ const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
6471
+ for (const { record, reason } of kept) {
6472
+ await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
5072
6473
  }
5073
6474
  return null;
5074
6475
  }
@@ -5137,6 +6538,15 @@ class GatewayClient {
5137
6538
  await this.#options.settings.update(args.patch);
5138
6539
  return null;
5139
6540
  }
6541
+ case "startUpgrade":
6542
+ await this.#upgrades().start(args.when);
6543
+ return null;
6544
+ case "cancelUpgrade":
6545
+ this.#upgrades().cancel();
6546
+ return null;
6547
+ case "checkUpgrade":
6548
+ await this.#upgrades().check();
6549
+ return null;
5140
6550
  case "createAgent": {
5141
6551
  const store = this.#options.agents;
5142
6552
  if (store === undefined)
@@ -5168,6 +6578,26 @@ class GatewayClient {
5168
6578
  const input = args;
5169
6579
  return listSkills(this.#options.harness, this.#skills, input.scope, context3);
5170
6580
  }
6581
+ case "earlierEntries": {
6582
+ const { conversationId, before, limit } = args;
6583
+ const stream = `conversation:${conversationId}`;
6584
+ const state = this.#subscriptions.get(stream);
6585
+ if (state === undefined)
6586
+ throw new Error(`Not subscribed to conversation ${conversationId}`);
6587
+ const replaced = () => this.#subscriptions.get(stream) !== state;
6588
+ const resubscribed = () => new Error(`Conversation ${conversationId} was resubscribed while its page loaded`);
6589
+ const page = await turnPage(await this.#conversation(conversationId), before, limit);
6590
+ if (replaced())
6591
+ throw resubscribed();
6592
+ if (state.images === undefined)
6593
+ return page;
6594
+ const raw = page.entries;
6595
+ const { entries } = await state.images.value({ entries: raw });
6596
+ if (replaced())
6597
+ throw resubscribed();
6598
+ state.earlier = [...state.earlier ?? [], ...raw];
6599
+ return { ...page, entries };
6600
+ }
5171
6601
  case "image": {
5172
6602
  const input = args;
5173
6603
  const image = this.#shownImage(input.ref);
@@ -5229,6 +6659,37 @@ class GatewayClient {
5229
6659
  return { outcome: "steered" };
5230
6660
  }, context3);
5231
6661
  }
6662
+ case "reorderQueued": {
6663
+ const { conversationId, submissionId, before } = args;
6664
+ return await this.#options.harness.commit(async (tx) => {
6665
+ const queuedState = async (id) => {
6666
+ const record = await tx.submission(id);
6667
+ if (record === undefined)
6668
+ throw new Error(`Submission ${id} does not exist`);
6669
+ assertQueuedInput(record, conversationId);
6670
+ if (record.entry !== undefined)
6671
+ return "sent";
6672
+ return record.status === "queued" ? "queued" : "unavailable";
6673
+ };
6674
+ const moved = await queuedState(submissionId);
6675
+ if (moved !== "queued")
6676
+ return { outcome: moved };
6677
+ if (before !== undefined && await queuedState(before) !== "queued") {
6678
+ return { outcome: "unavailable" };
6679
+ }
6680
+ if (before === submissionId)
6681
+ return { outcome: "moved" };
6682
+ const items = (await tx.doc(InboxDoc, conversationId)).items;
6683
+ const from = items.findIndex((candidate) => candidate.id === submissionId);
6684
+ if (from < 0 || before !== undefined && !items.some((candidate) => candidate.id === before)) {
6685
+ return { outcome: "unavailable" };
6686
+ }
6687
+ const [item] = items.splice(from, 1);
6688
+ const to = before === undefined ? items.findLastIndex((candidate) => candidate.mode !== "write") + 1 : items.findIndex((candidate) => candidate.id === before);
6689
+ items.splice(to, 0, item);
6690
+ return { outcome: "moved" };
6691
+ }, context3);
6692
+ }
5232
6693
  case "renameConversation": {
5233
6694
  const { title } = args;
5234
6695
  const trimmed = title.trim();
@@ -5250,17 +6711,20 @@ class GatewayClient {
5250
6711
  return null;
5251
6712
  }
5252
6713
  case "abort":
6714
+ if (await this.#isSubagent(conversation.id)) {
6715
+ await this.#options.harness.commit((tx) => markStop(tx, conversation.id, "user"), context3);
6716
+ }
5253
6717
  await conversation.abort(context3);
5254
6718
  return null;
5255
6719
  case "compact": {
5256
- const { instructions } = args;
5257
- return { taskId: await this.#compact(conversation, instructions) };
6720
+ const { instructions: instructions2 } = args;
6721
+ return { taskId: await this.#compact(conversation, instructions2) };
5258
6722
  }
5259
6723
  case "listCommands":
5260
6724
  return listCommands();
5261
6725
  case "runCommand": {
5262
6726
  const { name, args: input } = args;
5263
- await runCommand({ compact: (instructions) => this.#compact(conversation, instructions) }, name, input);
6727
+ await runCommand({ compact: (instructions2) => this.#compact(conversation, instructions2) }, name, input);
5264
6728
  return null;
5265
6729
  }
5266
6730
  case "setModel": {
@@ -5336,7 +6800,7 @@ class GatewayClient {
5336
6800
  const token = doc.token;
5337
6801
  return await this.#options.harness.commit(async (tx) => {
5338
6802
  const value = await tx.doc(token, conversation.id);
5339
- if (!Value2.Check(QuestionSchema, value))
6803
+ if (!Value.Check(QuestionSchema, value))
5340
6804
  throw new Error(`Document ${kind} does not hold question requests`);
5341
6805
  const request = value.requests.find((candidate) => candidate.id === requestId);
5342
6806
  if (request === undefined)
@@ -5385,6 +6849,11 @@ class GatewayClient {
5385
6849
  throw new Error("Terminals are unavailable on this host");
5386
6850
  return this.#hooks.terminals;
5387
6851
  }
6852
+ #processes() {
6853
+ if (this.#hooks.processes === undefined)
6854
+ throw new Error("Background processes are unavailable on this host");
6855
+ return this.#hooks.processes;
6856
+ }
5388
6857
  async#terminalOwner(conversation) {
5389
6858
  const cwd = (await conversation.agent(context3)).cwd;
5390
6859
  if (cwd === undefined)
@@ -5424,7 +6893,7 @@ class GatewayClient {
5424
6893
  async#dismissPendingQuestions(conversationId, context4) {
5425
6894
  const questionDocs = this.#questionDocs();
5426
6895
  const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
5427
- const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
6896
+ const pending = snapshots.some((value) => Value.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
5428
6897
  if (!pending)
5429
6898
  return false;
5430
6899
  return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
@@ -5433,7 +6902,7 @@ class GatewayClient {
5433
6902
  return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
5434
6903
  }
5435
6904
  get #chatsPrefix() {
5436
- return join7(this.#options.dataDir, "chats") + sep5;
6905
+ return join8(this.#options.dataDir, "chats") + sep6;
5437
6906
  }
5438
6907
  async#changes(conversation, context4) {
5439
6908
  const cwd = (await conversation.agent(context4)).cwd;
@@ -5468,8 +6937,8 @@ class GatewayClient {
5468
6937
  // apps/host/src/host.ts
5469
6938
  import { randomBytes as randomBytes4 } from "node:crypto";
5470
6939
  import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
5471
- import { basename as basename3, join as join10, sep as sep6 } from "node:path";
5472
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
6940
+ import { basename as basename3, join as join11, sep as sep7 } from "node:path";
6941
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT6 } from "@earendil-works/chord/context";
5473
6942
  import { createRegistry, Harness } from "@earendil-works/pi-durable";
5474
6943
  import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
5475
6944
 
@@ -5485,10 +6954,10 @@ import {
5485
6954
  StreamableHttpTransport,
5486
6955
  toLlmContent
5487
6956
  } from "@earendil-works/pi-mcp";
5488
- import { defineExtension as defineExtension5, defineTool as defineTool4 } from "@earendil-works/pi-durable";
6957
+ import { defineExtension as defineExtension7, defineTool as defineTool5 } from "@earendil-works/pi-durable";
5489
6958
  import { Type as Type5 } from "typebox";
5490
6959
  var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
5491
- var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
6960
+ var isObject3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5492
6961
  function expand(value) {
5493
6962
  let missing;
5494
6963
  const expanded = value.replace(ENV_VAR, (_all, name) => {
@@ -5502,7 +6971,7 @@ function expand(value) {
5502
6971
  function expandAll(values, field, error) {
5503
6972
  if (values === undefined)
5504
6973
  return;
5505
- if (!isObject2(values)) {
6974
+ if (!isObject3(values)) {
5506
6975
  error(`"${field}" must be an object of strings`);
5507
6976
  return;
5508
6977
  }
@@ -5512,21 +6981,21 @@ function expandAll(values, field, error) {
5512
6981
  error(`"${field}.${key}" must be a string`);
5513
6982
  return;
5514
6983
  }
5515
- const result2 = expand(value);
5516
- if (typeof result2 !== "string") {
5517
- error(`"${field}.${key}" references unset environment variable ${result2.missing}`);
6984
+ const result3 = expand(value);
6985
+ if (typeof result3 !== "string") {
6986
+ error(`"${field}.${key}" references unset environment variable ${result3.missing}`);
5518
6987
  return;
5519
6988
  }
5520
- expanded[key] = result2;
6989
+ expanded[key] = result3;
5521
6990
  }
5522
6991
  return expanded;
5523
6992
  }
5524
6993
  function parseServer(name, raw, error) {
5525
- const fail = (message) => {
5526
- error(`${name}: ${message}`);
6994
+ const fail = (message2) => {
6995
+ error(`${name}: ${message2}`);
5527
6996
  return;
5528
6997
  };
5529
- if (!isObject2(raw))
6998
+ if (!isObject3(raw))
5530
6999
  return fail("server entry must be an object");
5531
7000
  if (raw.enabled === false)
5532
7001
  return { name, disabled: true };
@@ -5548,7 +7017,7 @@ function parseServer(name, raw, error) {
5548
7017
  }
5549
7018
  if (raw.cwd !== undefined && typeof raw.cwd !== "string")
5550
7019
  return fail(`"cwd" must be a string`);
5551
- const env = expandAll(raw.env, "env", (message) => error(`${name}: ${message}`));
7020
+ const env = expandAll(raw.env, "env", (message2) => error(`${name}: ${message2}`));
5552
7021
  if (raw.env !== undefined && env === undefined)
5553
7022
  return;
5554
7023
  const cwd = raw.cwd === undefined ? homedir2() : isAbsolute2(raw.cwd) ? raw.cwd : resolve5(homedir2(), raw.cwd);
@@ -5563,7 +7032,7 @@ function parseServer(name, raw, error) {
5563
7032
  if (raw.url !== undefined || raw.type === "http" || raw.type === "sse") {
5564
7033
  if (typeof raw.url !== "string" || raw.url === "")
5565
7034
  return fail(`HTTP server needs a "url"`);
5566
- const headers = expandAll(raw.headers, "headers", (message) => error(`${name}: ${message}`));
7035
+ const headers = expandAll(raw.headers, "headers", (message2) => error(`${name}: ${message2}`));
5567
7036
  if (raw.headers !== undefined && headers === undefined)
5568
7037
  return;
5569
7038
  return {
@@ -5585,13 +7054,13 @@ async function loadConfig(configPath) {
5585
7054
  return { errors: [], entries: [] };
5586
7055
  return { errors: [`${configPath}: ${error instanceof Error ? error.message : String(error)}`], entries: [] };
5587
7056
  }
5588
- if (!isObject2(parsed) || parsed.mcpServers !== undefined && !isObject2(parsed.mcpServers)) {
7057
+ if (!isObject3(parsed) || parsed.mcpServers !== undefined && !isObject3(parsed.mcpServers)) {
5589
7058
  return { errors: [`${configPath}: "mcpServers" must be an object`], entries: [] };
5590
7059
  }
5591
7060
  const errors = [];
5592
7061
  const entries = [];
5593
7062
  for (const [name, raw] of Object.entries(parsed.mcpServers ?? {})) {
5594
- const entry = parseServer(name, raw, (message) => errors.push(message));
7063
+ const entry = parseServer(name, raw, (message2) => errors.push(message2));
5595
7064
  if (entry !== undefined)
5596
7065
  entries.push(entry);
5597
7066
  }
@@ -5609,7 +7078,7 @@ var callToolResultSchema = (tool) => ({
5609
7078
  required: ["content"]
5610
7079
  });
5611
7080
  function mcpTool(client, timeoutMs, exposure, name, tool) {
5612
- return defineTool4({
7081
+ return defineTool5({
5613
7082
  name,
5614
7083
  description: tool.description ?? tool.title ?? tool.name,
5615
7084
  parameters: Type5.Unsafe({
@@ -5620,11 +7089,11 @@ function mcpTool(client, timeoutMs, exposure, name, tool) {
5620
7089
  ...exposure === "direct" ? {} : { callers: ["tools"] },
5621
7090
  structuredOutputSchema: Type5.Unsafe(callToolResultSchema(tool)),
5622
7091
  execute: async (args, _api, context4) => {
5623
- const result2 = await client.callTool(tool.name, args, {
7092
+ const result3 = await client.callTool(tool.name, args, {
5624
7093
  signal: context4.abortSignal ?? NEVER_ABORT,
5625
7094
  timeoutMs
5626
7095
  });
5627
- return { output: toLlmContent(result2), structuredOutput: result2, isError: result2.isError === true };
7096
+ return { output: toLlmContent(result3), structuredOutput: result3, isError: result3.isError === true };
5628
7097
  }
5629
7098
  });
5630
7099
  }
@@ -5644,7 +7113,7 @@ function startMcp(options) {
5644
7113
  listener(value);
5645
7114
  };
5646
7115
  const install = () => {
5647
- options.registry.install(defineExtension5({ name: "mcp", tools: [...toolsets.values()].flat() }));
7116
+ options.registry.install(defineExtension7({ name: "mcp", tools: [...toolsets.values()].flat() }));
5648
7117
  options.onNamespaces?.([...namespaces.values()]);
5649
7118
  };
5650
7119
  const set = (name, patch) => {
@@ -5764,7 +7233,7 @@ ${stderr}`;
5764
7233
  import { randomBytes as randomBytes2 } from "node:crypto";
5765
7234
  import { existsSync as existsSync2, watch } from "node:fs";
5766
7235
  import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
5767
- import { join as join8 } from "node:path";
7236
+ import { join as join9 } from "node:path";
5768
7237
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
5769
7238
 
5770
7239
  // packages/protocol/src/agents.ts
@@ -5808,7 +7277,7 @@ async function load(dir) {
5808
7277
  const problem = (reason2) => void problems.push({ file, reason: reason2 });
5809
7278
  let parsed;
5810
7279
  try {
5811
- parsed = parseFrontmatter(await readFile3(join8(dir, file), "utf8"));
7280
+ parsed = parseFrontmatter(await readFile3(join9(dir, file), "utf8"));
5812
7281
  } catch (error) {
5813
7282
  problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
5814
7283
  continue;
@@ -5920,13 +7389,13 @@ async function openAgentRoles(options) {
5920
7389
  if (problem !== undefined)
5921
7390
  throw new Error(problem);
5922
7391
  await enqueue(async () => {
5923
- const target = join8(dir, `${role.name}.md`);
7392
+ const target = join9(dir, `${role.name}.md`);
5924
7393
  const exists = existsSync2(target);
5925
7394
  if (mustExist && !exists)
5926
7395
  throw new Error(`No such agent: "${role.name}"`);
5927
7396
  if (!mustExist && exists)
5928
7397
  throw new Error(`Agent "${role.name}" already exists`);
5929
- const temp = join8(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
7398
+ const temp = join9(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
5930
7399
  try {
5931
7400
  await writeFile(temp, serialize(role));
5932
7401
  await rename(temp, target);
@@ -5949,7 +7418,7 @@ async function openAgentRoles(options) {
5949
7418
  if (problem !== undefined)
5950
7419
  throw new Error(problem);
5951
7420
  await enqueue(async () => {
5952
- const target = join8(dir, `${name}.md`);
7421
+ const target = join9(dir, `${name}.md`);
5953
7422
  if (!existsSync2(target))
5954
7423
  throw new Error(`No such agent: "${name}"`);
5955
7424
  await rm2(target);
@@ -5983,7 +7452,7 @@ import {
5983
7452
  stat as stat7,
5984
7453
  writeFile as writeFile2
5985
7454
  } from "node:fs/promises";
5986
- import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
7455
+ import { basename as basename2, join as join10, resolve as resolve6 } from "node:path";
5987
7456
  var META = "meta.json";
5988
7457
  var DATA = "file";
5989
7458
  var CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
@@ -6078,16 +7547,16 @@ class AttachmentStore {
6078
7547
  return next;
6079
7548
  }
6080
7549
  #directory(id) {
6081
- return join9(this.#root, id);
7550
+ return join10(this.#root, id);
6082
7551
  }
6083
7552
  #dataPath(meta) {
6084
- return join9(this.#directory(meta.id), DATA, meta.fileName);
7553
+ return join10(this.#directory(meta.id), DATA, meta.fileName);
6085
7554
  }
6086
7555
  async#readMeta(id) {
6087
7556
  if (!isAttachmentId(id))
6088
7557
  return;
6089
7558
  try {
6090
- return metaOf(JSON.parse(await readFile4(join9(this.#directory(id), META), "utf8")), id);
7559
+ return metaOf(JSON.parse(await readFile4(join10(this.#directory(id), META), "utf8")), id);
6091
7560
  } catch {
6092
7561
  return;
6093
7562
  }
@@ -6095,10 +7564,10 @@ class AttachmentStore {
6095
7564
  async#writeMeta(meta) {
6096
7565
  const { id, ...value } = meta;
6097
7566
  const directory = this.#directory(id);
6098
- const temporary = join9(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
7567
+ const temporary = join10(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
6099
7568
  await writeFile2(temporary, `${JSON.stringify(value)}
6100
7569
  `, { mode: 384 });
6101
- await rename2(temporary, join9(directory, META));
7570
+ await rename2(temporary, join10(directory, META));
6102
7571
  }
6103
7572
  async#rebuildPending() {
6104
7573
  this.#pending.clear();
@@ -6143,7 +7612,7 @@ class AttachmentStore {
6143
7612
  expiresAt: this.#now() + this.#ttlMs
6144
7613
  };
6145
7614
  try {
6146
- await mkdir3(join9(directory, DATA), { recursive: true });
7615
+ await mkdir3(join10(directory, DATA), { recursive: true });
6147
7616
  await writeFile2(this.#dataPath(meta), new Uint8Array(0), { mode: 384 });
6148
7617
  if (size === 0)
6149
7618
  await chmod(this.#dataPath(meta), 292);
@@ -6279,16 +7748,16 @@ class AttachmentStore {
6279
7748
  }
6280
7749
 
6281
7750
  // apps/host/src/answer-timing.ts
6282
- function withAnswerStart(message, answerStartedAt) {
6283
- const pinomad = message.pinomad;
6284
- return { ...message, pinomad: { ...pinomad, answerStartedAt } };
7751
+ function withAnswerStart(message2, answerStartedAt) {
7752
+ const pinomad = message2.pinomad;
7753
+ return { ...message2, pinomad: { ...pinomad, answerStartedAt } };
6285
7754
  }
6286
7755
  function stripPinomad(context4) {
6287
7756
  let changed = false;
6288
- const messages = context4.messages.map((message) => {
6289
- if (message.role !== "assistant" || !("pinomad" in message))
6290
- return message;
6291
- const { pinomad: _pinomad, ...rest } = message;
7757
+ const messages = context4.messages.map((message2) => {
7758
+ if (message2.role !== "assistant" || !("pinomad" in message2))
7759
+ return message2;
7760
+ const { pinomad: _pinomad, ...rest } = message2;
6292
7761
  changed = true;
6293
7762
  return rest;
6294
7763
  });
@@ -6313,7 +7782,7 @@ function trackAnswerStart(stream) {
6313
7782
  const push = stream.push.bind(stream);
6314
7783
  stream.push = (event) => push(annotate(event));
6315
7784
  const end = stream.end.bind(stream);
6316
- stream.end = (result2) => end(result2 === undefined || answerStartedAt === undefined ? result2 : withAnswerStart(result2, answerStartedAt));
7785
+ stream.end = (result3) => end(result3 === undefined || answerStartedAt === undefined ? result3 : withAnswerStart(result3, answerStartedAt));
6317
7786
  return stream;
6318
7787
  }
6319
7788
  function withAnswerTiming(models) {
@@ -6328,10 +7797,594 @@ function withAnswerTiming(models) {
6328
7797
  });
6329
7798
  }
6330
7799
 
7800
+ // apps/host/src/processes.ts
7801
+ import { randomUUID as randomUUID4 } from "node:crypto";
7802
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT5 } from "@earendil-works/chord/context";
7803
+ var PROCESS_LIMIT = 8;
7804
+ var ENDED_PROCESS_LIMIT = 20;
7805
+ var ENDED_OUTPUT_BUDGET = 64 * 1024 * 1024;
7806
+ var STOP_GRACE_MS = 5000;
7807
+ var GROUP_WAIT_MS = 2000;
7808
+ var OUTPUT_CHUNK = 16 * 1024;
7809
+ var REPORT_LIMIT = 4 * 1024;
7810
+ var REPORT_LINES = 40;
7811
+ var REPORT_TAIL = `
7812
+
7813
+ Last output:
7814
+ `;
7815
+
7816
+ class Processes {
7817
+ #options;
7818
+ #limit;
7819
+ #endedLimit;
7820
+ #outputBudget;
7821
+ #processes = new Map;
7822
+ #roots = new Map;
7823
+ #sealed = new Set;
7824
+ #busy;
7825
+ #closing = false;
7826
+ constructor(options) {
7827
+ this.#options = options;
7828
+ this.#limit = options.limit ?? PROCESS_LIMIT;
7829
+ this.#endedLimit = options.endedLimit ?? ENDED_PROCESS_LIMIT;
7830
+ this.#outputBudget = options.outputBudget ?? ENDED_OUTPUT_BUDGET;
7831
+ this.#busy = new Published(false, (a, b) => a === b);
7832
+ }
7833
+ get limit() {
7834
+ return this.#limit;
7835
+ }
7836
+ get hello() {
7837
+ return { limit: this.#limit };
7838
+ }
7839
+ get busy() {
7840
+ return this.#busy;
7841
+ }
7842
+ async open(context4) {
7843
+ await settleLostProcesses(this.#options.harness(), context4);
7844
+ }
7845
+ async start(input, context4) {
7846
+ const harness = this.#options.harness();
7847
+ const root = await this.#root(input.conversationId);
7848
+ if (this.#closing)
7849
+ throw new Error("The host is shutting down");
7850
+ if (this.#options.installing?.() === true)
7851
+ throw new Error("An upgrade is installing; try again after it finishes");
7852
+ if (this.#sealed.has(input.conversationId) || await this.#archivedInFamily(harness, input.conversationId, context4)) {
7853
+ throw new Error("This conversation is archived");
7854
+ }
7855
+ if (this.#closing)
7856
+ throw new Error("The host is shutting down");
7857
+ if (this.#options.installing?.() === true)
7858
+ throw new Error("An upgrade is installing; try again after it finishes");
7859
+ if (this.#sealed.has(input.conversationId))
7860
+ throw new Error("This conversation is archived");
7861
+ const existing = input.id === undefined ? undefined : this.#processes.get(input.id);
7862
+ if (existing !== undefined) {
7863
+ if (!isProcessActive(existing.info.state))
7864
+ throw new Error(`Process ${existing.info.id} has already ended`);
7865
+ return info2(existing);
7866
+ }
7867
+ const active = [...this.#processes.values()].filter((entry2) => entry2.root === root && isProcessActive(entry2.info.state)).length;
7868
+ if (active >= this.#limit)
7869
+ throw new Error(`Process limit reached (${this.#limit})`);
7870
+ const id = input.id ?? randomUUID4();
7871
+ let resolveDone;
7872
+ const done = new Promise((resolve7) => {
7873
+ resolveDone = resolve7;
7874
+ });
7875
+ const entry = {
7876
+ info: {
7877
+ id,
7878
+ owner: input.conversationId,
7879
+ name: input.name,
7880
+ command: input.command,
7881
+ startedAt: this.#now(),
7882
+ state: "starting"
7883
+ },
7884
+ root,
7885
+ output: new OutputBuffer({
7886
+ ...this.#options.bufferSize === undefined ? {} : { bufferSize: this.#options.bufferSize },
7887
+ ...this.#options.flushMs === undefined ? {} : { flushMs: this.#options.flushMs },
7888
+ onFlush: () => this.#options.onOutput?.()
7889
+ }),
7890
+ done,
7891
+ resolveDone,
7892
+ pty: undefined,
7893
+ stopReason: undefined,
7894
+ killTimer: undefined,
7895
+ groupTimer: undefined,
7896
+ killed: false,
7897
+ killedAt: undefined,
7898
+ killWaitTimer: undefined,
7899
+ finishing: false,
7900
+ generation: 0,
7901
+ released: false
7902
+ };
7903
+ this.#processes.set(id, entry);
7904
+ this.#refreshBusy();
7905
+ try {
7906
+ await harness.commit((tx) => this.#applyDoc(tx, entry), context4);
7907
+ } catch (error) {
7908
+ this.#processes.delete(id);
7909
+ this.#refreshBusy();
7910
+ throw error;
7911
+ }
7912
+ try {
7913
+ this.#spawn(entry, input.cwd);
7914
+ } catch (error) {
7915
+ await this.#fail(entry, message2(error), context4);
7916
+ throw error;
7917
+ }
7918
+ await this.#write(entry, context4);
7919
+ return info2(entry);
7920
+ }
7921
+ async output(input, context4) {
7922
+ const root = await this.#root(input.conversationId);
7923
+ const entry = this.#processes.get(input.id);
7924
+ if (entry !== undefined && entry.root === root && !entry.released) {
7925
+ entry.output.flush();
7926
+ const slice = entry.output.snapshot(input.since, OUTPUT_CHUNK);
7927
+ const state = entry.info.state;
7928
+ return {
7929
+ text: stripAnsi(slice.text),
7930
+ next: slice.next,
7931
+ truncated: slice.truncated,
7932
+ status: state,
7933
+ ...typeof state === "object" && "exited" in state ? { exitCode: state.exited } : {}
7934
+ };
7935
+ }
7936
+ const record = await this.#docEntry(root, input.id, context4);
7937
+ if (record === undefined)
7938
+ throw new Error("Unknown process");
7939
+ return {
7940
+ text: "",
7941
+ next: input.since ?? 0,
7942
+ truncated: true,
7943
+ status: record.state,
7944
+ ...typeof record.state === "object" && "exited" in record.state ? { exitCode: record.state.exited } : {},
7945
+ released: true
7946
+ };
7947
+ }
7948
+ async stop(input, context4) {
7949
+ const root = await this.#root(input.conversationId);
7950
+ const entry = this.#processes.get(input.id);
7951
+ if (entry !== undefined && entry.root === root) {
7952
+ return await this.#stopEntry(entry, input.reason ?? "user", context4);
7953
+ }
7954
+ if (entry !== undefined)
7955
+ throw new Error("This process does not belong to the conversation's family");
7956
+ const record = await this.#docEntry(root, input.id, context4);
7957
+ if (record === undefined)
7958
+ throw new Error("Unknown process");
7959
+ return record;
7960
+ }
7961
+ async list(conversationId, context4) {
7962
+ const root = await this.#root(conversationId);
7963
+ const doc = await this.#options.harness().snapshot(ProcessDoc, root, context4);
7964
+ return doc?.processes ?? [];
7965
+ }
7966
+ async#docEntry(root, id, context4) {
7967
+ const doc = await this.#options.harness().snapshot(ProcessDoc, root, context4);
7968
+ return doc?.processes.find((process2) => process2.id === id);
7969
+ }
7970
+ attach(id, since, sink) {
7971
+ const entry = this.#processes.get(id);
7972
+ if (entry === undefined)
7973
+ throw new Error("Unknown process");
7974
+ if (entry.released)
7975
+ throw new Error("The output of this process is no longer available");
7976
+ return entry.output.attach(since, sink);
7977
+ }
7978
+ seal(owners) {
7979
+ for (const owner of owners)
7980
+ this.#sealed.add(owner);
7981
+ }
7982
+ unseal(owners) {
7983
+ for (const owner of owners)
7984
+ this.#sealed.delete(owner);
7985
+ }
7986
+ async closeOwners(owners, reason, context4) {
7987
+ const set = new Set(owners);
7988
+ const entries = [...this.#processes.values()].filter((entry) => set.has(entry.info.owner));
7989
+ await Promise.all(entries.map((entry) => this.#stopEntry(entry, reason, context4)));
7990
+ for (const entry of entries) {
7991
+ this.#release(entry, "Conversation archived");
7992
+ this.#processes.delete(entry.info.id);
7993
+ entry.resolveDone();
7994
+ }
7995
+ this.#refreshBusy();
7996
+ }
7997
+ async drain(context4) {
7998
+ const entries = [...this.#processes.values()].filter((entry) => isProcessActive(entry.info.state));
7999
+ await Promise.all(entries.map((entry) => this.#stopEntry(entry, "shutdown", context4)));
8000
+ }
8001
+ async close(context4) {
8002
+ this.#closing = true;
8003
+ const entries = [...this.#processes.values()];
8004
+ await Promise.all(entries.filter((entry) => isProcessActive(entry.info.state)).map((entry) => this.#stopEntry(entry, "shutdown", context4)));
8005
+ for (const entry of entries) {
8006
+ this.#release(entry, "The host shut down");
8007
+ this.#processes.delete(entry.info.id);
8008
+ entry.resolveDone();
8009
+ }
8010
+ this.#sealed.clear();
8011
+ this.#refreshBusy();
8012
+ }
8013
+ async#archivedInFamily(harness, conversationId, context4) {
8014
+ let current = conversationId;
8015
+ const seen = new Set;
8016
+ while (current !== undefined && !seen.has(current)) {
8017
+ seen.add(current);
8018
+ const index = await harness.snapshot(IndexDoc, context4);
8019
+ if (index?.conversations.some((entry) => entry.id === current && entry.archived === true) === true)
8020
+ return true;
8021
+ const record = await harness.commit((tx) => tx.conversation(current), BACKGROUND_CONTEXT5);
8022
+ current = record?.parent?.conversationId ?? record?.owner?.conversationId;
8023
+ }
8024
+ return false;
8025
+ }
8026
+ async#root(conversationId) {
8027
+ const cached = this.#roots.get(conversationId);
8028
+ if (cached !== undefined)
8029
+ return cached;
8030
+ const root = await familyRoot(this.#options.harness(), conversationId, BACKGROUND_CONTEXT5);
8031
+ this.#roots.set(conversationId, root);
8032
+ return root;
8033
+ }
8034
+ #spawn(entry, cwd) {
8035
+ const generation = ++entry.generation;
8036
+ const { shell, env } = this.#options;
8037
+ const pty = this.#options.spawn(shell.file, [...shell.args, "-c", entry.info.command], {
8038
+ name: "xterm-256color",
8039
+ cols: 120,
8040
+ rows: 40,
8041
+ cwd,
8042
+ env: { ...env, TERM: "xterm-256color", COLORTERM: "truecolor" }
8043
+ });
8044
+ entry.pty = pty;
8045
+ if (pty.pid !== undefined)
8046
+ entry.info.pid = pty.pid;
8047
+ entry.info.state = "running";
8048
+ pty.onData((data) => {
8049
+ if (entry.generation !== generation || entry.released)
8050
+ return;
8051
+ entry.output.push(data);
8052
+ });
8053
+ pty.onExit(({ exitCode, signal }) => {
8054
+ if (entry.generation !== generation || entry.released || !this.#processes.has(entry.info.id))
8055
+ return;
8056
+ this.#exit(entry, exitCode, signal);
8057
+ });
8058
+ }
8059
+ async#exit(entry, exitCode, signal) {
8060
+ entry.pty = undefined;
8061
+ entry.output.flush();
8062
+ if (entry.stopReason === undefined && this.#closing)
8063
+ entry.stopReason = "shutdown";
8064
+ if (entry.stopReason !== undefined) {
8065
+ if (this.#groupAlive(entry)) {
8066
+ this.#ensureKillTimer(entry);
8067
+ this.#pollGroup(entry);
8068
+ return;
8069
+ }
8070
+ await this.#finishStopped(entry);
8071
+ return;
8072
+ }
8073
+ this.#clearStopTimers(entry);
8074
+ entry.info.endedAt = this.#now();
8075
+ entry.info.state = signal === undefined || signal === 0 ? { exited: exitCode } : { exited: null, signal: signalName(signal) };
8076
+ const report = this.#options.installing?.() !== true;
8077
+ try {
8078
+ if (report)
8079
+ await this.#reportAndWrite(entry);
8080
+ else
8081
+ await this.#write(entry, BACKGROUND_CONTEXT5);
8082
+ } catch (error) {
8083
+ this.#diagnose(error);
8084
+ }
8085
+ entry.resolveDone();
8086
+ this.#refreshBusy();
8087
+ this.#evictOutputs();
8088
+ }
8089
+ async#finishStopped(entry) {
8090
+ if (entry.finishing)
8091
+ return;
8092
+ entry.finishing = true;
8093
+ this.#clearStopTimers(entry);
8094
+ entry.info.endedAt = this.#now();
8095
+ entry.info.state = { stopped: entry.stopReason ?? "shutdown" };
8096
+ try {
8097
+ await this.#write(entry, BACKGROUND_CONTEXT5);
8098
+ } catch (error) {
8099
+ this.#diagnose(error);
8100
+ }
8101
+ entry.resolveDone();
8102
+ this.#refreshBusy();
8103
+ this.#evictOutputs();
8104
+ }
8105
+ #ensureKillTimer(entry) {
8106
+ if (entry.killTimer !== undefined || entry.killed)
8107
+ return;
8108
+ const grace = this.#options.stopGraceMs ?? STOP_GRACE_MS;
8109
+ entry.killTimer = setTimeout(() => {
8110
+ entry.killTimer = undefined;
8111
+ entry.killed = true;
8112
+ entry.killedAt = this.#now();
8113
+ this.#signal(entry, "SIGKILL");
8114
+ entry.killWaitTimer ??= setTimeout(() => this.#finishAfterKillWait(entry), this.#options.groupWaitMs ?? GROUP_WAIT_MS);
8115
+ }, grace);
8116
+ }
8117
+ #pollGroup(entry) {
8118
+ if (entry.groupTimer !== undefined || entry.finishing)
8119
+ return;
8120
+ const check = () => {
8121
+ entry.groupTimer = undefined;
8122
+ if (entry.finishing || entry.released || !this.#processes.has(entry.info.id))
8123
+ return;
8124
+ if (!this.#groupAlive(entry)) {
8125
+ this.#finishStopped(entry);
8126
+ return;
8127
+ }
8128
+ const wait = this.#options.groupWaitMs ?? GROUP_WAIT_MS;
8129
+ if (entry.killedAt !== undefined && this.#now() - entry.killedAt >= wait) {
8130
+ this.#finishAfterKillWait(entry);
8131
+ return;
8132
+ }
8133
+ entry.groupTimer = setTimeout(check, 25);
8134
+ };
8135
+ check();
8136
+ }
8137
+ #finishAfterKillWait(entry) {
8138
+ entry.killWaitTimer = undefined;
8139
+ if (entry.finishing || entry.released || !this.#processes.has(entry.info.id))
8140
+ return;
8141
+ if (this.#groupAlive(entry)) {
8142
+ this.#diagnose(`Process group ${entry.info.pid ?? "unknown"} still appears alive after SIGKILL; marking it stopped`);
8143
+ }
8144
+ this.#finishStopped(entry);
8145
+ }
8146
+ #groupAlive(entry) {
8147
+ const pid = entry.info.pid;
8148
+ if (pid === undefined)
8149
+ return false;
8150
+ const alive = this.#options.groupAlive ?? ((group) => {
8151
+ try {
8152
+ process.kill(-group, 0);
8153
+ return true;
8154
+ } catch {
8155
+ return false;
8156
+ }
8157
+ });
8158
+ try {
8159
+ return alive(pid);
8160
+ } catch {
8161
+ return false;
8162
+ }
8163
+ }
8164
+ #clearStopTimers(entry) {
8165
+ if (entry.killTimer !== undefined)
8166
+ clearTimeout(entry.killTimer);
8167
+ if (entry.groupTimer !== undefined)
8168
+ clearTimeout(entry.groupTimer);
8169
+ if (entry.killWaitTimer !== undefined)
8170
+ clearTimeout(entry.killWaitTimer);
8171
+ entry.killTimer = undefined;
8172
+ entry.groupTimer = undefined;
8173
+ entry.killWaitTimer = undefined;
8174
+ }
8175
+ async#fail(entry, reason, context4) {
8176
+ entry.generation++;
8177
+ entry.pty = undefined;
8178
+ entry.info.state = { failed: reason };
8179
+ entry.info.endedAt = this.#now();
8180
+ try {
8181
+ await this.#write(entry, context4);
8182
+ } catch (error) {
8183
+ this.#diagnose(error);
8184
+ }
8185
+ entry.resolveDone();
8186
+ this.#refreshBusy();
8187
+ this.#evictOutputs();
8188
+ }
8189
+ async#stopEntry(entry, reason, context4) {
8190
+ if (!isProcessActive(entry.info.state))
8191
+ return info2(entry);
8192
+ entry.stopReason ??= reason;
8193
+ this.#signal(entry, "SIGTERM");
8194
+ this.#ensureKillTimer(entry);
8195
+ await entry.done;
8196
+ return info2(entry);
8197
+ }
8198
+ #signal(entry, signal) {
8199
+ const pid = entry.info.pid;
8200
+ try {
8201
+ if (pid !== undefined) {
8202
+ const kill = this.#options.kill ?? ((target, value) => process.kill(target, value));
8203
+ kill(-pid, signal);
8204
+ } else {
8205
+ entry.pty?.kill(signal);
8206
+ }
8207
+ } catch {}
8208
+ }
8209
+ async#write(entry, context4) {
8210
+ const kept = await this.#options.harness().commit((tx) => this.#applyDoc(tx, entry), context4);
8211
+ this.#retireMissing(kept, entry.root);
8212
+ }
8213
+ async#reportAndWrite(entry) {
8214
+ const harness = this.#options.harness();
8215
+ const conversation = await harness.conversation(entry.info.owner, BACKGROUND_CONTEXT5);
8216
+ const report = reportText(entry);
8217
+ if (conversation === undefined) {
8218
+ await this.#write(entry, BACKGROUND_CONTEXT5);
8219
+ return;
8220
+ }
8221
+ const kept = await conversation.commit(async (tx) => {
8222
+ await conversation.submit(tx, {
8223
+ type: "input",
8224
+ content: report,
8225
+ whenBusy: "steer",
8226
+ requestId: `process:${entry.info.id}:exit`
8227
+ });
8228
+ return this.#applyDoc(tx, entry);
8229
+ }, BACKGROUND_CONTEXT5);
8230
+ this.#retireMissing(kept, entry.root);
8231
+ }
8232
+ async#applyDoc(tx, entry) {
8233
+ const doc = await tx.doc(ProcessDoc, entry.root);
8234
+ const next = docEntry(entry.info);
8235
+ const index = doc.processes.findIndex((process2) => process2.id === entry.info.id);
8236
+ if (index === -1)
8237
+ doc.processes.push(next);
8238
+ else
8239
+ doc.processes[index] = next;
8240
+ const ended = doc.processes.filter((process2) => !isProcessActive(process2.state)).sort((a, b) => (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt)).slice(0, this.#endedLimit);
8241
+ const keep = new Set(ended.map((process2) => process2.id));
8242
+ doc.processes = doc.processes.filter((process2) => isProcessActive(process2.state) || keep.has(process2.id));
8243
+ return new Set(doc.processes.map((process2) => process2.id));
8244
+ }
8245
+ #retireMissing(kept, root) {
8246
+ for (const [id, entry] of this.#processes) {
8247
+ if (entry.root !== root || kept.has(id) || isProcessActive(entry.info.state))
8248
+ continue;
8249
+ this.#release(entry, "Output released");
8250
+ this.#processes.delete(id);
8251
+ entry.resolveDone();
8252
+ }
8253
+ this.#refreshBusy();
8254
+ }
8255
+ #release(entry, reason) {
8256
+ if (entry.released)
8257
+ return;
8258
+ entry.released = true;
8259
+ this.#clearStopTimers(entry);
8260
+ entry.output.close(reason);
8261
+ entry.output.discard();
8262
+ }
8263
+ #evictOutputs() {
8264
+ let total = 0;
8265
+ const ended = [];
8266
+ for (const entry of this.#processes.values()) {
8267
+ if (isProcessActive(entry.info.state) || entry.released)
8268
+ continue;
8269
+ total += entry.output.length;
8270
+ ended.push(entry);
8271
+ }
8272
+ if (total <= this.#outputBudget)
8273
+ return;
8274
+ ended.sort((a, b) => (a.info.endedAt ?? a.info.startedAt) - (b.info.endedAt ?? b.info.startedAt));
8275
+ for (const entry of ended) {
8276
+ if (total <= this.#outputBudget)
8277
+ return;
8278
+ total -= entry.output.length;
8279
+ this.#release(entry, "Output released");
8280
+ }
8281
+ }
8282
+ #refreshBusy() {
8283
+ this.#busy.set([...this.#processes.values()].some((entry) => isProcessActive(entry.info.state)));
8284
+ }
8285
+ #now() {
8286
+ return (this.#options.now ?? Date.now)();
8287
+ }
8288
+ #diagnose(error) {
8289
+ (this.#options.diagnostic ?? console.error)(`Background process update failed: ${message2(error)}`);
8290
+ }
8291
+ }
8292
+ async function startProcesses(options) {
8293
+ let spawn4 = options.spawn;
8294
+ if (spawn4 === undefined) {
8295
+ try {
8296
+ const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
8297
+ if (typeof loaded.spawn !== "function")
8298
+ throw new Error("the module has no spawn()");
8299
+ spawn4 = loaded.spawn;
8300
+ } catch (error) {
8301
+ const detail = error instanceof Error ? error.message : String(error);
8302
+ (options.diagnostic ?? console.error)(`Background processes 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.processes. (${detail})`);
8303
+ return;
8304
+ }
8305
+ }
8306
+ return new Processes({
8307
+ ...options,
8308
+ spawn: spawn4,
8309
+ shell: options.shell ?? loginShell(),
8310
+ env: options.env ?? shellEnv(process.env)
8311
+ });
8312
+ }
8313
+ async function settleLostProcesses(harness, context4) {
8314
+ let cursor;
8315
+ do {
8316
+ const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context4);
8317
+ for (const record of page.items) {
8318
+ if (record.parent !== undefined || record.owner !== undefined)
8319
+ continue;
8320
+ const doc = await harness.snapshot(ProcessDoc, record.id, context4);
8321
+ if (doc === undefined || !doc.processes.some((process2) => process2.state === "starting" || process2.state === "running")) {
8322
+ continue;
8323
+ }
8324
+ await harness.commit(async (tx) => {
8325
+ const value = await tx.doc(ProcessDoc, record.id);
8326
+ for (const process2 of value.processes) {
8327
+ if (process2.state === "starting" || process2.state === "running")
8328
+ process2.state = "lost";
8329
+ }
8330
+ }, context4);
8331
+ }
8332
+ cursor = page.next;
8333
+ } while (cursor !== undefined);
8334
+ }
8335
+ function info2(entry) {
8336
+ return { ...entry.info, ...entry.info.pid === undefined ? {} : { pid: entry.info.pid } };
8337
+ }
8338
+ function docEntry(entry) {
8339
+ return {
8340
+ id: entry.id,
8341
+ owner: entry.owner,
8342
+ name: entry.name,
8343
+ command: entry.command,
8344
+ ...entry.pid === undefined ? {} : { pid: entry.pid },
8345
+ startedAt: entry.startedAt,
8346
+ ...entry.endedAt === undefined ? {} : { endedAt: entry.endedAt },
8347
+ state: typeof entry.state === "object" ? { ...entry.state } : entry.state
8348
+ };
8349
+ }
8350
+ function reportText(entry) {
8351
+ const state = entry.info.state;
8352
+ const outcome = typeof state === "object" && "exited" in state ? state.signal === undefined ? `exited with code ${state.exited ?? "unknown"}` : `exited after signal ${state.signal}` : "exited";
8353
+ const raw = entry.output.snapshot(undefined, Number.MAX_SAFE_INTEGER).text;
8354
+ const clean = stripAnsi(raw).replace(/\r\n/g, `
8355
+ `).replace(/\r/g, `
8356
+ `);
8357
+ const tail = clean.split(`
8358
+ `).slice(-REPORT_LINES).join(`
8359
+ `).trimEnd();
8360
+ const header = `Background process "${entry.info.name}" (${entry.info.id}) ${outcome}.`;
8361
+ const available = Math.max(0, REPORT_LIMIT - header.length - REPORT_TAIL.length);
8362
+ const body = truncateTail(tail, available);
8363
+ return body === "" ? header : `${header}${REPORT_TAIL}${body}`;
8364
+ }
8365
+ function stripAnsi(text) {
8366
+ return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b[PX^_][^\x1b]*(?:\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1b[@-_]/g, "");
8367
+ }
8368
+ function truncateTail(text, max) {
8369
+ if (text.length <= max)
8370
+ return text;
8371
+ let from = text.length - max;
8372
+ const code = text.charCodeAt(from);
8373
+ if (code >= 56320 && code <= 57343)
8374
+ from++;
8375
+ return text.slice(from);
8376
+ }
8377
+ function signalName(signal) {
8378
+ return signal === 1 ? "SIGHUP" : signal === 2 ? "SIGINT" : signal === 9 ? "SIGKILL" : signal === 15 ? "SIGTERM" : String(signal);
8379
+ }
8380
+ function message2(error) {
8381
+ return error instanceof Error ? error.message : String(error);
8382
+ }
8383
+
6331
8384
  // apps/host/src/host.ts
6332
- var context4 = BACKGROUND_CONTEXT4;
8385
+ var context4 = BACKGROUND_CONTEXT6;
6333
8386
  async function hostToken(dataDir) {
6334
- const path = join10(dataDir, "token");
8387
+ const path = join11(dataDir, "token");
6335
8388
  try {
6336
8389
  return (await readFile5(path, "utf8")).trim();
6337
8390
  } catch {
@@ -6346,7 +8399,7 @@ async function openHost(options) {
6346
8399
  validatePublicUrl(options.remote.publicUrl);
6347
8400
  await mkdir4(options.dataDir, { recursive: true });
6348
8401
  const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
6349
- const release = await lockfile.lock(options.dataDir, {
8402
+ const release2 = await lockfile.lock(options.dataDir, {
6350
8403
  realpath: false,
6351
8404
  stale,
6352
8405
  retries: { retries: Math.ceil(stale / 1000) + 2, minTimeout: 1000, maxTimeout: 1000 }
@@ -6354,26 +8407,38 @@ async function openHost(options) {
6354
8407
  throw new Error(`Session is already open in another process: ${options.dataDir}`, { cause: error });
6355
8408
  });
6356
8409
  let harness;
8410
+ let processes;
6357
8411
  let mcp;
6358
8412
  let agentRoles;
6359
8413
  let attachments;
6360
8414
  let extensions = [];
8415
+ let upgrader;
6361
8416
  try {
6362
8417
  const models = withAnswerTiming(options.models);
6363
8418
  const token = await hostToken(options.dataDir);
6364
- attachments = new AttachmentStore({ dir: join10(options.dataDir, "attachments") });
8419
+ attachments = new AttachmentStore({ dir: join11(options.dataDir, "attachments") });
6365
8420
  await attachments.open();
6366
8421
  const reports = [];
6367
8422
  let report = (error) => void reports.push(error);
8423
+ processes = await startProcesses({
8424
+ ...options.processes,
8425
+ installing: () => upgrader?.installing === true,
8426
+ harness: () => {
8427
+ if (harness === undefined)
8428
+ throw new Error("harness is not open yet");
8429
+ return harness;
8430
+ }
8431
+ });
6368
8432
  const registry = createRegistry();
6369
- agentRoles = await openAgentRoles({ dir: join10(options.dataDir, "agents") });
8433
+ agentRoles = await openAgentRoles({ dir: join11(options.dataDir, "agents") });
6370
8434
  extensions = typeof options.extensions === "function" ? options.extensions({
6371
8435
  harness: () => {
6372
8436
  if (harness === undefined)
6373
8437
  throw new Error("harness is not open yet");
6374
8438
  return harness;
6375
8439
  },
6376
- agentRoles
8440
+ agentRoles,
8441
+ ...processes === undefined ? {} : { processes }
6377
8442
  }) : options.extensions ?? [];
6378
8443
  for (const { extension } of extensions)
6379
8444
  registry.install(extension);
@@ -6389,13 +8454,13 @@ async function openHost(options) {
6389
8454
  }
6390
8455
  });
6391
8456
  const hostSettings = await openHostSettings({
6392
- path: join10(options.dataDir, "settings.json"),
8457
+ path: join11(options.dataDir, "settings.json"),
6393
8458
  fallback: options.settingsFallback,
6394
8459
  models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
6395
8460
  });
6396
8461
  const environments = new Map;
6397
- const chatsDir = join10(options.dataDir, "chats");
6398
- harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join10(options.dataDir, "session.sqlite")), {
8462
+ const chatsDir = join11(options.dataDir, "chats");
8463
+ harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join11(options.dataDir, "session.sqlite")), {
6399
8464
  models,
6400
8465
  registry,
6401
8466
  env: async (target) => {
@@ -6403,7 +8468,7 @@ async function openHost(options) {
6403
8468
  if (cwd === undefined)
6404
8469
  return;
6405
8470
  const worktrees = worktreeRoot(options.dataDir);
6406
- if (cwd === worktrees || cwd.startsWith(worktrees + sep6)) {
8471
+ if (cwd === worktrees || cwd.startsWith(worktrees + sep7)) {
6407
8472
  const index = await target.read.snapshot(IndexDoc, context4);
6408
8473
  const record = checkoutAt(index?.checkouts, cwd);
6409
8474
  if (record !== undefined) {
@@ -6425,6 +8490,10 @@ async function openHost(options) {
6425
8490
  },
6426
8491
  onReport: (error) => report(error)
6427
8492
  }, context4);
8493
+ if (processes === undefined)
8494
+ await settleLostProcesses(harness, context4);
8495
+ else
8496
+ await processes.open(context4);
6428
8497
  await ensureIndex(harness, context4);
6429
8498
  await ensureDevices(harness, context4);
6430
8499
  for (const path of options.projects ?? [])
@@ -6432,6 +8501,18 @@ async function openHost(options) {
6432
8501
  const hostKey = await loadHostKey(options.dataDir);
6433
8502
  const offers = pairingOffers(options.remote?.pairingTtlMs);
6434
8503
  const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
8504
+ upgrader = options.upgrade === undefined ? undefined : await startHostUpgrader({
8505
+ current: options.upgrade.current,
8506
+ dataDir: options.dataDir,
8507
+ harness,
8508
+ settings: hostSettings,
8509
+ context: context4,
8510
+ seams: options.upgrade,
8511
+ ...processes === undefined ? {} : {
8512
+ processesBusy: processes.busy,
8513
+ drain: () => processes.drain(context4)
8514
+ }
8515
+ });
6435
8516
  const gateway = await startGateway({
6436
8517
  harness,
6437
8518
  models,
@@ -6461,6 +8542,8 @@ async function openHost(options) {
6461
8542
  },
6462
8543
  ...options.rtc === undefined ? {} : { rtc: options.rtc },
6463
8544
  ...options.terminals === undefined ? {} : { terminals: options.terminals },
8545
+ ...processes === undefined ? {} : { processes },
8546
+ ...upgrader === undefined ? {} : { upgrade: upgrader },
6464
8547
  secure: {
6465
8548
  hostKey,
6466
8549
  offers,
@@ -6493,6 +8576,7 @@ async function openHost(options) {
6493
8576
  harness,
6494
8577
  closed: opened.closed,
6495
8578
  settings: hostSettings,
8579
+ ...processes === undefined ? {} : { processes },
6496
8580
  createAccountOffer: async () => {
6497
8581
  const binding = await readServerBinding(options.dataDir).catch(() => {
6498
8582
  return;
@@ -6512,6 +8596,7 @@ async function openHost(options) {
6512
8596
  close() {
6513
8597
  closing ??= (async () => {
6514
8598
  try {
8599
+ upgrader?.close();
6515
8600
  await gateway.close();
6516
8601
  await opened.close(context4);
6517
8602
  await mcp?.close();
@@ -6522,13 +8607,16 @@ async function openHost(options) {
6522
8607
  options.modelAuth?.close();
6523
8608
  await Promise.all([...environments.values()].map((env) => env.cleanup(context4)));
6524
8609
  } finally {
6525
- await release();
8610
+ await release2();
6526
8611
  }
6527
8612
  })();
6528
8613
  return closing;
6529
8614
  }
6530
8615
  };
6531
8616
  } catch (error) {
8617
+ upgrader?.close();
8618
+ if (processes !== undefined)
8619
+ await processes.close(context4).catch(() => {});
6532
8620
  options.modelAuth?.close();
6533
8621
  attachments?.close();
6534
8622
  agentRoles?.close();
@@ -6536,18 +8624,18 @@ async function openHost(options) {
6536
8624
  await each.close?.().catch(() => {});
6537
8625
  await mcp?.close().catch(() => {});
6538
8626
  await harness?.close(context4).catch(() => {});
6539
- await release().catch(() => {});
8627
+ await release2().catch(() => {});
6540
8628
  throw error;
6541
8629
  }
6542
8630
  }
6543
8631
 
6544
8632
  // apps/host/src/service/log-rotation.ts
6545
8633
  import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
6546
- import { join as join11 } from "node:path";
8634
+ import { join as join12 } from "node:path";
6547
8635
  var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
6548
8636
  var HOST_LOG_HISTORY = 3;
6549
8637
  var HOST_LOG_CHECK_INTERVAL_MS = 10 * 60 * 1000;
6550
- var hostLogPath = (dataDir) => join11(dataDir, "logs", "host.log");
8638
+ var hostLogPath = (dataDir) => join12(dataDir, "logs", "host.log");
6551
8639
  var rotationTempPath = (log) => `${log}.rotating`;
6552
8640
  var isMissing = (error) => error.code === "ENOENT";
6553
8641
  async function rotateHostLog(dataDir, copy = copyFile) {
@@ -6590,8 +8678,8 @@ function startHostLogRotation(dataDir) {
6590
8678
  return;
6591
8679
  running = true;
6592
8680
  rotateHostLog(dataDir).catch((error) => {
6593
- const message = error instanceof Error ? error.message : String(error);
6594
- process.stderr.write(`${JSON.stringify({ event: "log-rotation-failed", error: message })}
8681
+ const message3 = error instanceof Error ? error.message : String(error);
8682
+ process.stderr.write(`${JSON.stringify({ event: "log-rotation-failed", error: message3 })}
6595
8683
  `);
6596
8684
  }).finally(() => {
6597
8685
  running = false;
@@ -6604,10 +8692,10 @@ function startHostLogRotation(dataDir) {
6604
8692
  }
6605
8693
 
6606
8694
  // apps/host/src/model-auth.ts
6607
- import { randomUUID as randomUUID3 } from "node:crypto";
8695
+ import { randomUUID as randomUUID5 } from "node:crypto";
6608
8696
  import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
6609
8697
  import { dirname as dirname3 } from "node:path";
6610
- import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
8698
+ import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
6611
8699
 
6612
8700
  // apps/host/src/pi-setup.ts
6613
8701
  import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
@@ -6722,11 +8810,22 @@ function modelSummaries(modelRuntime) {
6722
8810
  }
6723
8811
 
6724
8812
  // apps/host/src/model-auth.ts
6725
- var BUILTIN = new Set(getBuiltinProviders());
8813
+ var BUILTIN_PROVIDERS = getBuiltinProviders();
8814
+ var BUILTIN = new Set(BUILTIN_PROVIDERS);
6726
8815
  var CUSTOM_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
6727
8816
  var LOGIN_TIMEOUT_MS = 15 * 60000;
6728
8817
  var FINISHED_LOGIN_TTL_MS = 60000;
6729
8818
  var ENDPOINT_TIMEOUT_MS = 1e4;
8819
+ var CATALOG_THINKING_LEVELS = new Map;
8820
+ for (const provider of BUILTIN_PROVIDERS) {
8821
+ for (const model of getBuiltinModels(provider)) {
8822
+ if (!model.reasoning || model.thinkingLevelMap === undefined)
8823
+ continue;
8824
+ const maps = CATALOG_THINKING_LEVELS.get(model.id) ?? [];
8825
+ maps.push(model.thinkingLevelMap);
8826
+ CATALOG_THINKING_LEVELS.set(model.id, maps);
8827
+ }
8828
+ }
6730
8829
  var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && a.images === b.images && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
6731
8830
  var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
6732
8831
  async function readModelsFile(path) {
@@ -6887,6 +8986,45 @@ async function openModelAuth(runtime, options) {
6887
8986
  tail = next.catch(() => {});
6888
8987
  return next;
6889
8988
  };
8989
+ const opened = await readModelsFile(options.modelsPath);
8990
+ Promise.all([...custom].flatMap(([id, endpoint]) => {
8991
+ const entries = opened.providers[id]?.models;
8992
+ if (!Array.isArray(entries) || !entries.some(lacksLimits))
8993
+ return [];
8994
+ return [
8995
+ (async () => {
8996
+ const key = (await runtime.getAuth(id))?.auth.apiKey;
8997
+ const listed = await fetchEndpointModels(endpoint.api, endpoint.baseUrl, key).catch(() => []);
8998
+ return { id, endpoint, listed: new Map(listed.map((model) => [model.id, model])) };
8999
+ })()
9000
+ ];
9001
+ })).then((answers) => serial(async () => {
9002
+ if (closed)
9003
+ return;
9004
+ const filled = new Set;
9005
+ const fill = (target) => {
9006
+ for (const { id, endpoint, listed } of answers) {
9007
+ const entry = target.providers[id];
9008
+ if (entry?.api !== endpoint.api || entry.baseUrl !== endpoint.baseUrl || !Array.isArray(entry.models))
9009
+ continue;
9010
+ for (const model of entry.models) {
9011
+ const limits = listed.get(model.id);
9012
+ for (const field of LIMITS) {
9013
+ if (field in model || limits?.[field] === undefined)
9014
+ continue;
9015
+ model[field] = limits[field];
9016
+ filled.add(id);
9017
+ }
9018
+ }
9019
+ }
9020
+ };
9021
+ fill(await readModelsFile(options.modelsPath));
9022
+ if (filled.size === 0)
9023
+ return;
9024
+ await updateModelsFile(fill);
9025
+ await runtime.refresh({ providers: [...filled], allowNetwork: false });
9026
+ await republish();
9027
+ }));
6890
9028
  return {
6891
9029
  models,
6892
9030
  auth,
@@ -6908,7 +9046,7 @@ async function openModelAuth(runtime, options) {
6908
9046
  if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
6909
9047
  throw new Error(`A login for ${provider.name} is already in progress`);
6910
9048
  }
6911
- const id = randomUUID3();
9049
+ const id = randomUUID5();
6912
9050
  const login = {
6913
9051
  flow: { id, providerId, method, events: [], state: "running" },
6914
9052
  controller: new AbortController,
@@ -7073,32 +9211,98 @@ async function fetchEndpointModels(api, baseUrl, key) {
7073
9211
  if (!response.ok)
7074
9212
  throw new Error(`${url.host} answered ${response.status} for /models`);
7075
9213
  const body = await response.json();
7076
- const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) => typeof model.name === "string" ? [
7077
- endpointModel(model.name.replace(/^models\//, ""), {
7078
- contextWindow: positive(model.inputTokenLimit),
7079
- maxTokens: positive(model.outputTokenLimit),
7080
- reasoning: model.thinking === true,
7081
- image: false
7082
- })
7083
- ] : []) : (body.data ?? []).flatMap((model) => typeof model.id === "string" ? [
7084
- endpointModel(model.id, {
7085
- contextWindow: positive(model.context_window) ?? positive(model.context_length) ?? positive(model.max_input_tokens),
7086
- maxTokens: positive(model.max_output_tokens),
7087
- reasoning: model.reasoning === true,
7088
- image: Array.isArray(model.modalities?.input) && model.modalities.input.includes("image")
7089
- })
7090
- ] : []);
9214
+ const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) => {
9215
+ if (typeof model.name !== "string")
9216
+ return [];
9217
+ const id = model.name.replace(/^models\//, "");
9218
+ const reasoning = model.thinking === true;
9219
+ return [
9220
+ endpointModel(id, {
9221
+ contextWindow: positive(model.inputTokenLimit),
9222
+ maxTokens: positive(model.outputTokenLimit),
9223
+ reasoning,
9224
+ image: false,
9225
+ thinkingLevelMap: reasoning ? thinkingLevelMapFrom(reportedThinkingLevels(model)) ?? catalogThinkingLevelMap(id) : undefined
9226
+ })
9227
+ ];
9228
+ }) : (body.data ?? []).flatMap((model) => {
9229
+ if (typeof model.id !== "string")
9230
+ return [];
9231
+ const id = model.id;
9232
+ const reasoning = model.reasoning === true;
9233
+ return [
9234
+ endpointModel(id, {
9235
+ contextWindow: positive(model.context_window) ?? positive(model.context_length) ?? positive(model.max_input_tokens),
9236
+ maxTokens: positive(model.max_output_tokens),
9237
+ reasoning,
9238
+ image: Array.isArray(model.modalities?.input) && model.modalities.input.includes("image"),
9239
+ thinkingLevelMap: reasoning ? thinkingLevelMapFrom(reportedThinkingLevels(model)) ?? catalogThinkingLevelMap(id) : undefined
9240
+ })
9241
+ ];
9242
+ });
7091
9243
  if (models.length === 0)
7092
9244
  throw new Error(`${url.host} listed no models`);
7093
9245
  return models;
7094
9246
  }
9247
+ var LIMITS = ["contextWindow", "maxTokens"];
9248
+ var lacksLimits = (model) => typeof model === "object" && model !== null && LIMITS.some((field) => !(field in model));
7095
9249
  var positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
9250
+ var THINKING_LEVELS3 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
9251
+ function reportedThinkingLevels(model) {
9252
+ const levels = new Map;
9253
+ const add = (value) => {
9254
+ if (typeof value !== "string")
9255
+ return;
9256
+ const level = value.trim();
9257
+ const piLevel = level.toLowerCase() === "none" ? "off" : level.toLowerCase();
9258
+ if (!THINKING_LEVELS3.includes(piLevel) || levels.has(piLevel))
9259
+ return;
9260
+ levels.set(piLevel, level);
9261
+ };
9262
+ if (Array.isArray(model.reasoning_levels)) {
9263
+ for (const value of model.reasoning_levels)
9264
+ add(value);
9265
+ }
9266
+ if (Array.isArray(model.supported_reasoning_levels)) {
9267
+ for (const value of model.supported_reasoning_levels) {
9268
+ if (typeof value === "string")
9269
+ add(value);
9270
+ else if (typeof value === "object" && value !== null && "effort" in value)
9271
+ add(value.effort);
9272
+ }
9273
+ }
9274
+ return levels;
9275
+ }
9276
+ function thinkingLevelMapFrom(levels) {
9277
+ if (levels.size === 0)
9278
+ return;
9279
+ return {
9280
+ off: levels.get("off") ?? null,
9281
+ minimal: levels.get("minimal") ?? null,
9282
+ low: levels.get("low") ?? null,
9283
+ medium: levels.get("medium") ?? null,
9284
+ high: levels.get("high") ?? null,
9285
+ xhigh: levels.get("xhigh") ?? null,
9286
+ max: levels.get("max") ?? null
9287
+ };
9288
+ }
9289
+ function catalogThinkingLevelMap(id) {
9290
+ let matches = CATALOG_THINKING_LEVELS.get(id);
9291
+ if (matches === undefined) {
9292
+ const slash = id.indexOf("/");
9293
+ if (slash === -1)
9294
+ return;
9295
+ matches = CATALOG_THINKING_LEVELS.get(id.slice(slash + 1));
9296
+ }
9297
+ return matches?.length === 1 ? matches[0] : undefined;
9298
+ }
7096
9299
  function endpointModel(id, reported) {
7097
9300
  return {
7098
9301
  id,
7099
9302
  ...reported.contextWindow === undefined ? {} : { contextWindow: reported.contextWindow },
7100
9303
  ...reported.maxTokens === undefined ? {} : { maxTokens: reported.maxTokens },
7101
9304
  ...reported.reasoning ? { reasoning: true } : {},
9305
+ ...reported.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: reported.thinkingLevelMap },
7102
9306
  ...reported.image ? { input: ["text", "image"] } : {}
7103
9307
  };
7104
9308
  }
@@ -7133,29 +9337,40 @@ async function main(argv) {
7133
9337
  }
7134
9338
  if (relayOrigin !== undefined)
7135
9339
  validateRelayOrigin(relayOrigin);
7136
- const agentsHome = join12(homedir3(), ".agents");
7137
- const extensionsFor = (models, summaries, options2) => (host2) => [
7138
- createContext({
7139
- agentsHome,
7140
- checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
7141
- }),
7142
- createCoding(options2),
7143
- todo,
7144
- question,
7145
- createSubagent({
7146
- models,
7147
- modelSummaries: summaries,
7148
- exclude: [question.extension],
7149
- roles: () => host2.agentRoles.value.roles
7150
- }),
7151
- createCodemode()
7152
- ];
9340
+ const agentsHome = join13(homedir3(), ".agents");
9341
+ const extensionsFor = (models, summaries, options2) => (host2) => {
9342
+ const processes = createProcesses({
9343
+ ...host2.processes === undefined ? {} : { runtime: host2.processes }
9344
+ });
9345
+ return [
9346
+ createContext({
9347
+ agentsHome,
9348
+ checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
9349
+ }),
9350
+ createCoding(options2),
9351
+ todo,
9352
+ question,
9353
+ processes,
9354
+ createSubagent({
9355
+ models,
9356
+ modelSummaries: summaries,
9357
+ exclude: [question.extension, processes.extension],
9358
+ roles: () => host2.agentRoles.value.roles,
9359
+ worktree: {
9360
+ base: (dispatcher, context5) => subagentWorktreeBase(host2.harness(), dataDir, dispatcher, context5),
9361
+ path: (child) => worktreePath(dataDir, child)
9362
+ }
9363
+ }),
9364
+ createCodemode(),
9365
+ createContextBreakdown({ harness: host2.harness })
9366
+ ];
9367
+ };
7153
9368
  const common = {
7154
9369
  dataDir,
7155
9370
  agentsHome,
7156
9371
  projects: values.project ?? [],
7157
9372
  port: Number(values.port),
7158
- mcpConfig: join12(agentsHome, "mcp.json"),
9373
+ mcpConfig: join13(agentsHome, "mcp.json"),
7159
9374
  browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
7160
9375
  webRoot,
7161
9376
  ...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
@@ -7198,10 +9413,10 @@ async function main(argv) {
7198
9413
  };
7199
9414
  }
7200
9415
  async function piOptions() {
7201
- const modelsPath = join12(dataDir, "models.json");
7202
- const modelRuntime = await ModelRuntime.create({ authPath: join12(dataDir, "auth.json"), modelsPath });
9416
+ const modelsPath = join13(dataDir, "models.json");
9417
+ const modelRuntime = await ModelRuntime.create({ authPath: join13(dataDir, "auth.json"), modelsPath });
7203
9418
  const modelAuth = await openModelAuth(modelRuntime, { modelsPath, installId: await installId(dataDir) });
7204
- const settingsManager = SettingsManager.inMemory(await piSettings(join12(dataDir, "settings.json")));
9419
+ const settingsManager = SettingsManager.inMemory(await piSettings(join13(dataDir, "settings.json")));
7205
9420
  configureHarnessHttp(settingsManager);
7206
9421
  const summaries = () => modelAuth.models.value;
7207
9422
  const images = await createNodePhotonImages();
@@ -7226,7 +9441,11 @@ async function main(argv) {
7226
9441
  };
7227
9442
  }
7228
9443
  const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
7229
- const host = await openHost(options);
9444
+ let stop = () => {};
9445
+ const host = await openHost({
9446
+ ...options,
9447
+ ...packagedVersion === undefined ? {} : { upgrade: { current: packagedVersion, restart: () => stop(UPGRADE_EXIT_CODE) } }
9448
+ });
7230
9449
  const stopLogRotation = startHostLogRotation(dataDir);
7231
9450
  const { defaultModel: effectiveModel } = host.settings.value;
7232
9451
  const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
@@ -7234,7 +9453,7 @@ async function main(argv) {
7234
9453
  console.log(JSON.stringify({
7235
9454
  event: "ready",
7236
9455
  url: host.url,
7237
- tokenFile: join12(dataDir, "token"),
9456
+ tokenFile: join13(dataDir, "token"),
7238
9457
  dataDir,
7239
9458
  model,
7240
9459
  web,
@@ -7242,17 +9461,21 @@ async function main(argv) {
7242
9461
  ...host.relay === undefined ? {} : { relay: { origin: host.relay.origin, hostId: host.relay.hostId } }
7243
9462
  }));
7244
9463
  let stopping = false;
7245
- const stop = () => {
9464
+ stop = (code) => {
7246
9465
  if (stopping)
7247
9466
  return;
7248
9467
  stopping = true;
7249
9468
  stopLogRotation();
7250
- host.close().finally(() => process.exit(0));
9469
+ if (code === UPGRADE_EXIT_CODE) {
9470
+ console.log(JSON.stringify({ event: "upgraded", exitCode: code }));
9471
+ setTimeout(() => process.exit(code), 15000).unref();
9472
+ }
9473
+ host.close().finally(() => process.exit(code));
7251
9474
  };
7252
- process.on("SIGINT", stop);
7253
- process.on("SIGTERM", stop);
9475
+ process.on("SIGINT", () => stop(0));
9476
+ process.on("SIGTERM", () => stop(0));
7254
9477
  host.closed.then((end) => {
7255
- if (end.reason !== "failed" || stopping)
9478
+ if (end.kind !== "failed" || stopping)
7256
9479
  return;
7257
9480
  stopping = true;
7258
9481
  stopLogRotation();
@@ -7280,10 +9503,10 @@ async function piSettings(path) {
7280
9503
  return typeof block === "object" && block !== null && !Array.isArray(block) ? block : {};
7281
9504
  }
7282
9505
  async function installId(dataDir) {
7283
- const path = join12(dataDir, "install-id");
9506
+ const path = join13(dataDir, "install-id");
7284
9507
  let id = (await readFile7(path, "utf8").catch(() => "")).trim();
7285
9508
  if (id === "") {
7286
- id = randomUUID4();
9509
+ id = randomUUID6();
7287
9510
  await mkdir6(dataDir, { recursive: true });
7288
9511
  await writeFile5(path, `${id}
7289
9512
  `, { mode: 384 });