pinomad 0.13.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 (41) hide show
  1. package/dist/chunk-0dh2k3er.js +43 -0
  2. package/dist/{chunk-kmzzjv53.js → chunk-1mjbt2z2.js} +143 -10
  3. package/dist/{chunk-s0h7c7da.js → chunk-1rdbnvy6.js} +1 -1
  4. package/dist/{chunk-53xv3fny.js → chunk-35szq73k.js} +2446 -416
  5. package/dist/{chunk-aapsc9g0.js → chunk-5j7ybhjj.js} +1952 -295
  6. package/dist/{chunk-m51mq8vh.js → chunk-5pv0g1nj.js} +12 -10
  7. package/dist/{chunk-7mn38n4f.js → chunk-63ymn5c9.js} +2 -2
  8. package/dist/{chunk-563fnad9.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-d9mq5d46.js → chunk-b0nwq1dt.js} +10 -8
  12. package/dist/chunk-dsr19q5h.js +23 -0
  13. package/dist/{chunk-6th9xv39.js → chunk-e53c0s8z.js} +9 -7
  14. package/dist/{chunk-4fferbxw.js → chunk-e59k3bmz.js} +213 -30
  15. package/dist/chunk-eerpgvy9.js +315 -0
  16. package/dist/chunk-eewxy4cp.js +240 -0
  17. package/dist/{chunk-maqv34cs.js → chunk-evbv1fkn.js} +3 -3
  18. package/dist/{chunk-ra86wjp5.js → chunk-ex5yr09h.js} +11 -6
  19. package/dist/{chunk-t91zt5f0.js → chunk-hg9gs396.js} +1 -1
  20. package/dist/{chunk-90g1cg9n.js → chunk-mtkvft88.js} +1 -1
  21. package/dist/{chunk-wd7aracm.js → chunk-mxwcnnkg.js} +1 -303
  22. package/dist/{chunk-d4tq62yr.js → chunk-w927q9m1.js} +12 -11
  23. package/dist/pinomad.js +12 -12
  24. package/package.json +5 -1
  25. package/web/assets/{core-BHm2gdHL.js → core-BWHGsFpV.js} +1 -1
  26. package/web/assets/{highlighted-diff-DrHvSLK9.js → highlighted-diff-DJ7KZCpS.js} +2 -2
  27. package/web/assets/{highlights-1aLwssz-.js → highlights-CyEmo6OI.js} +1 -1
  28. package/web/assets/{highlights-themes-C1p4pFYS.js → highlights-themes-CSNkBojy.js} +1 -1
  29. package/web/assets/index-C_qkbyL7.css +1 -0
  30. package/web/assets/index-DefQtCI2.js +396 -0
  31. package/web/assets/{shiki-BwyCm3kZ.js → shiki-BzCmRG-v.js} +2 -2
  32. package/web/assets/{shiki-langs-CMlALLSg.js → shiki-langs-DOkjyV4d.js} +1 -1
  33. package/web/assets/{stream-tokenizer-BlgIhw0y.js → stream-tokenizer-CBPqFe4Z.js} +1 -1
  34. package/web/assets/{theming-themes-DDvoqEqT.js → theming-themes-wKVca0aB.js} +1 -1
  35. package/web/assets/{xterm-view-BnKOnF3C.js → xterm-view-CKa1-QCM.js} +12 -12
  36. package/web/index.html +3 -2
  37. package/web/theme-boot.js +11 -0
  38. package/dist/chunk-sd640dt7.js +0 -22
  39. package/dist/chunk-t6fw0wa9.js +0 -96
  40. package/web/assets/index-A_AUJoO6.css +0 -1
  41. package/web/assets/index-BafYMKiN.js +0 -396
@@ -5,7 +5,7 @@ import {
5
5
  openHostSettings,
6
6
  sameList,
7
7
  settingsDefaults
8
- } from "./chunk-563fnad9.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,9 +30,21 @@ 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,
@@ -40,9 +52,12 @@ import {
40
52
  MAX_PENDING_ATTACHMENT_BYTES,
41
53
  MAX_TRANSCRIPT_PAGE,
42
54
  PROTOCOL,
55
+ QuestionItemSchema,
56
+ QuestionSchema,
43
57
  RTC_DATA_CHANNEL_LABEL,
44
58
  RTC_DEFAULT_STUN,
45
59
  RTC_MAX_RECORD_BYTES,
60
+ TodoItemSchema,
46
61
  UNAUTHORIZED_CLOSE_CODE,
47
62
  attachmentBeginProblem,
48
63
  attachmentBlock,
@@ -50,6 +65,7 @@ import {
50
65
  base64DecodedLength,
51
66
  isAttachmentId,
52
67
  isCustomProviderApi,
68
+ isProcessActive,
53
69
  organize,
54
70
  parseAttachmentBlock,
55
71
  parseClientFrame,
@@ -57,7 +73,7 @@ import {
57
73
  rtcCandidateType,
58
74
  sdpCandidates,
59
75
  withRtcCandidates
60
- } from "./chunk-kmzzjv53.js";
76
+ } from "./chunk-1mjbt2z2.js";
61
77
  import {
62
78
  ENDPOINT_SET_SCHEMA_VERSION,
63
79
  MAX_HOST_ENDPOINT_URL_LENGTH,
@@ -73,29 +89,348 @@ import {
73
89
  parseEndpointSet,
74
90
  respondIK,
75
91
  toBase64Url
76
- } from "./chunk-pqe7q1s7.js";
92
+ } from "./chunk-6vbp1vva.js";
77
93
  import {
78
94
  DEFAULT_DATA_DIR
79
95
  } from "./chunk-ngf1zatm.js";
80
96
  import {
81
97
  packagedVersion,
82
98
  webRoot
83
- } from "./chunk-s0h7c7da.js";
99
+ } from "./chunk-1rdbnvy6.js";
84
100
  import {
85
101
  __require
86
102
  } from "./chunk-df32avw6.js";
87
103
 
88
104
  // apps/host/src/main.ts
89
- import { randomUUID as randomUUID4 } from "node:crypto";
105
+ import { randomUUID as randomUUID6 } from "node:crypto";
90
106
  import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
91
107
  import { homedir as homedir3 } from "node:os";
92
- import { join as join12, resolve as resolve7 } from "node:path";
108
+ import { join as join13, resolve as resolve7 } from "node:path";
93
109
  import { parseArgs } from "node:util";
94
110
  import { createModels } from "@earendil-works/pi-ai/models";
95
111
  import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
96
112
  import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
97
113
  import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
98
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
+
99
434
  // apps/host/src/extensions/question.ts
100
435
  import {
101
436
  defineDoc,
@@ -103,55 +438,7 @@ import {
103
438
  defineTask,
104
439
  defineTool
105
440
  } from "@earendil-works/pi-durable";
106
- import { Type as Type2 } from "typebox";
107
-
108
- // packages/protocol/src/presentation.ts
109
441
  import { Type } from "typebox";
110
- import { Value } from "typebox/value";
111
- var TodoItemSchema = Type.Object({
112
- text: Type.String({ minLength: 1 }),
113
- status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")])
114
- });
115
- var TodoSchema = Type.Object({ items: Type.Array(TodoItemSchema) });
116
- var QuestionOptionSchema = Type.Object({
117
- label: Type.String({ minLength: 1 }),
118
- description: Type.Optional(Type.String())
119
- });
120
- var QuestionItemSchema = Type.Object({
121
- question: Type.String({ minLength: 1 }),
122
- header: Type.String({ minLength: 1, maxLength: 16 }),
123
- options: Type.Array(QuestionOptionSchema, { minItems: 2, maxItems: 4 }),
124
- multiSelect: Type.Optional(Type.Boolean())
125
- });
126
- var QuestionAnswerSchema = Type.Object({
127
- selected: Type.Array(Type.String()),
128
- other: Type.Optional(Type.String())
129
- });
130
- var QuestionResolutionSchema = Type.Union([
131
- Type.Object({ outcome: Type.Literal("answered"), answers: Type.Array(QuestionAnswerSchema), at: Type.Number() }),
132
- Type.Object({ outcome: Type.Literal("dismissed"), at: Type.Number() }),
133
- Type.Object({ outcome: Type.Literal("cancelled"), at: Type.Number() })
134
- ]);
135
- var QuestionRequestSchema = Type.Object({
136
- id: Type.String({ minLength: 1 }),
137
- questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 }),
138
- askedAt: Type.Number(),
139
- resolution: Type.Optional(QuestionResolutionSchema)
140
- });
141
- var QuestionSchema = Type.Object({ requests: Type.Array(QuestionRequestSchema) });
142
- var DiffSchema = Type.Object({ patch: Type.String() });
143
- var CodemodeCallSchema = Type.Object({
144
- id: Type.Optional(Type.Number()),
145
- name: Type.String(),
146
- args: Type.String(),
147
- status: Type.Union([Type.Literal("running"), Type.Literal("ok"), Type.Literal("error"), Type.Literal("cancelled")]),
148
- durationMs: Type.Optional(Type.Number()),
149
- error: Type.Optional(Type.String()),
150
- details: Type.Optional(Type.Unknown())
151
- });
152
- var CodemodeSchema = Type.Object({ code: Type.Optional(Type.String()), calls: Type.Array(CodemodeCallSchema) });
153
-
154
- // apps/host/src/extensions/question.ts
155
442
  var QuestionDoc = defineDoc({
156
443
  kind: "question.requests",
157
444
  version: 1,
@@ -227,8 +514,8 @@ var askUserQuestion = defineTool({
227
514
  name: "ask_user_question",
228
515
  callers: ["model"],
229
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.",
230
- parameters: Type2.Object({
231
- questions: Type2.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
517
+ parameters: Type.Object({
518
+ questions: Type.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
232
519
  }),
233
520
  replay: "safe",
234
521
  execute: async (args, api, context) => {
@@ -297,31 +584,31 @@ function createCodemode() {
297
584
  }
298
585
 
299
586
  // apps/host/src/extensions/context.ts
300
- import { dirname as dirname2, sep as sep4 } from "node:path";
587
+ import { dirname as dirname2, sep as sep5 } from "node:path";
301
588
  import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
302
589
  import { formatSkillsForPrompt, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
303
590
 
304
591
  // apps/host/src/skills.ts
305
- import { execFile as execFile2 } from "node:child_process";
592
+ import { execFile as execFile3 } from "node:child_process";
306
593
  import { existsSync } from "node:fs";
307
594
  import { realpath as realpath4, stat as stat4 } from "node:fs/promises";
308
- import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
309
- 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";
310
597
  import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
311
598
  import { loadSkills } from "@earendil-works/pi-coding-agent";
312
599
 
313
600
  // apps/host/src/checkout.ts
314
- import { execFile, spawn } from "node:child_process";
601
+ import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
315
602
  import { randomBytes } from "node:crypto";
316
603
  import { realpath, rm, stat } from "node:fs/promises";
317
604
  import { tmpdir } from "node:os";
318
605
  import { join, relative, sep } from "node:path";
319
- import { promisify } from "node:util";
320
- var run = promisify(execFile);
606
+ import { promisify as promisify2 } from "node:util";
607
+ var run = promisify2(execFile2);
321
608
  var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
322
609
  function gitCapped(dir, args, maxBytes, env) {
323
610
  return new Promise((resolve, reject) => {
324
- 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 } });
325
612
  const chunks = [];
326
613
  let size = 0;
327
614
  let killed = false;
@@ -446,14 +733,14 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
446
733
  }
447
734
 
448
735
  // apps/host/src/files.ts
449
- import { spawn as spawn2 } from "node:child_process";
736
+ import { spawn as spawn3 } from "node:child_process";
450
737
  import { opendir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
451
- 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";
452
739
  import { AgentDoc as AgentDoc2 } from "@earendil-works/pi-durable";
453
740
 
454
741
  // apps/host/src/organization.ts
455
742
  import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
456
- import { basename, join as join2, resolve } from "node:path";
743
+ import { basename, join as join2, resolve, sep as sep2 } from "node:path";
457
744
  import { clampThinkingLevel } from "@earendil-works/pi-ai";
458
745
  import {
459
746
  AgentDoc,
@@ -759,6 +1046,17 @@ async function familyRoot(harness, id, context) {
759
1046
  }
760
1047
  return current;
761
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
+ }
762
1060
  async function cleanupArchivedWorktrees(harness, rootId, context) {
763
1061
  const index = await harness.snapshot(IndexDoc, context);
764
1062
  const checkouts = index?.checkouts ?? [];
@@ -830,7 +1128,7 @@ function createCandidateLister(enumerate, options = {}) {
830
1128
  }
831
1129
  function gitLines(cwd, args, limits) {
832
1130
  return new Promise((resolvePromise, rejectPromise) => {
833
- const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
1131
+ const child = spawn3("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
834
1132
  const chunks = [];
835
1133
  let bytes = 0;
836
1134
  let truncated = false;
@@ -878,7 +1176,7 @@ function gitLines(cwd, args, limits) {
878
1176
  }
879
1177
  async function inWorkTree(root) {
880
1178
  return await new Promise((resolvePromise) => {
881
- 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"] });
882
1180
  let out = "";
883
1181
  child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
884
1182
  child.on("error", () => resolvePromise(false));
@@ -887,7 +1185,7 @@ async function inWorkTree(root) {
887
1185
  }
888
1186
  async function hasHead(root) {
889
1187
  return await new Promise((resolvePromise) => {
890
- 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"] });
891
1189
  child.on("error", () => resolvePromise(false));
892
1190
  child.on("close", (code) => resolvePromise(code === 0));
893
1191
  });
@@ -1008,7 +1306,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
1008
1306
  const cwd = agent.cwd;
1009
1307
  if (cwd === undefined)
1010
1308
  throw new Error(`Conversation ${scope.conversationId} has no checkout`);
1011
- if (cwd.startsWith(join3(dataDir, "chats") + sep2))
1309
+ if (cwd.startsWith(join3(dataDir, "chats") + sep3))
1012
1310
  return { files: [], truncated: false };
1013
1311
  const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
1014
1312
  const ready = record === undefined || await worktreeSettled(record);
@@ -1040,7 +1338,7 @@ async function listFiles(harness, dataDir, enumerate, scope, query, context) {
1040
1338
  }
1041
1339
 
1042
1340
  // apps/host/src/skills.ts
1043
- var exec = promisify2(execFile2);
1341
+ var exec2 = promisify3(execFile3);
1044
1342
  function invocableSkillName(name) {
1045
1343
  return name.length <= 64 && /^[a-z0-9-]+$/.test(name) && !name.startsWith("-") && !name.endsWith("-") && !name.includes("--");
1046
1344
  }
@@ -1104,10 +1402,10 @@ async function committedSkillFiles(gitRoot, layers) {
1104
1402
  if (!await inWorkTree(gitRoot) || !await hasHead(gitRoot))
1105
1403
  return null;
1106
1404
  const pathspecs = layers.map((dir) => {
1107
- const rel = relative2(gitRoot, dir).split(sep3).join("/");
1405
+ const rel = relative2(gitRoot, dir).split(sep4).join("/");
1108
1406
  return rel === "" ? ".agents/skills" : `${rel}/.agents/skills`;
1109
1407
  });
1110
- 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 });
1111
1409
  return new Set(stdout.split("\x00").filter((path) => path !== ""));
1112
1410
  }
1113
1411
  async function enumerateSkills(root, mode, agentsHome) {
@@ -1119,7 +1417,7 @@ async function enumerateSkills(root, mode, agentsHome) {
1119
1417
  if (mode === "head" && gitRoot !== undefined) {
1120
1418
  const committed = await committedSkillFiles(gitRoot, layers.dirs);
1121
1419
  if (committed !== null) {
1122
- 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("/")));
1123
1421
  }
1124
1422
  }
1125
1423
  }
@@ -1253,7 +1551,7 @@ function createContext(options) {
1253
1551
  const seen = new Set;
1254
1552
  files = [
1255
1553
  ...loadProjectContextFiles({ cwd: dirname2(checkout.repo), agentDir: agentsHome }),
1256
- ...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))
1257
1555
  ].filter((file) => {
1258
1556
  if (seen.has(file.path))
1259
1557
  return false;
@@ -1280,21 +1578,336 @@ ${file.content}`).join(`
1280
1578
  };
1281
1579
  }
1282
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
+
1283
1896
  // apps/host/src/extensions/subagent.ts
1897
+ import { join as join6 } from "node:path";
1284
1898
  import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
1285
1899
  import {
1286
1900
  AgentDoc as AgentDoc4,
1287
- AssistantEntry,
1288
1901
  configure as configure2,
1289
- defineExtension as defineExtension3,
1290
- defineTool as defineTool2,
1902
+ defineExtension as defineExtension5,
1903
+ defineTool as defineTool3,
1291
1904
  section as section2
1292
1905
  } from "@earendil-works/pi-durable";
1293
1906
  import { Type as Type3 } from "typebox";
1294
1907
 
1295
1908
  // apps/host/src/subagent-doc.ts
1296
- import { defineDoc as defineDoc4 } from "@earendil-works/pi-durable";
1297
- var SubagentDoc = defineDoc4({
1909
+ import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
1910
+ var SubagentDoc = defineDoc6({
1298
1911
  kind: "pinomad.subagent",
1299
1912
  version: 1,
1300
1913
  scope: "conversation",
@@ -1304,8 +1917,273 @@ var SubagentDoc = defineDoc4({
1304
1917
  checkpointWhen: () => false
1305
1918
  });
1306
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
+
1307
2181
  // apps/host/src/extensions/subagent.ts
1308
- 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
+ }
1309
2187
  var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
1310
2188
  function createSubagent(options) {
1311
2189
  const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
@@ -1317,15 +2195,22 @@ function createSubagent(options) {
1317
2195
  const defined = roles();
1318
2196
  throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
1319
2197
  };
1320
- const tool = defineTool2({
2198
+ const tool = defineTool3({
1321
2199
  name: "subagent",
1322
2200
  callers: ["model"],
1323
- 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()}.`,
1324
2202
  parameters: Type3.Object({
1325
2203
  task: Type3.String({
1326
2204
  minLength: 1,
1327
2205
  description: "Complete, self-contained instructions. The subagent sees none of this conversation."
1328
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." })),
1329
2214
  role: Type3.Optional(Type3.String({
1330
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."
1331
2216
  })),
@@ -1335,16 +2220,39 @@ function createSubagent(options) {
1335
2220
  }),
1336
2221
  replay: "safe",
1337
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
+ }
1338
2230
  const child = await api.commit(async (tx) => {
1339
- const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
1340
- if (existing !== undefined) {
1341
- 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);
1342
2237
  return {
1343
- 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 },
1344
2241
  ...doc2?.model === undefined ? {} : { model: doc2.model },
1345
2242
  ...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
1346
2243
  };
1347
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
+ }
1348
2256
  const role = args.role === undefined ? undefined : roleNamed(args.role);
1349
2257
  if (role?.model !== undefined && args.model !== undefined) {
1350
2258
  throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
@@ -1377,21 +2285,38 @@ function createSubagent(options) {
1377
2285
  thinkingLevel = clampThinkingLevel2(override, parent.thinkingLevel);
1378
2286
  }
1379
2287
  }
2288
+ await assertCapacity(tx, root);
1380
2289
  const change = {
1381
2290
  extensions: { remove: [extension, ...options.exclude] },
1382
- instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
2291
+ instructions: role === undefined || role.prompt === "" ? instructions(name2) : `${instructions(name2)}
1383
2292
 
1384
2293
  ${role.prompt}`,
1385
2294
  ...model === undefined ? {} : { model },
1386
2295
  ...thinkingLevel === undefined ? {} : { thinkingLevel }
1387
2296
  };
1388
- const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
1389
- 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 });
1390
2313
  const childAgent = await tx.doc(AgentDoc4, created.id);
1391
2314
  const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc4, api.conversationId)).model;
1392
2315
  const startedAt = Date.now();
1393
2316
  const doc = await tx.doc(SubagentDoc, created.id);
1394
2317
  doc.startedAt = startedAt;
2318
+ doc.name = name2;
2319
+ doc.lifecycle = background ? "background" : "foreground";
1395
2320
  if (ref !== undefined)
1396
2321
  doc.model = `${ref.provider}/${ref.modelId}`;
1397
2322
  if (role !== undefined)
@@ -1399,33 +2324,183 @@ ${role.prompt}`,
1399
2324
  const label = args.description?.trim();
1400
2325
  if (label !== undefined && label !== "")
1401
2326
  doc.label = label;
1402
- 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
+ };
1403
2349
  }, context);
1404
- 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);
1405
2371
  const handle = await api.conversation(child.id, context);
1406
2372
  if (handle === undefined)
1407
2373
  throw new Error(`Subagent conversation ${child.id} does not exist`);
1408
2374
  const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
1409
2375
  const settled = await (await handle.submit(request, context)).wait(context);
1410
- if (settled.status !== "done" || settled.type !== "input")
1411
- throw new Error(`Subagent failed: ${settled.status}`);
1412
- const answer = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
1413
- const message = answer?.model?.[0];
1414
- const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
1415
- const result2 = {
1416
- output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
1417
- details: {
1418
- conversationId: child.id,
1419
- ...child.model === undefined ? {} : { model: child.model },
1420
- ...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";
1421
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
1422
2396
  };
1423
- return result2;
1424
2397
  }
1425
2398
  });
1426
- const extension = defineExtension3({
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 }
2418
+ };
2419
+ }
2420
+ });
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({
1427
2501
  name: "subagent",
1428
- tools: [tool],
2502
+ tools: [tool, send, stop, read, list],
2503
+ tasks: [SubagentAnchor, SubagentReporter],
1429
2504
  sections: [
1430
2505
  section2("agent_roles", () => {
1431
2506
  const defined = roles();
@@ -1443,9 +2518,9 @@ ${role.prompt}`,
1443
2518
  }
1444
2519
 
1445
2520
  // apps/host/src/extensions/todo.ts
1446
- 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";
1447
2522
  import { Type as Type4 } from "typebox";
1448
- var TodoDoc = defineDoc5({
2523
+ var TodoDoc = defineDoc8({
1449
2524
  kind: "todo.list",
1450
2525
  version: 1,
1451
2526
  scope: "conversation",
@@ -1454,7 +2529,7 @@ var TodoDoc = defineDoc5({
1454
2529
  initial: () => ({ items: [] }),
1455
2530
  checkpointWhen: () => false
1456
2531
  });
1457
- var todoWrite = defineTool3({
2532
+ var todoWrite = defineTool4({
1458
2533
  name: "todo_write",
1459
2534
  description: "Replace the todo list shown to the user with the given items.",
1460
2535
  parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
@@ -1468,7 +2543,7 @@ var todoWrite = defineTool3({
1468
2543
  }
1469
2544
  });
1470
2545
  var todo = {
1471
- extension: defineExtension4({ name: "todo", tools: [todoWrite] }),
2546
+ extension: defineExtension6({ name: "todo", tools: [todoWrite] }),
1472
2547
  docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
1473
2548
  };
1474
2549
 
@@ -1476,8 +2551,8 @@ var todo = {
1476
2551
  import { stat as stat6 } from "node:fs/promises";
1477
2552
  import { createServer } from "node:http";
1478
2553
  import { homedir, networkInterfaces } from "node:os";
1479
- import { join as join7, sep as sep5 } from "node:path";
1480
- 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";
1481
2556
  import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
1482
2557
  import {
1483
2558
  AgentDoc as AgentDoc5,
@@ -1486,7 +2561,7 @@ import {
1486
2561
  LiveDoc
1487
2562
  } from "@earendil-works/pi-durable";
1488
2563
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
1489
- import { Value as Value2 } from "typebox/value";
2564
+ import { Value } from "typebox/value";
1490
2565
 
1491
2566
  // apps/host/src/secure-carrier.ts
1492
2567
  import { WebSocket } from "ws";
@@ -1522,7 +2597,7 @@ function webSocketCarrier(socket) {
1522
2597
  }
1523
2598
 
1524
2599
  // apps/host/src/rtc.ts
1525
- import { randomUUID } from "node:crypto";
2600
+ import { randomUUID as randomUUID2 } from "node:crypto";
1526
2601
  var BUFFER_HIGH_WATER = 256 * 1024;
1527
2602
  var BUFFER_LOW_WATER = 64 * 1024;
1528
2603
  var PUMP_STALL_MS = 1e4;
@@ -1781,7 +2856,7 @@ function nodeRtcFactory(loaded) {
1781
2856
  const max = parseRtcMaxMessageSize(input.offerSdp);
1782
2857
  if (max.kind === "invalid")
1783
2858
  throw new Error(max.reason);
1784
- const pc = new ndc.PeerConnection(`pinomad-${randomUUID()}`, { iceServers: [...input.stun] });
2859
+ const pc = new ndc.PeerConnection(`pinomad-${randomUUID2()}`, { iceServers: [...input.stun] });
1785
2860
  const timers = input.timers;
1786
2861
  const candidates = new Set;
1787
2862
  let localSdp;
@@ -2040,17 +3115,17 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
2040
3115
  stallTimer = timers.setTimeout(stall, 1000);
2041
3116
  };
2042
3117
  channel.onBufferedAmountLow(() => pump());
2043
- channel.onMessage((message) => {
2044
- if (typeof message === "string")
3118
+ channel.onMessage((message2) => {
3119
+ if (typeof message2 === "string")
2045
3120
  handlers?.text();
2046
- else if (message instanceof ArrayBuffer)
2047
- handlers?.binary(new Uint8Array(message));
3121
+ else if (message2 instanceof ArrayBuffer)
3122
+ handlers?.binary(new Uint8Array(message2));
2048
3123
  else
2049
- handlers?.binary(message);
3124
+ handlers?.binary(message2);
2050
3125
  });
2051
3126
  channel.onClosed(finish);
2052
- channel.onError((message) => {
2053
- log("rtc-datachannel-error", { message });
3127
+ channel.onError((message2) => {
3128
+ log("rtc-datachannel-error", { message: message2 });
2054
3129
  finish();
2055
3130
  });
2056
3131
  return {
@@ -2080,12 +3155,218 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
2080
3155
  }
2081
3156
 
2082
3157
  // apps/host/src/terminals.ts
2083
- import { randomUUID as randomUUID2 } from "node:crypto";
3158
+ import { randomUUID as randomUUID3 } from "node:crypto";
2084
3159
  import { userInfo } from "node:os";
2085
- var TERMINAL_LIMIT = 8;
3160
+
3161
+ // apps/host/src/output-buffer.ts
2086
3162
  var BUFFER_SIZE = 512 * 1024;
2087
3163
  var FLUSH_MS = 8;
2088
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;
2089
3370
  var MAX_TITLE = 1024;
2090
3371
  var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
2091
3372
 
@@ -2125,7 +3406,7 @@ class Terminals {
2125
3406
  const number = (this.#numbers.get(owner) ?? 0) + 1;
2126
3407
  this.#numbers.set(owner, number);
2127
3408
  const terminal = {
2128
- id: randomUUID2(),
3409
+ id: randomUUID3(),
2129
3410
  owner,
2130
3411
  cwd,
2131
3412
  number,
@@ -2136,12 +3417,16 @@ class Terminals {
2136
3417
  title: undefined,
2137
3418
  rename: undefined,
2138
3419
  exit: undefined,
2139
- buffer: "",
2140
- start: 0,
2141
- pending: "",
2142
- flushTimer: undefined,
2143
- titleCarry: "",
2144
- 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: ""
2145
3430
  };
2146
3431
  this.#terminals.set(terminal.id, terminal);
2147
3432
  try {
@@ -2171,16 +3456,12 @@ class Terminals {
2171
3456
  restart(id, size) {
2172
3457
  const terminal = this.#get(id);
2173
3458
  this.#stop(terminal);
2174
- terminal.start += terminal.buffer.length + terminal.pending.length + 1;
2175
- terminal.buffer = "";
2176
- terminal.pending = "";
3459
+ terminal.output.discard();
2177
3460
  terminal.titleCarry = "";
2178
3461
  terminal.title = undefined;
2179
3462
  terminal.exit = undefined;
2180
3463
  terminal.cols = size.cols;
2181
3464
  terminal.rows = size.rows;
2182
- for (const viewer of terminal.viewers)
2183
- viewer.position = undefined;
2184
3465
  try {
2185
3466
  this.#start(terminal);
2186
3467
  } catch (error) {
@@ -2209,15 +3490,7 @@ class Terminals {
2209
3490
  this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
2210
3491
  }
2211
3492
  attach(id, since, sink) {
2212
- const terminal = this.#get(id);
2213
- const end = terminal.start + terminal.buffer.length;
2214
- const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
2215
- const viewer = { sink, position, sending: false };
2216
- terminal.viewers.add(viewer);
2217
- this.#pump(terminal, viewer);
2218
- return () => {
2219
- terminal.viewers.delete(viewer);
2220
- };
3493
+ return this.#get(id).output.attach(since, sink);
2221
3494
  }
2222
3495
  #get(id) {
2223
3496
  const terminal = this.#terminals.get(id);
@@ -2239,25 +3512,19 @@ class Terminals {
2239
3512
  pty.onData((data) => {
2240
3513
  if (terminal.generation !== generation)
2241
3514
  return;
2242
- terminal.pending += data;
2243
- if (terminal.pending.length >= (this.#options.maxChunk ?? MAX_CHUNK))
2244
- this.#flush(terminal);
2245
- else
2246
- terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
3515
+ this.#options.onOutput?.();
3516
+ terminal.output.push(data);
2247
3517
  });
2248
3518
  pty.onExit(({ exitCode, signal }) => {
2249
3519
  if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
2250
3520
  return;
2251
- this.#flush(terminal);
3521
+ terminal.output.flush();
2252
3522
  terminal.pty = undefined;
2253
3523
  terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
2254
3524
  this.#changed(terminal.owner);
2255
3525
  });
2256
3526
  }
2257
3527
  #stop(terminal) {
2258
- if (terminal.flushTimer !== undefined)
2259
- clearTimeout(terminal.flushTimer);
2260
- terminal.flushTimer = undefined;
2261
3528
  terminal.generation++;
2262
3529
  const pty = terminal.pty;
2263
3530
  terminal.pty = undefined;
@@ -2267,10 +3534,8 @@ class Terminals {
2267
3534
  }
2268
3535
  #remove(terminal) {
2269
3536
  this.#stop(terminal);
3537
+ terminal.output.close("Terminal closed");
2270
3538
  this.#terminals.delete(terminal.id);
2271
- for (const viewer of terminal.viewers)
2272
- viewer.sink.ended("Terminal closed");
2273
- terminal.viewers.clear();
2274
3539
  }
2275
3540
  #resize(terminal, size) {
2276
3541
  if (terminal.cols === size.cols && terminal.rows === size.rows)
@@ -2282,30 +3547,6 @@ class Terminals {
2282
3547
  } catch {}
2283
3548
  this.#changed(terminal.owner);
2284
3549
  }
2285
- #flush(terminal) {
2286
- if (terminal.flushTimer !== undefined)
2287
- clearTimeout(terminal.flushTimer);
2288
- terminal.flushTimer = undefined;
2289
- const data = terminal.pending;
2290
- if (data === "")
2291
- return;
2292
- terminal.pending = "";
2293
- terminal.buffer += data;
2294
- const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
2295
- if (terminal.buffer.length > bufferSize) {
2296
- let cut = terminal.buffer.length - bufferSize;
2297
- const line = terminal.buffer.indexOf(`
2298
- `, cut);
2299
- if (line !== -1 && line - cut < 1024)
2300
- cut = line + 1;
2301
- terminal.buffer = terminal.buffer.slice(cut);
2302
- terminal.start += cut;
2303
- }
2304
- if (this.#readTitle(terminal, data))
2305
- this.#changed(terminal.owner);
2306
- for (const viewer of terminal.viewers)
2307
- this.#pump(terminal, viewer);
2308
- }
2309
3550
  #readTitle(terminal, data) {
2310
3551
  const text = terminal.titleCarry + data;
2311
3552
  let title;
@@ -2321,29 +3562,6 @@ class Terminals {
2321
3562
  terminal.title = title;
2322
3563
  return terminal.rename === undefined;
2323
3564
  }
2324
- async#pump(terminal, viewer) {
2325
- if (viewer.sending)
2326
- return;
2327
- viewer.sending = true;
2328
- try {
2329
- const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
2330
- while (terminal.viewers.has(viewer)) {
2331
- const end = terminal.start + terminal.buffer.length;
2332
- const reset = viewer.position === undefined || viewer.position < terminal.start;
2333
- const from = reset ? terminal.start : viewer.position;
2334
- if (!reset && from >= end)
2335
- return;
2336
- let to = reset ? end : Math.min(end, from + maxChunk);
2337
- if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
2338
- to--;
2339
- const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
2340
- viewer.position = to;
2341
- await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
2342
- }
2343
- } finally {
2344
- viewer.sending = false;
2345
- }
2346
- }
2347
3565
  #changed(owner) {
2348
3566
  const watchers = this.#watchers.get(owner);
2349
3567
  if (watchers === undefined)
@@ -2354,20 +3572,20 @@ class Terminals {
2354
3572
  }
2355
3573
  }
2356
3574
  async function startTerminals(options = {}) {
2357
- let spawn3 = options.spawn;
2358
- if (spawn3 === undefined) {
3575
+ let spawn4 = options.spawn;
3576
+ if (spawn4 === undefined) {
2359
3577
  try {
2360
3578
  const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
2361
3579
  if (typeof loaded.spawn !== "function")
2362
3580
  throw new Error("the module has no spawn()");
2363
- spawn3 = loaded.spawn;
3581
+ spawn4 = loaded.spawn;
2364
3582
  } catch (error) {
2365
3583
  const detail = error instanceof Error ? error.message : String(error);
2366
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})`);
2367
3585
  return;
2368
3586
  }
2369
3587
  }
2370
- 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) });
2371
3589
  }
2372
3590
  function loginShell() {
2373
3591
  let file = process.env.SHELL;
@@ -2381,12 +3599,12 @@ function loginShell() {
2381
3599
  return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
2382
3600
  }
2383
3601
  function shellEnv(env) {
2384
- const result2 = {};
3602
+ const result3 = {};
2385
3603
  for (const [key, value] of Object.entries(env)) {
2386
3604
  if (value !== undefined && !key.startsWith("PINOMAD_"))
2387
- result2[key] = value;
3605
+ result3[key] = value;
2388
3606
  }
2389
- return result2;
3607
+ return result3;
2390
3608
  }
2391
3609
  function info(terminal) {
2392
3610
  return {
@@ -2398,13 +3616,10 @@ function info(terminal) {
2398
3616
  ...terminal.exit === undefined ? {} : { exit: terminal.exit }
2399
3617
  };
2400
3618
  }
2401
- function isHighSurrogate(code) {
2402
- return code >= 55296 && code <= 56319;
2403
- }
2404
3619
 
2405
3620
  // apps/host/src/folders.ts
2406
3621
  import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
2407
- import { isAbsolute, join as join6 } from "node:path";
3622
+ import { isAbsolute, join as join7 } from "node:path";
2408
3623
  var denied = (error) => {
2409
3624
  const code = error?.code;
2410
3625
  return code === "EACCES" || code === "EPERM";
@@ -2434,10 +3649,10 @@ async function listFolders(path, home) {
2434
3649
  continue;
2435
3650
  let isDirectory = entry.isDirectory();
2436
3651
  if (!isDirectory && entry.isSymbolicLink()) {
2437
- 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);
2438
3653
  }
2439
3654
  if (isDirectory)
2440
- folders.push({ name: entry.name, path: join6(dir, entry.name) });
3655
+ folders.push({ name: entry.name, path: join7(dir, entry.name) });
2441
3656
  }
2442
3657
  folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
2443
3658
  return { path: dir, home: await realpath5(home).catch(() => home), folders };
@@ -2623,7 +3838,7 @@ function imageProblem(images) {
2623
3838
  // apps/host/src/image-refs.ts
2624
3839
  import { createHash } from "node:crypto";
2625
3840
  import { applyImmutable } from "@earendil-works/chord/delta";
2626
- var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3841
+ var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2627
3842
  var isImageBlock = (value) => value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
2628
3843
  var hashed = new WeakMap;
2629
3844
  function refOf(block) {
@@ -2715,7 +3930,7 @@ function rewriteEntries(value) {
2715
3930
  return out ?? value;
2716
3931
  }
2717
3932
  function rewriteEntry(value) {
2718
- if (!isObject(value) || value.model === undefined)
3933
+ if (!isObject2(value) || value.model === undefined)
2719
3934
  return value;
2720
3935
  const model = rewriteMessages(value.model);
2721
3936
  return model === value.model ? value : { ...value, model };
@@ -2732,7 +3947,7 @@ function rewriteMessages(value) {
2732
3947
  return out ?? value;
2733
3948
  }
2734
3949
  function rewriteMessage(value) {
2735
- if (!isObject(value) || value.content === undefined)
3950
+ if (!isObject2(value) || value.content === undefined)
2736
3951
  return value;
2737
3952
  const content = rewriteContent(value.content);
2738
3953
  return content === value.content ? value : { ...value, content };
@@ -2743,7 +3958,7 @@ function rewriteContent(value) {
2743
3958
  let out;
2744
3959
  for (let i = 0;i < value.length; i++) {
2745
3960
  const block = value[i];
2746
- const next = isObject(block) && isImageBlock(block) ? withRef(block) : block;
3961
+ const next = isObject2(block) && isImageBlock(block) ? withRef(block) : block;
2747
3962
  if (next !== block)
2748
3963
  (out ??= value.slice())[i] = next;
2749
3964
  }
@@ -2761,31 +3976,31 @@ function rewriteItems(value) {
2761
3976
  return out ?? value;
2762
3977
  }
2763
3978
  function rewriteItem(value) {
2764
- if (!isObject(value) || value.content === undefined)
3979
+ if (!isObject2(value) || value.content === undefined)
2765
3980
  return value;
2766
3981
  const content = rewriteContent(value.content);
2767
3982
  return content === value.content ? value : { ...value, content };
2768
3983
  }
2769
3984
  function rewriteInbox(value) {
2770
- if (!isObject(value) || value.items === undefined)
3985
+ if (!isObject2(value) || value.items === undefined)
2771
3986
  return value;
2772
3987
  const items = rewriteItems(value.items);
2773
3988
  return items === value.items ? value : { ...value, items };
2774
3989
  }
2775
3990
  function rewriteGeneration(value) {
2776
- if (!isObject(value) || value.message === undefined)
3991
+ if (!isObject2(value) || value.message === undefined)
2777
3992
  return value;
2778
- const message = rewriteMessage(value.message);
2779
- return message === value.message ? value : { ...value, message };
3993
+ const message2 = rewriteMessage(value.message);
3994
+ return message2 === value.message ? value : { ...value, message: message2 };
2780
3995
  }
2781
3996
  function rewriteLive(value) {
2782
- if (!isObject(value) || value.generation === undefined)
3997
+ if (!isObject2(value) || value.generation === undefined)
2783
3998
  return value;
2784
3999
  const generation = rewriteGeneration(value.generation);
2785
4000
  return generation === value.generation ? value : { ...value, generation };
2786
4001
  }
2787
4002
  function rewriteDocs(value) {
2788
- if (!isObject(value))
4003
+ if (!isObject2(value))
2789
4004
  return value;
2790
4005
  let out;
2791
4006
  for (const key of ["pi.inbox", "pi.live"]) {
@@ -2799,7 +4014,7 @@ function rewriteDocs(value) {
2799
4014
  return out ?? value;
2800
4015
  }
2801
4016
  function rewriteView(value) {
2802
- if (!isObject(value))
4017
+ if (!isObject2(value))
2803
4018
  return value;
2804
4019
  let out;
2805
4020
  if (value.entries !== undefined) {
@@ -2827,7 +4042,7 @@ function rewriteSlot(slot, value) {
2827
4042
  case "content":
2828
4043
  return rewriteContent(value);
2829
4044
  case "block":
2830
- return isObject(value) && isImageBlock(value) ? withRef(value) : value;
4045
+ return isObject2(value) && isImageBlock(value) ? withRef(value) : value;
2831
4046
  case "docs":
2832
4047
  return rewriteDocs(value);
2833
4048
  case "inbox":
@@ -2892,35 +4107,35 @@ function reachesIntoImage(op, root) {
2892
4107
  if (slotAt(path.slice(0, length)) !== "block")
2893
4108
  continue;
2894
4109
  const node = resolve4(root, path, length);
2895
- if (isObject(node) && isImageBlock(node))
4110
+ if (isObject2(node) && isImageBlock(node))
2896
4111
  return true;
2897
4112
  }
2898
4113
  return false;
2899
4114
  }
2900
4115
  function* contentArrays(view) {
2901
- if (!isObject(view))
4116
+ if (!isObject2(view))
2902
4117
  return;
2903
4118
  if (Array.isArray(view.entries)) {
2904
4119
  for (const entry of view.entries) {
2905
- if (!isObject(entry) || !Array.isArray(entry.model))
4120
+ if (!isObject2(entry) || !Array.isArray(entry.model))
2906
4121
  continue;
2907
- for (const message of entry.model) {
2908
- if (isObject(message) && Array.isArray(message.content))
2909
- yield message.content;
4122
+ for (const message2 of entry.model) {
4123
+ if (isObject2(message2) && Array.isArray(message2.content))
4124
+ yield message2.content;
2910
4125
  }
2911
4126
  }
2912
4127
  }
2913
- if (!isObject(view.docs))
4128
+ if (!isObject2(view.docs))
2914
4129
  return;
2915
4130
  const inbox = view.docs["pi.inbox"];
2916
- if (isObject(inbox) && Array.isArray(inbox.items)) {
4131
+ if (isObject2(inbox) && Array.isArray(inbox.items)) {
2917
4132
  for (const item of inbox.items) {
2918
- if (isObject(item) && Array.isArray(item.content))
4133
+ if (isObject2(item) && Array.isArray(item.content))
2919
4134
  yield item.content;
2920
4135
  }
2921
4136
  }
2922
4137
  const live = view.docs["pi.live"];
2923
- 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)) {
2924
4139
  yield live.generation.message.content;
2925
4140
  }
2926
4141
  }
@@ -2928,7 +4143,7 @@ function imageBlocks(view) {
2928
4143
  const blocks = [];
2929
4144
  for (const content of contentArrays(view)) {
2930
4145
  for (const block of content) {
2931
- if (isObject(block) && isImageBlock(block))
4146
+ if (isObject2(block) && isImageBlock(block))
2932
4147
  blocks.push(block);
2933
4148
  }
2934
4149
  }
@@ -3016,8 +4231,8 @@ class ImageVariants {
3016
4231
  if (job === undefined)
3017
4232
  return;
3018
4233
  this.#active += 1;
3019
- this.#derive(job.key, job.variant, job.original).then((result2) => {
3020
- job.resolve(result2);
4234
+ this.#derive(job.key, job.variant, job.original).then((result3) => {
4235
+ job.resolve(result3);
3021
4236
  this.#finish();
3022
4237
  }, (error) => {
3023
4238
  job.reject(error instanceof Error ? error : new Error(String(error)));
@@ -3038,13 +4253,13 @@ class ImageVariants {
3038
4253
  if (prepared === undefined || prepared.data === original.data && prepared.mimeType === original.mimeType) {
3039
4254
  return { ...original, variant: "original" };
3040
4255
  }
3041
- const result2 = {
4256
+ const result3 = {
3042
4257
  data: prepared.data,
3043
4258
  mimeType: prepared.mimeType,
3044
4259
  variant
3045
4260
  };
3046
- this.#write(key, result2);
3047
- return result2;
4261
+ this.#write(key, result3);
4262
+ return result3;
3048
4263
  } catch {
3049
4264
  return { ...original, variant: "original" };
3050
4265
  }
@@ -3057,8 +4272,8 @@ class ImageVariants {
3057
4272
  this.#cache.set(key, cached);
3058
4273
  return cached;
3059
4274
  }
3060
- #write(key, result2) {
3061
- const bytes = result2.data.length;
4275
+ #write(key, result3) {
4276
+ const bytes = result3.data.length;
3062
4277
  if (bytes > this.#maxCacheBytes)
3063
4278
  return;
3064
4279
  while (this.#cacheBytes + bytes > this.#maxCacheBytes) {
@@ -3069,17 +4284,17 @@ class ImageVariants {
3069
4284
  this.#cache.delete(oldest);
3070
4285
  this.#cacheBytes -= previous.data.length;
3071
4286
  }
3072
- this.#cache.set(key, result2);
4287
+ this.#cache.set(key, result3);
3073
4288
  this.#cacheBytes += bytes;
3074
4289
  }
3075
4290
  }
3076
4291
 
3077
4292
  // apps/host/src/auto-title.ts
3078
- import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
4293
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
3079
4294
 
3080
4295
  // apps/host/src/title-doc.ts
3081
- import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
3082
- var TitleDoc = defineDoc6({
4296
+ import { defineDoc as defineDoc9 } from "@earendil-works/pi-durable";
4297
+ var TitleDoc = defineDoc9({
3083
4298
  kind: "pinomad.title",
3084
4299
  version: 1,
3085
4300
  scope: "conversation",
@@ -3090,11 +4305,11 @@ var TitleDoc = defineDoc6({
3090
4305
  });
3091
4306
 
3092
4307
  // apps/host/src/auto-title.ts
3093
- var context = BACKGROUND_CONTEXT;
4308
+ var context = BACKGROUND_CONTEXT2;
3094
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.";
3095
4310
  var TITLE_INPUT_LIMIT = 2000;
3096
4311
  var TITLE_MAX_LENGTH = 80;
3097
- var textOf = (content) => {
4312
+ var textOf2 = (content) => {
3098
4313
  if (typeof content === "string")
3099
4314
  return content;
3100
4315
  if (!Array.isArray(content))
@@ -3151,7 +4366,7 @@ function attachAutoTitle(runs, deps) {
3151
4366
  return;
3152
4367
  const firstMessage = first.model?.[0];
3153
4368
  const firstText = (stripUserBlocks(firstMessage?.content ?? "").text || "(images or files only)").slice(0, TITLE_INPUT_LIMIT);
3154
- const answer = textOf(last.content).slice(0, TITLE_INPUT_LIMIT);
4369
+ const answer = textOf2(last.content).slice(0, TITLE_INPUT_LIMIT);
3155
4370
  const request = {
3156
4371
  systemPrompt: TITLE_PROMPT,
3157
4372
  messages: [
@@ -3198,8 +4413,8 @@ ${answer}`}`,
3198
4413
 
3199
4414
  // apps/host/src/push.ts
3200
4415
  import { createHash as createHash2 } from "node:crypto";
3201
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
3202
- 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";
3203
4418
 
3204
4419
  // packages/client/src/family.ts
3205
4420
  var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
@@ -3256,8 +4471,8 @@ function diffRunNotices(previous, organized) {
3256
4471
  }
3257
4472
 
3258
4473
  // apps/host/src/push.ts
3259
- var context2 = BACKGROUND_CONTEXT2;
3260
- var PushDoc = defineDoc7({
4474
+ var context2 = BACKGROUND_CONTEXT3;
4475
+ var PushDoc = defineDoc10({
3261
4476
  kind: "pinomad.push",
3262
4477
  version: 2,
3263
4478
  scope: "session",
@@ -3394,18 +4609,18 @@ async function attachPushSender(options) {
3394
4609
  return await new Promise((resolve5) => {
3395
4610
  const waiters = titleWaiters.get(notice.id) ?? new Set;
3396
4611
  titleWaiters.set(notice.id, waiters);
3397
- const settle = (title) => {
3398
- waiters.delete(settle);
4612
+ const settle2 = (title) => {
4613
+ waiters.delete(settle2);
3399
4614
  if (waiters.size === 0)
3400
4615
  titleWaiters.delete(notice.id);
3401
4616
  timers.clearTimeout(timer);
3402
4617
  resolve5(title);
3403
4618
  };
3404
- const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
3405
- waiters.add(settle);
4619
+ const timer = timers.setTimeout(() => settle2(FALLBACK_TITLE), TITLE_WAIT_MS);
4620
+ waiters.add(settle2);
3406
4621
  namedTitle(notice.id).then((title) => {
3407
4622
  if (title !== undefined)
3408
- settle(title);
4623
+ settle2(title);
3409
4624
  });
3410
4625
  });
3411
4626
  };
@@ -3545,8 +4760,8 @@ async function attachPushSender(options) {
3545
4760
  continue;
3546
4761
  const waiters = titleWaiters.get(id);
3547
4762
  titleWaiters.delete(id);
3548
- for (const settle of waiters ?? [])
3549
- settle(title);
4763
+ for (const settle2 of waiters ?? [])
4764
+ settle2(title);
3550
4765
  }
3551
4766
  },
3552
4767
  end: () => {}
@@ -3602,7 +4817,7 @@ function toolTarget(args) {
3602
4817
  }
3603
4818
 
3604
4819
  // apps/host/src/gateway.ts
3605
- var context3 = BACKGROUND_CONTEXT3;
4820
+ var context3 = BACKGROUND_CONTEXT4;
3606
4821
  var forkTails = new WeakMap;
3607
4822
  function assertQueuedInput(record, conversationId) {
3608
4823
  if (record.conversationId !== conversationId) {
@@ -3615,7 +4830,7 @@ async function dismissPendingQuestions(tx, docs, conversationId) {
3615
4830
  let dismissed = false;
3616
4831
  for (const doc of docs) {
3617
4832
  const value = await tx.doc(doc.token, conversationId);
3618
- if (!Value2.Check(QuestionSchema, value))
4833
+ if (!Value.Check(QuestionSchema, value))
3619
4834
  continue;
3620
4835
  for (const request of value.requests) {
3621
4836
  if (request.resolution === undefined) {
@@ -3667,6 +4882,19 @@ function submitResult(record) {
3667
4882
  ...record.entry === undefined ? {} : { entry: record.entry }
3668
4883
  };
3669
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
+ });
3670
4898
  var LOOPBACK_SECURE_PATH = "/secure";
3671
4899
  async function startGateway(options) {
3672
4900
  if (options.secure?.lan?.publicUrl !== undefined)
@@ -3702,10 +4930,16 @@ async function startGateway(options) {
3702
4930
  const clients = new Set;
3703
4931
  let rtc;
3704
4932
  const rtcHello = () => rtc?.hello();
3705
- const terminals = await startTerminals(options.terminals);
3706
- 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) => {
3707
4941
  for (const client of clients)
3708
- client.send({ type: "notice", level, message });
4942
+ client.send({ type: "notice", level, message: message2 });
3709
4943
  };
3710
4944
  let remote;
3711
4945
  let relay;
@@ -3740,6 +4974,7 @@ async function startGateway(options) {
3740
4974
  devicePublicKey,
3741
4975
  rtcHello,
3742
4976
  terminals,
4977
+ ...options.processes === undefined ? {} : { processes: options.processes },
3743
4978
  closed: (self) => {
3744
4979
  clients.delete(self);
3745
4980
  connections.delete(self);
@@ -3977,6 +5212,7 @@ async function startGateway(options) {
3977
5212
  await detachPush?.close();
3978
5213
  conversations.dispose();
3979
5214
  terminals?.closeAll();
5215
+ await options.processes?.close(context3);
3980
5216
  await rtc?.close();
3981
5217
  await Promise.all([...connections].map((connection) => connection.dispose()));
3982
5218
  for (const socket of server.clients)
@@ -4149,6 +5385,8 @@ function secureHandshake(carrier, secure, policy, handlers) {
4149
5385
  session = new SecureSession(admitted.result);
4150
5386
  stage = "settled";
4151
5387
  await carrier.send(admitted.message);
5388
+ if (closed)
5389
+ return;
4152
5390
  if (admitted.purpose === "signal")
4153
5391
  handlers.signal?.(connection, admitted.deviceKey);
4154
5392
  else
@@ -4209,10 +5447,10 @@ function secureHandshake(carrier, secure, policy, handlers) {
4209
5447
  if (decision.outcome === "admitted" && decision.requestId !== undefined) {
4210
5448
  policy.accountAdmitted(decision.requestId, deviceKey);
4211
5449
  }
4212
- const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
5450
+ const { message: message2, result: result3 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
4213
5451
  return {
4214
- result: result2,
4215
- message,
5452
+ result: result3,
5453
+ message: message2,
4216
5454
  deviceKey,
4217
5455
  purpose,
4218
5456
  ...decision.outcome === "admitted" ? { joined: decision.entry } : {}
@@ -4324,7 +5562,7 @@ class ConversationList {
4324
5562
  facts.tools = live?.tools;
4325
5563
  for (const token of this.#questionTokens) {
4326
5564
  const value = await harness.snapshot(token, record.id, context3);
4327
- 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)) {
4328
5566
  facts.pending.add(token.definition.kind);
4329
5567
  }
4330
5568
  }
@@ -4335,6 +5573,9 @@ class ConversationList {
4335
5573
  if (typeof subagent?.role === "string" && subagent.role !== "")
4336
5574
  facts.role = subagent.role;
4337
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";
4338
5579
  }
4339
5580
  facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
4340
5581
  const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
@@ -4363,10 +5604,10 @@ class ConversationList {
4363
5604
  continue;
4364
5605
  touched.add(entry.conversationId);
4365
5606
  facts.newest = entry.id;
4366
- const message = entry.model?.[0];
4367
- facts.updatedAt = typeof message?.timestamp === "number" ? message.timestamp : undefined;
4368
- facts.failed = entry.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error";
4369
- 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;
4370
5611
  } else if (change.type === "document") {
4371
5612
  const id = change.conversationId;
4372
5613
  const value = change.kind === "change" ? change.value : null;
@@ -4386,7 +5627,7 @@ class ConversationList {
4386
5627
  if (wasRunning && !facts.running && this.#records.get(id)?.owner === undefined)
4387
5628
  ended.add(id);
4388
5629
  } else if (this.#questionKinds.has(change.record.kind)) {
4389
- 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);
4390
5631
  const next = new Set(facts.pending);
4391
5632
  if (pending)
4392
5633
  next.add(change.record.kind);
@@ -4402,6 +5643,8 @@ class ConversationList {
4402
5643
  facts.label = typeof subagent?.label === "string" && subagent.label !== "" ? subagent.label : undefined;
4403
5644
  facts.role = typeof subagent?.role === "string" && subagent.role !== "" ? subagent.role : undefined;
4404
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";
4405
5648
  touched.add(id);
4406
5649
  } else if (change.record.kind === TitleDoc.definition.kind) {
4407
5650
  const named = value?.title;
@@ -4569,7 +5812,8 @@ function summaryOf(record, facts, role, currentAgent) {
4569
5812
  return {
4570
5813
  id: record.id,
4571
5814
  kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
4572
- ...record.owner === undefined ? {} : { lifecycle: "foreground" },
5815
+ ...record.owner === undefined ? {} : { lifecycle: facts.lifecycle ?? "foreground" },
5816
+ ...facts.name === undefined ? {} : { name: facts.name },
4573
5817
  ...facts.model === undefined ? {} : { model: facts.model },
4574
5818
  ...parent === undefined ? {} : { parent },
4575
5819
  ...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
@@ -4583,13 +5827,13 @@ function summaryOf(record, facts, role, currentAgent) {
4583
5827
  };
4584
5828
  }
4585
5829
  function sameSummary(a, b) {
4586
- return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
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;
4587
5831
  }
4588
5832
  function titleOf(entry) {
4589
- const message = entry?.model?.[0];
4590
- if (message?.role !== "user")
5833
+ const message2 = entry?.model?.[0];
5834
+ if (message2?.role !== "user")
4591
5835
  return {};
4592
- const title = stripUserBlocks(message.content).text.replace(/\s+/g, " ").trim();
5836
+ const title = stripUserBlocks(message2.content).text.replace(/\s+/g, " ").trim();
4593
5837
  return title === "" ? {} : { title };
4594
5838
  }
4595
5839
  var WHOLE_TRANSCRIPT = { min: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
@@ -4635,21 +5879,21 @@ async function historyFacts(harness, id) {
4635
5879
  newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
4636
5880
  cursor = page.next;
4637
5881
  } while (cursor !== undefined);
4638
- const message = newest?.model?.[0];
5882
+ const message2 = newest?.model?.[0];
4639
5883
  return {
4640
5884
  ...titleOf(first),
4641
5885
  ...newest === undefined ? {} : {
4642
5886
  newest: newest.id,
4643
- ...typeof message?.timestamp === "number" ? { updatedAt: message.timestamp } : {},
4644
- failed: newest.kind === "pi.assistant" && message?.role === "assistant" && message.stopReason === "error",
4645
- ...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) } : {}
4646
5890
  }
4647
5891
  };
4648
5892
  }
4649
- function callsOf(message) {
4650
- if (message?.role !== "assistant")
5893
+ function callsOf(message2) {
5894
+ if (message2?.role !== "assistant")
4651
5895
  return;
4652
- const content = message.content;
5896
+ const content = message2.content;
4653
5897
  if (!Array.isArray(content))
4654
5898
  return;
4655
5899
  const calls = new Map;
@@ -4756,6 +6000,12 @@ class GatewayClient {
4756
6000
  throw new Error("Provider setup is not available on this host");
4757
6001
  return modelAuth;
4758
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
+ }
4759
6009
  #hostValues;
4760
6010
  #subscriptions = new Map;
4761
6011
  #terminalSizes = new Map;
@@ -4773,7 +6023,8 @@ class GatewayClient {
4773
6023
  auth: options.modelAuth?.auth ?? constantValue({ logins: [] }),
4774
6024
  providers: options.modelAuth?.providers ?? constantValue({ providers: [], logins: [] }),
4775
6025
  settings: options.settings,
4776
- agents: options.agents ?? constantValue({ roles: [], problems: [] })
6026
+ agents: options.agents ?? constantValue({ roles: [], problems: [] }),
6027
+ upgrade: options.upgrade ?? NO_UPGRADES
4777
6028
  };
4778
6029
  this.#hooks = hooks;
4779
6030
  this.devicePublicKey = hooks.devicePublicKey;
@@ -4822,7 +6073,8 @@ class GatewayClient {
4822
6073
  toolPresentations: options.toolPresentations ?? {},
4823
6074
  endpointSet: this.#hooks.endpointSet(),
4824
6075
  ...rtc === undefined ? {} : { rtc },
4825
- ...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 }
4826
6078
  });
4827
6079
  }
4828
6080
  send(frame) {
@@ -4909,6 +6161,18 @@ class GatewayClient {
4909
6161
  });
4910
6162
  return { stop: detach };
4911
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
+ }
4912
6176
  if (stream.startsWith("doc:")) {
4913
6177
  const at = stream.lastIndexOf(":");
4914
6178
  return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
@@ -5054,8 +6318,11 @@ class GatewayClient {
5054
6318
  #submissionByRequest(conversationId, requestId) {
5055
6319
  return this.#options.harness.commit((tx) => tx.submissionByRequest(conversationId, requestId), context3);
5056
6320
  }
5057
- async#compact(conversation, instructions) {
5058
- 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);
5059
6326
  this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
5060
6327
  return taskId;
5061
6328
  }
@@ -5073,6 +6340,17 @@ class GatewayClient {
5073
6340
  }
5074
6341
  }
5075
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) {
5076
6354
  switch (method) {
5077
6355
  case "terminalInput": {
5078
6356
  const input = args;
@@ -5102,6 +6380,11 @@ class GatewayClient {
5102
6380
  this.#terminals().close(args.terminalId);
5103
6381
  this.#terminalSizes.delete(args.terminalId);
5104
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
+ }
5105
6388
  case "addProject":
5106
6389
  return addProject(this.#options.harness, args.path, context3);
5107
6390
  case "removeProject":
@@ -5114,6 +6397,9 @@ class GatewayClient {
5114
6397
  const existing = await this.#submissionByRequest(input.conversationId, input.requestId);
5115
6398
  if (existing !== undefined)
5116
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
+ }
5117
6403
  const resolved = images();
5118
6404
  const conversation2 = await this.#conversation(input.conversationId);
5119
6405
  const attachments = await this.#attachmentText(input.attachments);
@@ -5163,13 +6449,27 @@ class GatewayClient {
5163
6449
  }
5164
6450
  case "archive": {
5165
6451
  const input = args;
5166
- await archive(this.#options.harness, input.conversationId, input.archived, context3);
5167
- if (input.archived) {
5168
- this.#hooks.terminals?.closeOwners(await conversationSubtree(this.#options.harness, input.conversationId, context3));
5169
- const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
5170
- for (const { record, reason } of kept) {
5171
- await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
5172
- }
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}` });
5173
6473
  }
5174
6474
  return null;
5175
6475
  }
@@ -5238,6 +6538,15 @@ class GatewayClient {
5238
6538
  await this.#options.settings.update(args.patch);
5239
6539
  return null;
5240
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;
5241
6550
  case "createAgent": {
5242
6551
  const store = this.#options.agents;
5243
6552
  if (store === undefined)
@@ -5402,17 +6711,20 @@ class GatewayClient {
5402
6711
  return null;
5403
6712
  }
5404
6713
  case "abort":
6714
+ if (await this.#isSubagent(conversation.id)) {
6715
+ await this.#options.harness.commit((tx) => markStop(tx, conversation.id, "user"), context3);
6716
+ }
5405
6717
  await conversation.abort(context3);
5406
6718
  return null;
5407
6719
  case "compact": {
5408
- const { instructions } = args;
5409
- return { taskId: await this.#compact(conversation, instructions) };
6720
+ const { instructions: instructions2 } = args;
6721
+ return { taskId: await this.#compact(conversation, instructions2) };
5410
6722
  }
5411
6723
  case "listCommands":
5412
6724
  return listCommands();
5413
6725
  case "runCommand": {
5414
6726
  const { name, args: input } = args;
5415
- await runCommand({ compact: (instructions) => this.#compact(conversation, instructions) }, name, input);
6727
+ await runCommand({ compact: (instructions2) => this.#compact(conversation, instructions2) }, name, input);
5416
6728
  return null;
5417
6729
  }
5418
6730
  case "setModel": {
@@ -5488,7 +6800,7 @@ class GatewayClient {
5488
6800
  const token = doc.token;
5489
6801
  return await this.#options.harness.commit(async (tx) => {
5490
6802
  const value = await tx.doc(token, conversation.id);
5491
- if (!Value2.Check(QuestionSchema, value))
6803
+ if (!Value.Check(QuestionSchema, value))
5492
6804
  throw new Error(`Document ${kind} does not hold question requests`);
5493
6805
  const request = value.requests.find((candidate) => candidate.id === requestId);
5494
6806
  if (request === undefined)
@@ -5537,6 +6849,11 @@ class GatewayClient {
5537
6849
  throw new Error("Terminals are unavailable on this host");
5538
6850
  return this.#hooks.terminals;
5539
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
+ }
5540
6857
  async#terminalOwner(conversation) {
5541
6858
  const cwd = (await conversation.agent(context3)).cwd;
5542
6859
  if (cwd === undefined)
@@ -5576,7 +6893,7 @@ class GatewayClient {
5576
6893
  async#dismissPendingQuestions(conversationId, context4) {
5577
6894
  const questionDocs = this.#questionDocs();
5578
6895
  const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
5579
- 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));
5580
6897
  if (!pending)
5581
6898
  return false;
5582
6899
  return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
@@ -5585,7 +6902,7 @@ class GatewayClient {
5585
6902
  return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
5586
6903
  }
5587
6904
  get #chatsPrefix() {
5588
- return join7(this.#options.dataDir, "chats") + sep5;
6905
+ return join8(this.#options.dataDir, "chats") + sep6;
5589
6906
  }
5590
6907
  async#changes(conversation, context4) {
5591
6908
  const cwd = (await conversation.agent(context4)).cwd;
@@ -5620,8 +6937,8 @@ class GatewayClient {
5620
6937
  // apps/host/src/host.ts
5621
6938
  import { randomBytes as randomBytes4 } from "node:crypto";
5622
6939
  import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
5623
- import { basename as basename3, join as join10, sep as sep6 } from "node:path";
5624
- 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";
5625
6942
  import { createRegistry, Harness } from "@earendil-works/pi-durable";
5626
6943
  import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
5627
6944
 
@@ -5637,10 +6954,10 @@ import {
5637
6954
  StreamableHttpTransport,
5638
6955
  toLlmContent
5639
6956
  } from "@earendil-works/pi-mcp";
5640
- 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";
5641
6958
  import { Type as Type5 } from "typebox";
5642
6959
  var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
5643
- var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
6960
+ var isObject3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5644
6961
  function expand(value) {
5645
6962
  let missing;
5646
6963
  const expanded = value.replace(ENV_VAR, (_all, name) => {
@@ -5654,7 +6971,7 @@ function expand(value) {
5654
6971
  function expandAll(values, field, error) {
5655
6972
  if (values === undefined)
5656
6973
  return;
5657
- if (!isObject2(values)) {
6974
+ if (!isObject3(values)) {
5658
6975
  error(`"${field}" must be an object of strings`);
5659
6976
  return;
5660
6977
  }
@@ -5664,21 +6981,21 @@ function expandAll(values, field, error) {
5664
6981
  error(`"${field}.${key}" must be a string`);
5665
6982
  return;
5666
6983
  }
5667
- const result2 = expand(value);
5668
- if (typeof result2 !== "string") {
5669
- 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}`);
5670
6987
  return;
5671
6988
  }
5672
- expanded[key] = result2;
6989
+ expanded[key] = result3;
5673
6990
  }
5674
6991
  return expanded;
5675
6992
  }
5676
6993
  function parseServer(name, raw, error) {
5677
- const fail = (message) => {
5678
- error(`${name}: ${message}`);
6994
+ const fail = (message2) => {
6995
+ error(`${name}: ${message2}`);
5679
6996
  return;
5680
6997
  };
5681
- if (!isObject2(raw))
6998
+ if (!isObject3(raw))
5682
6999
  return fail("server entry must be an object");
5683
7000
  if (raw.enabled === false)
5684
7001
  return { name, disabled: true };
@@ -5700,7 +7017,7 @@ function parseServer(name, raw, error) {
5700
7017
  }
5701
7018
  if (raw.cwd !== undefined && typeof raw.cwd !== "string")
5702
7019
  return fail(`"cwd" must be a string`);
5703
- const env = expandAll(raw.env, "env", (message) => error(`${name}: ${message}`));
7020
+ const env = expandAll(raw.env, "env", (message2) => error(`${name}: ${message2}`));
5704
7021
  if (raw.env !== undefined && env === undefined)
5705
7022
  return;
5706
7023
  const cwd = raw.cwd === undefined ? homedir2() : isAbsolute2(raw.cwd) ? raw.cwd : resolve5(homedir2(), raw.cwd);
@@ -5715,7 +7032,7 @@ function parseServer(name, raw, error) {
5715
7032
  if (raw.url !== undefined || raw.type === "http" || raw.type === "sse") {
5716
7033
  if (typeof raw.url !== "string" || raw.url === "")
5717
7034
  return fail(`HTTP server needs a "url"`);
5718
- const headers = expandAll(raw.headers, "headers", (message) => error(`${name}: ${message}`));
7035
+ const headers = expandAll(raw.headers, "headers", (message2) => error(`${name}: ${message2}`));
5719
7036
  if (raw.headers !== undefined && headers === undefined)
5720
7037
  return;
5721
7038
  return {
@@ -5737,13 +7054,13 @@ async function loadConfig(configPath) {
5737
7054
  return { errors: [], entries: [] };
5738
7055
  return { errors: [`${configPath}: ${error instanceof Error ? error.message : String(error)}`], entries: [] };
5739
7056
  }
5740
- if (!isObject2(parsed) || parsed.mcpServers !== undefined && !isObject2(parsed.mcpServers)) {
7057
+ if (!isObject3(parsed) || parsed.mcpServers !== undefined && !isObject3(parsed.mcpServers)) {
5741
7058
  return { errors: [`${configPath}: "mcpServers" must be an object`], entries: [] };
5742
7059
  }
5743
7060
  const errors = [];
5744
7061
  const entries = [];
5745
7062
  for (const [name, raw] of Object.entries(parsed.mcpServers ?? {})) {
5746
- const entry = parseServer(name, raw, (message) => errors.push(message));
7063
+ const entry = parseServer(name, raw, (message2) => errors.push(message2));
5747
7064
  if (entry !== undefined)
5748
7065
  entries.push(entry);
5749
7066
  }
@@ -5761,7 +7078,7 @@ var callToolResultSchema = (tool) => ({
5761
7078
  required: ["content"]
5762
7079
  });
5763
7080
  function mcpTool(client, timeoutMs, exposure, name, tool) {
5764
- return defineTool4({
7081
+ return defineTool5({
5765
7082
  name,
5766
7083
  description: tool.description ?? tool.title ?? tool.name,
5767
7084
  parameters: Type5.Unsafe({
@@ -5772,11 +7089,11 @@ function mcpTool(client, timeoutMs, exposure, name, tool) {
5772
7089
  ...exposure === "direct" ? {} : { callers: ["tools"] },
5773
7090
  structuredOutputSchema: Type5.Unsafe(callToolResultSchema(tool)),
5774
7091
  execute: async (args, _api, context4) => {
5775
- const result2 = await client.callTool(tool.name, args, {
7092
+ const result3 = await client.callTool(tool.name, args, {
5776
7093
  signal: context4.abortSignal ?? NEVER_ABORT,
5777
7094
  timeoutMs
5778
7095
  });
5779
- return { output: toLlmContent(result2), structuredOutput: result2, isError: result2.isError === true };
7096
+ return { output: toLlmContent(result3), structuredOutput: result3, isError: result3.isError === true };
5780
7097
  }
5781
7098
  });
5782
7099
  }
@@ -5796,7 +7113,7 @@ function startMcp(options) {
5796
7113
  listener(value);
5797
7114
  };
5798
7115
  const install = () => {
5799
- options.registry.install(defineExtension5({ name: "mcp", tools: [...toolsets.values()].flat() }));
7116
+ options.registry.install(defineExtension7({ name: "mcp", tools: [...toolsets.values()].flat() }));
5800
7117
  options.onNamespaces?.([...namespaces.values()]);
5801
7118
  };
5802
7119
  const set = (name, patch) => {
@@ -5916,7 +7233,7 @@ ${stderr}`;
5916
7233
  import { randomBytes as randomBytes2 } from "node:crypto";
5917
7234
  import { existsSync as existsSync2, watch } from "node:fs";
5918
7235
  import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, rm as rm2, writeFile } from "node:fs/promises";
5919
- import { join as join8 } from "node:path";
7236
+ import { join as join9 } from "node:path";
5920
7237
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
5921
7238
 
5922
7239
  // packages/protocol/src/agents.ts
@@ -5960,7 +7277,7 @@ async function load(dir) {
5960
7277
  const problem = (reason2) => void problems.push({ file, reason: reason2 });
5961
7278
  let parsed;
5962
7279
  try {
5963
- parsed = parseFrontmatter(await readFile3(join8(dir, file), "utf8"));
7280
+ parsed = parseFrontmatter(await readFile3(join9(dir, file), "utf8"));
5964
7281
  } catch (error) {
5965
7282
  problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
5966
7283
  continue;
@@ -6072,13 +7389,13 @@ async function openAgentRoles(options) {
6072
7389
  if (problem !== undefined)
6073
7390
  throw new Error(problem);
6074
7391
  await enqueue(async () => {
6075
- const target = join8(dir, `${role.name}.md`);
7392
+ const target = join9(dir, `${role.name}.md`);
6076
7393
  const exists = existsSync2(target);
6077
7394
  if (mustExist && !exists)
6078
7395
  throw new Error(`No such agent: "${role.name}"`);
6079
7396
  if (!mustExist && exists)
6080
7397
  throw new Error(`Agent "${role.name}" already exists`);
6081
- const temp = join8(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
7398
+ const temp = join9(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
6082
7399
  try {
6083
7400
  await writeFile(temp, serialize(role));
6084
7401
  await rename(temp, target);
@@ -6101,7 +7418,7 @@ async function openAgentRoles(options) {
6101
7418
  if (problem !== undefined)
6102
7419
  throw new Error(problem);
6103
7420
  await enqueue(async () => {
6104
- const target = join8(dir, `${name}.md`);
7421
+ const target = join9(dir, `${name}.md`);
6105
7422
  if (!existsSync2(target))
6106
7423
  throw new Error(`No such agent: "${name}"`);
6107
7424
  await rm2(target);
@@ -6135,7 +7452,7 @@ import {
6135
7452
  stat as stat7,
6136
7453
  writeFile as writeFile2
6137
7454
  } from "node:fs/promises";
6138
- 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";
6139
7456
  var META = "meta.json";
6140
7457
  var DATA = "file";
6141
7458
  var CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
@@ -6230,16 +7547,16 @@ class AttachmentStore {
6230
7547
  return next;
6231
7548
  }
6232
7549
  #directory(id) {
6233
- return join9(this.#root, id);
7550
+ return join10(this.#root, id);
6234
7551
  }
6235
7552
  #dataPath(meta) {
6236
- return join9(this.#directory(meta.id), DATA, meta.fileName);
7553
+ return join10(this.#directory(meta.id), DATA, meta.fileName);
6237
7554
  }
6238
7555
  async#readMeta(id) {
6239
7556
  if (!isAttachmentId(id))
6240
7557
  return;
6241
7558
  try {
6242
- 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);
6243
7560
  } catch {
6244
7561
  return;
6245
7562
  }
@@ -6247,10 +7564,10 @@ class AttachmentStore {
6247
7564
  async#writeMeta(meta) {
6248
7565
  const { id, ...value } = meta;
6249
7566
  const directory = this.#directory(id);
6250
- const temporary = join9(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
7567
+ const temporary = join10(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
6251
7568
  await writeFile2(temporary, `${JSON.stringify(value)}
6252
7569
  `, { mode: 384 });
6253
- await rename2(temporary, join9(directory, META));
7570
+ await rename2(temporary, join10(directory, META));
6254
7571
  }
6255
7572
  async#rebuildPending() {
6256
7573
  this.#pending.clear();
@@ -6295,7 +7612,7 @@ class AttachmentStore {
6295
7612
  expiresAt: this.#now() + this.#ttlMs
6296
7613
  };
6297
7614
  try {
6298
- await mkdir3(join9(directory, DATA), { recursive: true });
7615
+ await mkdir3(join10(directory, DATA), { recursive: true });
6299
7616
  await writeFile2(this.#dataPath(meta), new Uint8Array(0), { mode: 384 });
6300
7617
  if (size === 0)
6301
7618
  await chmod(this.#dataPath(meta), 292);
@@ -6431,16 +7748,16 @@ class AttachmentStore {
6431
7748
  }
6432
7749
 
6433
7750
  // apps/host/src/answer-timing.ts
6434
- function withAnswerStart(message, answerStartedAt) {
6435
- const pinomad = message.pinomad;
6436
- return { ...message, pinomad: { ...pinomad, answerStartedAt } };
7751
+ function withAnswerStart(message2, answerStartedAt) {
7752
+ const pinomad = message2.pinomad;
7753
+ return { ...message2, pinomad: { ...pinomad, answerStartedAt } };
6437
7754
  }
6438
7755
  function stripPinomad(context4) {
6439
7756
  let changed = false;
6440
- const messages = context4.messages.map((message) => {
6441
- if (message.role !== "assistant" || !("pinomad" in message))
6442
- return message;
6443
- 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;
6444
7761
  changed = true;
6445
7762
  return rest;
6446
7763
  });
@@ -6465,7 +7782,7 @@ function trackAnswerStart(stream) {
6465
7782
  const push = stream.push.bind(stream);
6466
7783
  stream.push = (event) => push(annotate(event));
6467
7784
  const end = stream.end.bind(stream);
6468
- 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));
6469
7786
  return stream;
6470
7787
  }
6471
7788
  function withAnswerTiming(models) {
@@ -6480,10 +7797,594 @@ function withAnswerTiming(models) {
6480
7797
  });
6481
7798
  }
6482
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
+
6483
8384
  // apps/host/src/host.ts
6484
- var context4 = BACKGROUND_CONTEXT4;
8385
+ var context4 = BACKGROUND_CONTEXT6;
6485
8386
  async function hostToken(dataDir) {
6486
- const path = join10(dataDir, "token");
8387
+ const path = join11(dataDir, "token");
6487
8388
  try {
6488
8389
  return (await readFile5(path, "utf8")).trim();
6489
8390
  } catch {
@@ -6498,7 +8399,7 @@ async function openHost(options) {
6498
8399
  validatePublicUrl(options.remote.publicUrl);
6499
8400
  await mkdir4(options.dataDir, { recursive: true });
6500
8401
  const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
6501
- const release = await lockfile.lock(options.dataDir, {
8402
+ const release2 = await lockfile.lock(options.dataDir, {
6502
8403
  realpath: false,
6503
8404
  stale,
6504
8405
  retries: { retries: Math.ceil(stale / 1000) + 2, minTimeout: 1000, maxTimeout: 1000 }
@@ -6506,26 +8407,38 @@ async function openHost(options) {
6506
8407
  throw new Error(`Session is already open in another process: ${options.dataDir}`, { cause: error });
6507
8408
  });
6508
8409
  let harness;
8410
+ let processes;
6509
8411
  let mcp;
6510
8412
  let agentRoles;
6511
8413
  let attachments;
6512
8414
  let extensions = [];
8415
+ let upgrader;
6513
8416
  try {
6514
8417
  const models = withAnswerTiming(options.models);
6515
8418
  const token = await hostToken(options.dataDir);
6516
- attachments = new AttachmentStore({ dir: join10(options.dataDir, "attachments") });
8419
+ attachments = new AttachmentStore({ dir: join11(options.dataDir, "attachments") });
6517
8420
  await attachments.open();
6518
8421
  const reports = [];
6519
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
+ });
6520
8432
  const registry = createRegistry();
6521
- agentRoles = await openAgentRoles({ dir: join10(options.dataDir, "agents") });
8433
+ agentRoles = await openAgentRoles({ dir: join11(options.dataDir, "agents") });
6522
8434
  extensions = typeof options.extensions === "function" ? options.extensions({
6523
8435
  harness: () => {
6524
8436
  if (harness === undefined)
6525
8437
  throw new Error("harness is not open yet");
6526
8438
  return harness;
6527
8439
  },
6528
- agentRoles
8440
+ agentRoles,
8441
+ ...processes === undefined ? {} : { processes }
6529
8442
  }) : options.extensions ?? [];
6530
8443
  for (const { extension } of extensions)
6531
8444
  registry.install(extension);
@@ -6541,13 +8454,13 @@ async function openHost(options) {
6541
8454
  }
6542
8455
  });
6543
8456
  const hostSettings = await openHostSettings({
6544
- path: join10(options.dataDir, "settings.json"),
8457
+ path: join11(options.dataDir, "settings.json"),
6545
8458
  fallback: options.settingsFallback,
6546
8459
  models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
6547
8460
  });
6548
8461
  const environments = new Map;
6549
- const chatsDir = join10(options.dataDir, "chats");
6550
- 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")), {
6551
8464
  models,
6552
8465
  registry,
6553
8466
  env: async (target) => {
@@ -6555,7 +8468,7 @@ async function openHost(options) {
6555
8468
  if (cwd === undefined)
6556
8469
  return;
6557
8470
  const worktrees = worktreeRoot(options.dataDir);
6558
- if (cwd === worktrees || cwd.startsWith(worktrees + sep6)) {
8471
+ if (cwd === worktrees || cwd.startsWith(worktrees + sep7)) {
6559
8472
  const index = await target.read.snapshot(IndexDoc, context4);
6560
8473
  const record = checkoutAt(index?.checkouts, cwd);
6561
8474
  if (record !== undefined) {
@@ -6577,6 +8490,10 @@ async function openHost(options) {
6577
8490
  },
6578
8491
  onReport: (error) => report(error)
6579
8492
  }, context4);
8493
+ if (processes === undefined)
8494
+ await settleLostProcesses(harness, context4);
8495
+ else
8496
+ await processes.open(context4);
6580
8497
  await ensureIndex(harness, context4);
6581
8498
  await ensureDevices(harness, context4);
6582
8499
  for (const path of options.projects ?? [])
@@ -6584,6 +8501,18 @@ async function openHost(options) {
6584
8501
  const hostKey = await loadHostKey(options.dataDir);
6585
8502
  const offers = pairingOffers(options.remote?.pairingTtlMs);
6586
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
+ });
6587
8516
  const gateway = await startGateway({
6588
8517
  harness,
6589
8518
  models,
@@ -6613,6 +8542,8 @@ async function openHost(options) {
6613
8542
  },
6614
8543
  ...options.rtc === undefined ? {} : { rtc: options.rtc },
6615
8544
  ...options.terminals === undefined ? {} : { terminals: options.terminals },
8545
+ ...processes === undefined ? {} : { processes },
8546
+ ...upgrader === undefined ? {} : { upgrade: upgrader },
6616
8547
  secure: {
6617
8548
  hostKey,
6618
8549
  offers,
@@ -6645,6 +8576,7 @@ async function openHost(options) {
6645
8576
  harness,
6646
8577
  closed: opened.closed,
6647
8578
  settings: hostSettings,
8579
+ ...processes === undefined ? {} : { processes },
6648
8580
  createAccountOffer: async () => {
6649
8581
  const binding = await readServerBinding(options.dataDir).catch(() => {
6650
8582
  return;
@@ -6664,6 +8596,7 @@ async function openHost(options) {
6664
8596
  close() {
6665
8597
  closing ??= (async () => {
6666
8598
  try {
8599
+ upgrader?.close();
6667
8600
  await gateway.close();
6668
8601
  await opened.close(context4);
6669
8602
  await mcp?.close();
@@ -6674,13 +8607,16 @@ async function openHost(options) {
6674
8607
  options.modelAuth?.close();
6675
8608
  await Promise.all([...environments.values()].map((env) => env.cleanup(context4)));
6676
8609
  } finally {
6677
- await release();
8610
+ await release2();
6678
8611
  }
6679
8612
  })();
6680
8613
  return closing;
6681
8614
  }
6682
8615
  };
6683
8616
  } catch (error) {
8617
+ upgrader?.close();
8618
+ if (processes !== undefined)
8619
+ await processes.close(context4).catch(() => {});
6684
8620
  options.modelAuth?.close();
6685
8621
  attachments?.close();
6686
8622
  agentRoles?.close();
@@ -6688,18 +8624,18 @@ async function openHost(options) {
6688
8624
  await each.close?.().catch(() => {});
6689
8625
  await mcp?.close().catch(() => {});
6690
8626
  await harness?.close(context4).catch(() => {});
6691
- await release().catch(() => {});
8627
+ await release2().catch(() => {});
6692
8628
  throw error;
6693
8629
  }
6694
8630
  }
6695
8631
 
6696
8632
  // apps/host/src/service/log-rotation.ts
6697
8633
  import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
6698
- import { join as join11 } from "node:path";
8634
+ import { join as join12 } from "node:path";
6699
8635
  var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
6700
8636
  var HOST_LOG_HISTORY = 3;
6701
8637
  var HOST_LOG_CHECK_INTERVAL_MS = 10 * 60 * 1000;
6702
- var hostLogPath = (dataDir) => join11(dataDir, "logs", "host.log");
8638
+ var hostLogPath = (dataDir) => join12(dataDir, "logs", "host.log");
6703
8639
  var rotationTempPath = (log) => `${log}.rotating`;
6704
8640
  var isMissing = (error) => error.code === "ENOENT";
6705
8641
  async function rotateHostLog(dataDir, copy = copyFile) {
@@ -6742,8 +8678,8 @@ function startHostLogRotation(dataDir) {
6742
8678
  return;
6743
8679
  running = true;
6744
8680
  rotateHostLog(dataDir).catch((error) => {
6745
- const message = error instanceof Error ? error.message : String(error);
6746
- 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 })}
6747
8683
  `);
6748
8684
  }).finally(() => {
6749
8685
  running = false;
@@ -6756,10 +8692,10 @@ function startHostLogRotation(dataDir) {
6756
8692
  }
6757
8693
 
6758
8694
  // apps/host/src/model-auth.ts
6759
- import { randomUUID as randomUUID3 } from "node:crypto";
8695
+ import { randomUUID as randomUUID5 } from "node:crypto";
6760
8696
  import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
6761
8697
  import { dirname as dirname3 } from "node:path";
6762
- import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
8698
+ import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
6763
8699
 
6764
8700
  // apps/host/src/pi-setup.ts
6765
8701
  import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
@@ -6874,11 +8810,22 @@ function modelSummaries(modelRuntime) {
6874
8810
  }
6875
8811
 
6876
8812
  // apps/host/src/model-auth.ts
6877
- var BUILTIN = new Set(getBuiltinProviders());
8813
+ var BUILTIN_PROVIDERS = getBuiltinProviders();
8814
+ var BUILTIN = new Set(BUILTIN_PROVIDERS);
6878
8815
  var CUSTOM_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
6879
8816
  var LOGIN_TIMEOUT_MS = 15 * 60000;
6880
8817
  var FINISHED_LOGIN_TTL_MS = 60000;
6881
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
+ }
6882
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);
6883
8830
  var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
6884
8831
  async function readModelsFile(path) {
@@ -7099,7 +9046,7 @@ async function openModelAuth(runtime, options) {
7099
9046
  if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
7100
9047
  throw new Error(`A login for ${provider.name} is already in progress`);
7101
9048
  }
7102
- const id = randomUUID3();
9049
+ const id = randomUUID5();
7103
9050
  const login = {
7104
9051
  flow: { id, providerId, method, events: [], state: "running" },
7105
9052
  controller: new AbortController,
@@ -7264,21 +9211,35 @@ async function fetchEndpointModels(api, baseUrl, key) {
7264
9211
  if (!response.ok)
7265
9212
  throw new Error(`${url.host} answered ${response.status} for /models`);
7266
9213
  const body = await response.json();
7267
- const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) => typeof model.name === "string" ? [
7268
- endpointModel(model.name.replace(/^models\//, ""), {
7269
- contextWindow: positive(model.inputTokenLimit),
7270
- maxTokens: positive(model.outputTokenLimit),
7271
- reasoning: model.thinking === true,
7272
- image: false
7273
- })
7274
- ] : []) : (body.data ?? []).flatMap((model) => typeof model.id === "string" ? [
7275
- endpointModel(model.id, {
7276
- contextWindow: positive(model.context_window) ?? positive(model.context_length) ?? positive(model.max_input_tokens),
7277
- maxTokens: positive(model.max_output_tokens),
7278
- reasoning: model.reasoning === true,
7279
- image: Array.isArray(model.modalities?.input) && model.modalities.input.includes("image")
7280
- })
7281
- ] : []);
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
+ });
7282
9243
  if (models.length === 0)
7283
9244
  throw new Error(`${url.host} listed no models`);
7284
9245
  return models;
@@ -7286,12 +9247,62 @@ async function fetchEndpointModels(api, baseUrl, key) {
7286
9247
  var LIMITS = ["contextWindow", "maxTokens"];
7287
9248
  var lacksLimits = (model) => typeof model === "object" && model !== null && LIMITS.some((field) => !(field in model));
7288
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
+ }
7289
9299
  function endpointModel(id, reported) {
7290
9300
  return {
7291
9301
  id,
7292
9302
  ...reported.contextWindow === undefined ? {} : { contextWindow: reported.contextWindow },
7293
9303
  ...reported.maxTokens === undefined ? {} : { maxTokens: reported.maxTokens },
7294
9304
  ...reported.reasoning ? { reasoning: true } : {},
9305
+ ...reported.thinkingLevelMap === undefined ? {} : { thinkingLevelMap: reported.thinkingLevelMap },
7295
9306
  ...reported.image ? { input: ["text", "image"] } : {}
7296
9307
  };
7297
9308
  }
@@ -7326,29 +9337,40 @@ async function main(argv) {
7326
9337
  }
7327
9338
  if (relayOrigin !== undefined)
7328
9339
  validateRelayOrigin(relayOrigin);
7329
- const agentsHome = join12(homedir3(), ".agents");
7330
- const extensionsFor = (models, summaries, options2) => (host2) => [
7331
- createContext({
7332
- agentsHome,
7333
- checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
7334
- }),
7335
- createCoding(options2),
7336
- todo,
7337
- question,
7338
- createSubagent({
7339
- models,
7340
- modelSummaries: summaries,
7341
- exclude: [question.extension],
7342
- roles: () => host2.agentRoles.value.roles
7343
- }),
7344
- createCodemode()
7345
- ];
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
+ };
7346
9368
  const common = {
7347
9369
  dataDir,
7348
9370
  agentsHome,
7349
9371
  projects: values.project ?? [],
7350
9372
  port: Number(values.port),
7351
- mcpConfig: join12(agentsHome, "mcp.json"),
9373
+ mcpConfig: join13(agentsHome, "mcp.json"),
7352
9374
  browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
7353
9375
  webRoot,
7354
9376
  ...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
@@ -7391,10 +9413,10 @@ async function main(argv) {
7391
9413
  };
7392
9414
  }
7393
9415
  async function piOptions() {
7394
- const modelsPath = join12(dataDir, "models.json");
7395
- 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 });
7396
9418
  const modelAuth = await openModelAuth(modelRuntime, { modelsPath, installId: await installId(dataDir) });
7397
- const settingsManager = SettingsManager.inMemory(await piSettings(join12(dataDir, "settings.json")));
9419
+ const settingsManager = SettingsManager.inMemory(await piSettings(join13(dataDir, "settings.json")));
7398
9420
  configureHarnessHttp(settingsManager);
7399
9421
  const summaries = () => modelAuth.models.value;
7400
9422
  const images = await createNodePhotonImages();
@@ -7419,7 +9441,11 @@ async function main(argv) {
7419
9441
  };
7420
9442
  }
7421
9443
  const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
7422
- 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
+ });
7423
9449
  const stopLogRotation = startHostLogRotation(dataDir);
7424
9450
  const { defaultModel: effectiveModel } = host.settings.value;
7425
9451
  const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
@@ -7427,7 +9453,7 @@ async function main(argv) {
7427
9453
  console.log(JSON.stringify({
7428
9454
  event: "ready",
7429
9455
  url: host.url,
7430
- tokenFile: join12(dataDir, "token"),
9456
+ tokenFile: join13(dataDir, "token"),
7431
9457
  dataDir,
7432
9458
  model,
7433
9459
  web,
@@ -7435,15 +9461,19 @@ async function main(argv) {
7435
9461
  ...host.relay === undefined ? {} : { relay: { origin: host.relay.origin, hostId: host.relay.hostId } }
7436
9462
  }));
7437
9463
  let stopping = false;
7438
- const stop = () => {
9464
+ stop = (code) => {
7439
9465
  if (stopping)
7440
9466
  return;
7441
9467
  stopping = true;
7442
9468
  stopLogRotation();
7443
- 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));
7444
9474
  };
7445
- process.on("SIGINT", stop);
7446
- process.on("SIGTERM", stop);
9475
+ process.on("SIGINT", () => stop(0));
9476
+ process.on("SIGTERM", () => stop(0));
7447
9477
  host.closed.then((end) => {
7448
9478
  if (end.kind !== "failed" || stopping)
7449
9479
  return;
@@ -7473,10 +9503,10 @@ async function piSettings(path) {
7473
9503
  return typeof block === "object" && block !== null && !Array.isArray(block) ? block : {};
7474
9504
  }
7475
9505
  async function installId(dataDir) {
7476
- const path = join12(dataDir, "install-id");
9506
+ const path = join13(dataDir, "install-id");
7477
9507
  let id = (await readFile7(path, "utf8").catch(() => "")).trim();
7478
9508
  if (id === "") {
7479
- id = randomUUID4();
9509
+ id = randomUUID6();
7480
9510
  await mkdir6(dataDir, { recursive: true });
7481
9511
  await writeFile5(path, `${id}
7482
9512
  `, { mode: 384 });