pi-agent-replicate 0.0.0-stage → 1.4.1

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 (82) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +152 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +68 -2
  6. package/RELEASING.md +132 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +23 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/configuration.md +14 -0
  15. package/docs/development.md +27 -0
  16. package/docs/public-subagent-tree-api.md +58 -0
  17. package/docs/retained-roles.md +71 -0
  18. package/docs/settled-lifecycle-contract.md +71 -0
  19. package/docs/worktree-subagents.md +58 -0
  20. package/examples/role-pack/extension.ts +18 -0
  21. package/examples/role-pack/package.json +16 -0
  22. package/examples/role-pack/roles/example-reviewer.md +12 -0
  23. package/index.d.ts +116 -0
  24. package/index.js +1064 -0
  25. package/index.ts +155 -0
  26. package/package.json +70 -4
  27. package/pi-extension/subagents/activity.js +575 -0
  28. package/pi-extension/subagents/activity.ts +980 -0
  29. package/pi-extension/subagents/completion.js +184 -0
  30. package/pi-extension/subagents/completion.ts +229 -0
  31. package/pi-extension/subagents/delivery-drain.js +15 -0
  32. package/pi-extension/subagents/delivery-drain.ts +20 -0
  33. package/pi-extension/subagents/delivery-files.js +86 -0
  34. package/pi-extension/subagents/delivery-files.ts +105 -0
  35. package/pi-extension/subagents/delivery-log.js +51 -0
  36. package/pi-extension/subagents/delivery-log.ts +51 -0
  37. package/pi-extension/subagents/delivery-payload.ts +55 -0
  38. package/pi-extension/subagents/delivery.ts +219 -0
  39. package/pi-extension/subagents/execution-owner.js +30 -0
  40. package/pi-extension/subagents/execution-owner.ts +38 -0
  41. package/pi-extension/subagents/herdr.js +645 -0
  42. package/pi-extension/subagents/herdr.ts +927 -0
  43. package/pi-extension/subagents/index.ts +3400 -0
  44. package/pi-extension/subagents/launch.js +774 -0
  45. package/pi-extension/subagents/launch.ts +1124 -0
  46. package/pi-extension/subagents/lifecycle.js +477 -0
  47. package/pi-extension/subagents/lifecycle.ts +582 -0
  48. package/pi-extension/subagents/lineage.js +806 -0
  49. package/pi-extension/subagents/lineage.ts +841 -0
  50. package/pi-extension/subagents/replica-background.js +32 -0
  51. package/pi-extension/subagents/replica-background.ts +38 -0
  52. package/pi-extension/subagents/replica-branch.ts +45 -0
  53. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  54. package/pi-extension/subagents/replica-history.js +22 -0
  55. package/pi-extension/subagents/replica-history.ts +21 -0
  56. package/pi-extension/subagents/replica-work.js +326 -0
  57. package/pi-extension/subagents/replica-work.ts +336 -0
  58. package/pi-extension/subagents/replica-worker.js +74 -0
  59. package/pi-extension/subagents/replica-worker.ts +71 -0
  60. package/pi-extension/subagents/replication.ts +99 -0
  61. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  62. package/pi-extension/subagents/role-catalog.js +123 -0
  63. package/pi-extension/subagents/role-catalog.ts +24 -0
  64. package/pi-extension/subagents/runtime-routing.js +154 -0
  65. package/pi-extension/subagents/runtime-routing.ts +239 -0
  66. package/pi-extension/subagents/session-binding.js +346 -0
  67. package/pi-extension/subagents/session-binding.ts +339 -0
  68. package/pi-extension/subagents/session-work.js +219 -0
  69. package/pi-extension/subagents/session-work.ts +167 -0
  70. package/pi-extension/subagents/session.js +318 -0
  71. package/pi-extension/subagents/session.ts +448 -0
  72. package/pi-extension/subagents/settled-contract.js +47 -0
  73. package/pi-extension/subagents/settled-contract.ts +175 -0
  74. package/pi-extension/subagents/source-state.js +180 -0
  75. package/pi-extension/subagents/source-state.ts +184 -0
  76. package/pi-extension/subagents/status.ts +493 -0
  77. package/pi-extension/subagents/subagent-done.js +805 -0
  78. package/pi-extension/subagents/subagent-done.ts +878 -0
  79. package/pi-extension/subagents/terminal.js +111 -0
  80. package/pi-extension/subagents/terminal.ts +208 -0
  81. package/pi-extension/subagents/watchdog.js +122 -0
  82. package/pi-extension/subagents/watchdog.ts +171 -0
package/index.js ADDED
@@ -0,0 +1,1064 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { discoverAgentCatalog, effectiveRoleTools, resolveAgent } from "./pi-extension/subagents/role-catalog.js";
5
+ import { resolveRuntimePlan, wrapPiModelRegistry } from "./pi-extension/subagents/runtime-routing.js";
6
+ const TREE_REGISTRY = Symbol.for("pi-lpt-herdr-subagents/public-tree/v2");
7
+ const PROCESS_ID = Symbol.for("pi-lpt-herdr-subagents/public-tree/process");
8
+ const MAX_METADATA = 64 * 1024;
9
+ const MAX_ERROR = 4000;
10
+ const MAX_SESSION = 10000;
11
+ const CANCEL_TIMEOUT_MS = 10000;
12
+ export class SubagentTreeError extends Error {
13
+ constructor(message){
14
+ super(message);
15
+ this.name = "SubagentTreeError";
16
+ }
17
+ }
18
+ export class SubagentTreeValidationError extends SubagentTreeError {
19
+ constructor(message){
20
+ super(message);
21
+ this.name = "SubagentTreeValidationError";
22
+ }
23
+ }
24
+ export class SubagentTreeOwnershipError extends SubagentTreeError {
25
+ constructor(message){
26
+ super(message);
27
+ this.name = "SubagentTreeOwnershipError";
28
+ }
29
+ }
30
+ function processIdentity() {
31
+ const g = globalThis;
32
+ return g[PROCESS_ID] ??= `${process.pid}-${randomUUID()}`;
33
+ }
34
+ function registry() {
35
+ const g = globalThis;
36
+ return g[TREE_REGISTRY] ??= new Map();
37
+ }
38
+ const stateKey = (tree, owner)=>`${tree}:${owner}`;
39
+ const readJson = (path)=>{
40
+ try {
41
+ return JSON.parse(readFileSync(path, "utf8"));
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ };
46
+ function atomic(path, value) {
47
+ mkdirSync(dirname(path), {
48
+ recursive: true
49
+ });
50
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
51
+ writeFileSync(tmp, `${JSON.stringify(value)}\n`);
52
+ renameSync(tmp, path);
53
+ }
54
+ function exclusiveAtomic(path, value) {
55
+ mkdirSync(dirname(path), {
56
+ recursive: true
57
+ });
58
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
59
+ try {
60
+ writeFileSync(tmp, `${JSON.stringify(value)}\n`);
61
+ linkSync(tmp, path);
62
+ return true;
63
+ } catch (e) {
64
+ if (e.code === "EEXIST") return false;
65
+ throw e;
66
+ } finally{
67
+ try {
68
+ unlinkSync(tmp);
69
+ } catch {}
70
+ }
71
+ }
72
+ const bounded = (s, n)=>s.length <= n ? s : s.slice(0, n);
73
+ const message = (e)=>e instanceof Error ? e.message : String(e);
74
+ function metadata(value) {
75
+ let encoded;
76
+ try {
77
+ encoded = JSON.stringify(value);
78
+ } catch (e) {
79
+ throw new SubagentTreeValidationError(`metadata must be JSON-compatible: ${message(e)}`);
80
+ }
81
+ if (encoded === undefined) throw new SubagentTreeValidationError("metadata must be JSON-compatible");
82
+ if (Buffer.byteLength(encoded, "utf8") > MAX_METADATA) throw new SubagentTreeValidationError("metadata exceeds the 64 KiB limit");
83
+ return deepFreeze({
84
+ value: JSON.parse(encoded)
85
+ });
86
+ }
87
+ function identityMetadata(base, meta, ownerId, nodeId, parentId) {
88
+ return deepFreeze({
89
+ ...base,
90
+ callerId: meta.callerId,
91
+ treeId: meta.treeId,
92
+ ownerId,
93
+ ...nodeId ? {
94
+ nodeId
95
+ } : {},
96
+ ...parentId ? {
97
+ parentId
98
+ } : {}
99
+ });
100
+ }
101
+ function context(ctx) {
102
+ const m = ctx.sessionManager;
103
+ const file = m.getSessionFile();
104
+ if (!file) throw new SubagentTreeValidationError("No session file");
105
+ return {
106
+ sessionFile: file,
107
+ sessionId: m.getSessionId(),
108
+ sessionDir: m.getSessionDir(),
109
+ cwd: ctx.cwd ?? process.cwd(),
110
+ agentDir: process.env.PI_CODING_AGENT_DIR
111
+ };
112
+ }
113
+ function ownerSession(meta, owner) {
114
+ if (owner === meta.callerId) return meta.boundSessionFile;
115
+ return readJson(join(meta.treeDir, "nodes", `${owner}.json`))?.sessionFile;
116
+ }
117
+ function header(path) {
118
+ try {
119
+ const x = JSON.parse(readFileSync(path, "utf8").split("\n", 1)[0]);
120
+ return typeof x.parentSession === "string" ? x.parentSession : undefined;
121
+ } catch {
122
+ return undefined;
123
+ }
124
+ }
125
+ function findTreeMeta(sessionDir, treeId) {
126
+ const root = join(sessionDir, "artifacts");
127
+ if (!existsSync(root)) return;
128
+ for (const sid of readdirSync(root)){
129
+ const p = join(root, sid, "subagent-trees", treeId, "tree.json"), m = readJson(p);
130
+ if (m) return m;
131
+ }
132
+ return;
133
+ }
134
+ function validateLineage(meta, owner, ctx, lineage, bound) {
135
+ const fresh = context(ctx).sessionFile;
136
+ if (lineage === undefined) return fresh;
137
+ if (typeof lineage.previousSessionFile !== "string" || !lineage.previousSessionFile) throw new SubagentTreeOwnershipError("fork lineage previousSessionFile is required");
138
+ const prior = bound ?? ownerSession(meta, owner);
139
+ if (!prior || prior !== lineage.previousSessionFile) throw new SubagentTreeOwnershipError("fork lineage does not match the currently bound session");
140
+ if (header(fresh) !== lineage.previousSessionFile) throw new SubagentTreeOwnershipError("fork session header does not match the previous session");
141
+ return fresh;
142
+ }
143
+ function processAlive(pid) {
144
+ try {
145
+ process.kill(pid, 0);
146
+ return true;
147
+ } catch (error) {
148
+ return error.code !== "ESRCH";
149
+ }
150
+ }
151
+ function reclaimDeadClaim(path, owner) {
152
+ for(;;){
153
+ if (exclusiveAtomic(path, owner)) return true;
154
+ const old = readJson(path);
155
+ if (old?.process === owner.process) return false;
156
+ if (typeof old?.pid === "number" && processAlive(old.pid)) return false;
157
+ let before;
158
+ try {
159
+ const st = statSync(path);
160
+ before = {
161
+ dev: st.dev,
162
+ ino: st.ino
163
+ };
164
+ } catch {
165
+ continue;
166
+ }
167
+ const moved = `${path}.${processIdentity()}.${randomUUID()}.stale`;
168
+ try {
169
+ renameSync(path, moved);
170
+ } catch {
171
+ continue;
172
+ }
173
+ let same = false;
174
+ try {
175
+ const st = statSync(moved);
176
+ same = st.dev === before.dev && st.ino === before.ino;
177
+ } catch {}
178
+ if (!same) {
179
+ try {
180
+ if (!existsSync(path)) renameSync(moved, path);
181
+ } catch {}
182
+ continue;
183
+ }
184
+ try {
185
+ unlinkSync(moved);
186
+ } catch {}
187
+ }
188
+ }
189
+ function claimBranch(meta, owner) {
190
+ const p = join(meta.treeDir, "branches", `${owner}.claim`), identity = {
191
+ process: processIdentity(),
192
+ pid: process.pid,
193
+ owner,
194
+ token: randomUUID()
195
+ };
196
+ if (reclaimDeadClaim(p, identity)) return;
197
+ throw new SubagentTreeOwnershipError("subagent branch is owned by another process");
198
+ }
199
+ function claimRoot(s) {
200
+ const p = join(s.meta.treeDir, "root.claim"), identity = {
201
+ process: processIdentity(),
202
+ pid: process.pid,
203
+ token: randomUUID()
204
+ };
205
+ if (exclusiveAtomic(p, identity)) return;
206
+ const old = readJson(p);
207
+ if (old?.process === identity.process) throw new SubagentTreeOwnershipError("subagent tree already has a direct root");
208
+ if (readdirSync(join(s.meta.treeDir, "nodes")).some((x)=>x.endsWith(".json"))) throw new SubagentTreeOwnershipError("subagent tree already has a direct root");
209
+ if (reclaimDeadClaim(p, identity)) return;
210
+ throw new SubagentTreeOwnershipError("subagent tree already has a direct root");
211
+ }
212
+ function deferred() {
213
+ let resolve;
214
+ const promise = new Promise((r)=>{
215
+ resolve = r;
216
+ });
217
+ return {
218
+ promise,
219
+ resolve
220
+ };
221
+ }
222
+ function nodePath(s, id) {
223
+ return join(s.meta.treeDir, "nodes", `${id}.json`);
224
+ }
225
+ function inflightPath(s, id) {
226
+ return join(s.meta.treeDir, "branches", `${s.ownerId}.${id}.inflight.json`);
227
+ }
228
+ function clearInflight(s, id, token) {
229
+ const p = inflightPath(s, id), record = readJson(p);
230
+ if (record?.token === token) try {
231
+ unlinkSync(p);
232
+ } catch {}
233
+ }
234
+ function inflightRecords(s) {
235
+ const dir = join(s.meta.treeDir, "branches");
236
+ if (!existsSync(dir)) return [];
237
+ return readdirSync(dir).filter((x)=>x.endsWith(".inflight.json")).flatMap((x)=>{
238
+ const path = join(dir, x), r = readJson(path);
239
+ return r ? [
240
+ {
241
+ path,
242
+ ...r
243
+ }
244
+ ] : [
245
+ {
246
+ path
247
+ }
248
+ ];
249
+ });
250
+ }
251
+ function writeNode(s, node) {
252
+ atomic(nodePath(s, node.nodeId), node);
253
+ }
254
+ function terminalIntentPath(s) {
255
+ return join(s.meta.treeDir, "terminal-intent.json");
256
+ }
257
+ function claimTerminalIntent(s, kind) {
258
+ return exclusiveAtomic(terminalIntentPath(s), {
259
+ kind,
260
+ ownerId: s.ownerId,
261
+ process: processIdentity(),
262
+ terminalId: randomUUID(),
263
+ at: Date.now()
264
+ });
265
+ }
266
+ function currentTerminalIntent(s) {
267
+ return readJson(terminalIntentPath(s))?.kind;
268
+ }
269
+ function branchNodes(s) {
270
+ const d = join(s.meta.treeDir, "nodes");
271
+ if (!existsSync(d)) return [];
272
+ return readdirSync(d).filter((x)=>x.endsWith(".json")).flatMap((f)=>{
273
+ const n = readJson(join(d, f));
274
+ return n?.ownerId === s.ownerId ? [
275
+ n
276
+ ] : [];
277
+ });
278
+ }
279
+ function allNodes(s) {
280
+ const d = join(s.meta.treeDir, "nodes");
281
+ if (!existsSync(d)) return [];
282
+ return readdirSync(d).filter((x)=>x.endsWith(".json")).flatMap((f)=>{
283
+ const n = readJson(join(d, f));
284
+ return n ? [
285
+ n
286
+ ] : [];
287
+ });
288
+ }
289
+ function pendingInflightRecords(s, ownerId) {
290
+ const nodes = allNodes(s);
291
+ return inflightRecords(s).filter((r)=>{
292
+ if (ownerId !== undefined && r.ownerId !== ownerId) return false;
293
+ const node = r.nodeId ? nodes.find((n)=>n.nodeId === r.nodeId) : undefined;
294
+ return !node || node.ownerId !== r.ownerId || !node.surface;
295
+ });
296
+ }
297
+ function pendingBranchEvidence(s, ownerId) {
298
+ const nodes = allNodes(s), inflight = pendingInflightRecords(s, ownerId).length, queued = nodes.filter((n)=>n.ownerId === ownerId && n.status === "queued" && !existsSync(n.resultPath)).length;
299
+ return { inflight, queued };
300
+ }
301
+ async function waitBranchTransactions(s) {
302
+ const deadline = Date.now() + CANCEL_TIMEOUT_MS;
303
+ while(s.launchTransactions.size && Date.now() < deadline)await new Promise((r)=>setTimeout(r, 25));
304
+ return s.launchTransactions.size === 0;
305
+ }
306
+ async function cancelBranch(s) {
307
+ if (s.branchCancellation) return s.branchCancellation;
308
+ s.cancellationRequested = true;
309
+ s.branchCancellation = (async () => {
310
+ const mux = await import("./pi-extension/subagents/terminal.js");
311
+ for (const controller of s.launchControllers) controller.abort();
312
+ const transactionsSettled = await waitBranchTransactions(s), pids = new Set(), nodePids = new Map();
313
+ for (const n of branchNodes(s)) {
314
+ if (n.open === false) continue;
315
+ if (!n.surface) {
316
+ if (n.status !== "settled") {
317
+ const result = writeResult(s, n, failedResult("tree_cancelled", "queued child cancelled", n.metadata));
318
+ s.pending.get(n.nodeId)?.resolve(result);
319
+ }
320
+ continue;
321
+ }
322
+ if (n.status !== "settled") try {
323
+ const info = mux.getPaneProcessInfo(n.surface), captured = info.pids.filter((pid) => Number.isInteger(pid) && pid > 0);
324
+ for (const pid of captured) pids.add(pid);
325
+ if (captured.length) nodePids.set(n.nodeId, captured);
326
+ } catch { }
327
+ try { mux.closePane(n.surface); } catch { }
328
+ try { n.open = !(await mux.waitForPaneAbsence(n.surface, { timeoutMs: 5000 })); } catch (e) { n.open = !/pane_not_found|pane not found/i.test(message(e)); }
329
+ if (n.status !== "settled") {
330
+ const result = writeResult(s, n, failedResult("tree_cancelled", "running child cancelled", n.metadata));
331
+ s.pending.get(n.nodeId)?.resolve(result);
332
+ } else writeNode(s, n);
333
+ }
334
+ let remaining = [];
335
+ let processCheckConfirmed = true;
336
+ try { remaining = await mux.waitForProcessesExit([...pids], { timeoutMs: 5000 }); } catch { processCheckConfirmed = false; }
337
+ const inflight = pendingInflightRecords(s, s.ownerId), ack = { ownerId: s.ownerId, process: processIdentity(), pid: process.pid, at: Date.now(), transactionsSettled, inflight: inflight.length, remainingPids: remaining, nodes: branchNodes(s).map((n) => ({ nodeId: n.nodeId, open: n.open ?? false })) };
338
+ if (!existsSync(join(s.meta.treeDir, "branches", `${s.ownerId}.cancelled.json`))) atomic(join(s.meta.treeDir, "branches", `${s.ownerId}.cancelled.json`), ack);
339
+ if (s.ownerId === s.meta.callerId && processCheckConfirmed && !remaining.length) for (const n of branchNodes(s)) {
340
+ // The root may close a child branch before it can publish its own acknowledgement.
341
+ // Captured process exit plus pane absence is equivalent termination evidence.
342
+ const captured = nodePids.get(n.nodeId);
343
+ if (!captured?.length || n.open !== false || captured.some((pid) => remaining.includes(pid))) continue;
344
+ const childAckPath = join(s.meta.treeDir, "branches", `${n.nodeId}.cancelled.json`), pending = pendingBranchEvidence(s, n.nodeId);
345
+ if (!existsSync(childAckPath)) atomic(childAckPath, { ownerId: n.nodeId, process: processIdentity(), pid: process.pid, at: Date.now(), synthesized: true, transactionsSettled: pending.inflight === 0 && pending.queued === 0, inflight: pending.inflight, unresolvedQueued: pending.queued, remainingPids: [], nodes: [{ nodeId: n.nodeId, open: false }] });
346
+ }
347
+ })();
348
+ return s.branchCancellation;
349
+ }
350
+ function writeResult(s, node, result) {
351
+ const frozen = deepFreeze(result);
352
+ if (!exclusiveAtomic(node.resultPath, frozen)) {
353
+ const winner = readJson(node.resultPath);
354
+ if (winner) {
355
+ atomic(nodePath(s, node.nodeId), {
356
+ ...node,
357
+ status: "settled"
358
+ });
359
+ return deepFreeze(winner);
360
+ }
361
+ return frozen;
362
+ }
363
+ atomic(nodePath(s, node.nodeId), {
364
+ ...node,
365
+ status: "settled"
366
+ });
367
+ return frozen;
368
+ }
369
+ function isCancelled(s) {
370
+ return s.cancellationRequested || existsSync(join(s.meta.treeDir, "cancel.request"));
371
+ }
372
+ function failedResult(code, msg, meta) {
373
+ return {
374
+ resultId: randomUUID(),
375
+ assistantEntryId: randomUUID(),
376
+ answer: null,
377
+ outcome: "unexpected-abort",
378
+ error: {
379
+ code,
380
+ message: bounded(msg, MAX_ERROR)
381
+ },
382
+ ...meta ? {
383
+ metadata: meta
384
+ } : {}
385
+ };
386
+ }
387
+ function rows(s) {
388
+ const d = join(s.meta.treeDir, "nodes");
389
+ if (!existsSync(d)) return [];
390
+ return readdirSync(d).filter((x)=>x.endsWith(".json")).sort().flatMap((f)=>{
391
+ const n = readJson(join(d, f));
392
+ if (!n) return [];
393
+ const r = readJson(n.resultPath);
394
+ return [
395
+ {
396
+ nodeId: n.nodeId,
397
+ parentId: n.parentId,
398
+ outcome: r?.outcome ?? "unexpected-abort",
399
+ open: n.open ?? (r?.outcome !== "clean" && r?.outcome !== "empty"),
400
+ ...n.sessionFile ? {
401
+ sessionReference: bounded(n.sessionFile, MAX_SESSION)
402
+ } : {},
403
+ ...r?.error ? {
404
+ error: r.error
405
+ } : {}
406
+ }
407
+ ];
408
+ });
409
+ }
410
+ function rootResult(s) {
411
+ const d = join(s.meta.treeDir, "nodes");
412
+ if (!existsSync(d)) return undefined;
413
+ const roots = readdirSync(d).filter((x)=>x.endsWith(".json")).map((f)=>readJson(join(d, f))).filter((n)=>Boolean(n && n.parentId === s.meta.callerId));
414
+ if (roots.length !== 1) return undefined;
415
+ return readJson(roots[0].resultPath);
416
+ }
417
+ async function authoritative(s, candidate) {
418
+ const terminalPath = join(s.meta.treeDir, "terminal.json");
419
+ if (exclusiveAtomic(terminalPath, candidate)) return candidate;
420
+ for(;;){
421
+ const record = readJson(terminalPath);
422
+ if (record) return record;
423
+ await new Promise((r)=>setTimeout(r, 10));
424
+ }
425
+ }
426
+ function adopt(s, r) {
427
+ if (s.terminal) return;
428
+ s.terminal = deepFreeze(r);
429
+ s.resolveTree(s.terminal);
430
+ void deliver(s, s.terminal);
431
+ }
432
+ async function finish(s, r) {
433
+ adopt(s, await authoritative(s, r));
434
+ }
435
+ async function closeCleanPanes(s) {
436
+ const terminal = await import("./pi-extension/subagents/terminal.js"), dir = join(s.meta.treeDir, "nodes");
437
+ for (const f of readdirSync(dir).filter((x)=>x.endsWith(".json"))){
438
+ const n = readJson(join(dir, f)), r = n ? readJson(n.resultPath) : undefined;
439
+ if (!n || !r || !n.surface || r.worktree || r.outcome !== "clean" && r.outcome !== "empty" || n.open === false) continue;
440
+ try {
441
+ terminal.closePane(n.surface);
442
+ n.open = !await terminal.waitForPaneAbsence(n.surface, {
443
+ timeoutMs: 5000
444
+ });
445
+ writeNode(s, n);
446
+ } catch {
447
+ n.open = true;
448
+ writeNode(s, n);
449
+ }
450
+ }
451
+ }
452
+ async function maybeFinish(s) {
453
+ if (s.terminal || s.ownerId !== s.meta.callerId) return;
454
+ const existing = readJson(join(s.meta.treeDir, "terminal.json"));
455
+ if (existing) {
456
+ adopt(s, existing);
457
+ return;
458
+ }
459
+ if (isCancelled(s) || currentTerminalIntent(s) === "cancelled") return;
460
+ const root = rootResult(s);
461
+ if (!root) return;
462
+ let all = rows(s);
463
+ if (all.some((r)=>!readJson(join(s.meta.treeDir, "results", `${r.nodeId}.json`)))) return;
464
+ if (!claimTerminalIntent(s, "completed")) {
465
+ if (currentTerminalIntent(s) !== "completed") return;
466
+ return;
467
+ }
468
+ await closeCleanPanes(s);
469
+ all = rows(s);
470
+ const state = root.outcome === "clean" || root.outcome === "empty" ? "completed" : "failed";
471
+ await finish(s, {
472
+ terminalId: randomUUID(),
473
+ state,
474
+ rootResult: root,
475
+ callerMetadata: s.meta.callerMetadata,
476
+ nodes: all
477
+ });
478
+ }
479
+ async function deliverAttempt(s, r) {
480
+ if (!s.callback || existsSync(join(s.meta.treeDir, "delivery.done"))) return;
481
+ const claimPath = join(s.meta.treeDir, "delivery.claim"), claim = {
482
+ process: processIdentity(),
483
+ pid: process.pid,
484
+ token: randomUUID(),
485
+ terminalId: r.terminalId,
486
+ version: s.callbackVersion
487
+ };
488
+ if (!reclaimDeadClaim(claimPath, claim)) return;
489
+ const callback = s.callback;
490
+ try {
491
+ if (!callback) return;
492
+ await callback(r);
493
+ if (!existsSync(join(s.meta.treeDir, "delivery.done"))) writeFileSync(join(s.meta.treeDir, "delivery.done"), `${r.terminalId}\n`, {
494
+ flag: "wx"
495
+ });
496
+ } catch {} finally{
497
+ const held = readJson(claimPath);
498
+ if (!existsSync(join(s.meta.treeDir, "delivery.done")) && held?.token === claim.token) try {
499
+ unlinkSync(claimPath);
500
+ } catch {}
501
+ }
502
+ }
503
+ async function deliver(s, r) {
504
+ const attempt = s.deliverySerial.then(()=>deliverAttempt(s, r));
505
+ s.deliverySerial = attempt.catch(()=>{});
506
+ return attempt;
507
+ }
508
+ async function watch(s, node, running, pending) {
509
+ const [{ waitForCompletion }, terminal, session, activity, launchModule] = await Promise.all([
510
+ import("./pi-extension/subagents/completion.js"),
511
+ import("./pi-extension/subagents/terminal.js"),
512
+ import("./pi-extension/subagents/session.js"),
513
+ import("./pi-extension/subagents/activity.js"),
514
+ import("./pi-extension/subagents/launch.js")
515
+ ]);
516
+ const abort = new AbortController();
517
+ try {
518
+ const completion = await Promise.race([
519
+ waitForCompletion(abort.signal, {
520
+ intervalMs: 100,
521
+ sessionFile: running.sessionFile,
522
+ readTerminalTail: ()=>terminal.readPaneAsync(running.surface, 5),
523
+ inspectPane: ()=>terminal.inspectPane(running.surface)
524
+ }),
525
+ (async ()=>{
526
+ for(;;){
527
+ if (isCancelled(s)) throw new Error("tree cancellation requested");
528
+ const e = activity.readSubagentSettledEventsFile(running.settledEventsFile, running.id).at(-1);
529
+ if (e?.outcome === "error") return {
530
+ reason: "error",
531
+ exitCode: 1,
532
+ errorMessage: e.errorMessage
533
+ };
534
+ if (e?.outcome === "clean" || e?.outcome === "empty") return {
535
+ reason: "done",
536
+ exitCode: 0
537
+ };
538
+ await new Promise((r)=>setTimeout(r, 100));
539
+ }
540
+ })()
541
+ ]);
542
+ abort.abort();
543
+ const a = session.inspectNewestAppendedAssistant(running.sessionFile, running.sessionBaseline) ?? {
544
+ id: randomUUID(),
545
+ text: null,
546
+ empty: true,
547
+ stopReason: "error",
548
+ errorMessage: "Subagent produced no settled assistant response"
549
+ };
550
+ const bad = completion.reason === "error" || a.stopReason === "error" || completion.exitCode !== 0;
551
+ if (isCancelled(s)) return;
552
+ let open = true;
553
+ try {
554
+ const inspection = await terminal.inspectPane(running.surface);
555
+ const processInfo = terminal.getPaneProcessInfo(running.surface);
556
+ open = inspection.kind !== "missing" || processInfo.pids.length > 0;
557
+ node.open = open;
558
+ } catch {
559
+ node.open = true;
560
+ }
561
+ const outcome = a.stopReason === "aborted" ? "unexpected-abort" : bad ? "error" : a.empty ? "empty" : "clean";
562
+ const result = writeResult(s, node, {
563
+ resultId: randomUUID(),
564
+ assistantEntryId: a.id,
565
+ answer: a.text,
566
+ outcome,
567
+ sessionReference: bounded(running.sessionFile, MAX_SESSION),
568
+ ...outcome === "error" || outcome === "unexpected-abort" ? {
569
+ error: {
570
+ code: a.stopReason === "error" ? "provider_error" : "runtime_error",
571
+ message: bounded(a.errorMessage ?? completion.errorMessage ?? "Subagent failed", MAX_ERROR)
572
+ }
573
+ } : {},
574
+ ...node.metadata ? {
575
+ metadata: node.metadata
576
+ } : {},
577
+ ...running.worktree ? {
578
+ worktree: launchModule.captureWorktreeHandoff(running.worktree)
579
+ } : {}
580
+ });
581
+ pending.resolve(result);
582
+ if (!isCancelled(s)) void maybeFinish(s);
583
+ } catch (e) {
584
+ if (!readJson(node.resultPath) && !isCancelled(s)) {
585
+ const result = writeResult(s, node, failedResult("runtime_error", message(e), node.metadata));
586
+ pending.resolve(result);
587
+ void maybeFinish(s);
588
+ }
589
+ }
590
+ }
591
+ async function launch(s, o) {
592
+ if (s.terminal || readJson(join(s.meta.treeDir, "terminal.json")) || isCancelled(s)) throw new SubagentTreeError("subagent tree is terminal");
593
+ if (o.parentId !== s.ownerId) throw new SubagentTreeOwnershipError("parent is not owned by this branch");
594
+ if (!o.name || !o.task) throw new SubagentTreeValidationError("child name and task are required");
595
+ if (!Array.isArray(o.tools) || !o.tools.length) throw new SubagentTreeValidationError("child tools are required");
596
+ const consumerMetadata = o.metadata === undefined ? metadata(null) : metadata(o.metadata);
597
+ const role = o.agent ? resolveAgent(o.agent, s.pi) : undefined;
598
+ if (o.agent && !role) {
599
+ const d = discoverAgentCatalog(s.pi).diagnostics.find((x)=>x.agentName === o.agent);
600
+ throw new SubagentTreeValidationError(d?.message ?? `Agent ${JSON.stringify(o.agent)} was not found`);
601
+ }
602
+ let tools;
603
+ try {
604
+ tools = effectiveRoleTools(o.tools, role);
605
+ } catch (e) {
606
+ throw new SubagentTreeValidationError(message(e));
607
+ }
608
+ if (isCancelled(s)) throw new SubagentTreeError("subagent tree is terminal");
609
+ const parent = context(s.ctx);
610
+ if (!s.ctx.model) throw new SubagentTreeValidationError("No parent model is selected");
611
+ const thinking = s.pi.getThinkingLevel();
612
+ if (typeof thinking !== "string") throw new SubagentTreeValidationError(`Unsupported thinking level: ${thinking}`);
613
+ const registryAdapter = wrapPiModelRegistry(s.ctx.modelRegistry);
614
+ const plan = resolveRuntimePlan({
615
+ model: role?.model,
616
+ thinking: role?.thinking
617
+ }, {
618
+ provider: s.ctx.model.provider,
619
+ modelId: s.ctx.model.id,
620
+ thinking
621
+ }, registryAdapter);
622
+ if (o.parentId === s.meta.callerId) claimRoot(s);
623
+ const id = randomUUID(), childMetadata = identityMetadata(consumerMetadata, s.meta, s.ownerId, id, o.parentId), node = {
624
+ nodeId: id,
625
+ parentId: o.parentId,
626
+ ownerId: s.ownerId,
627
+ name: o.name,
628
+ resultPath: join(s.meta.treeDir, "results", `${id}.json`),
629
+ status: "queued",
630
+ metadata: childMetadata
631
+ }, pending = deferred();
632
+ s.pending.set(id, pending);
633
+ s.children.set(id, Object.freeze({
634
+ nodeId: id,
635
+ parentId: o.parentId,
636
+ result: pending.promise
637
+ }));
638
+ writeNode(s, node);
639
+ const transaction = deferred(), controller = new AbortController(), inflightToken = randomUUID();
640
+ s.launchTransactions.add(transaction.promise);
641
+ s.launchControllers.add(controller);
642
+ atomic(inflightPath(s, id), {
643
+ ownerId: s.ownerId,
644
+ pid: process.pid,
645
+ process: processIdentity(),
646
+ nodeId: id,
647
+ token: inflightToken
648
+ });
649
+ const contextPath = join(s.meta.treeDir, "context", `${id}.json`);
650
+ atomic(contextPath, {
651
+ ...s.meta,
652
+ ownerId: id
653
+ });
654
+ try {
655
+ if (isCancelled(s)) {
656
+ const r = writeResult(s, node, failedResult("tree_cancelled", "subagent tree cancellation requested", childMetadata));
657
+ pending.resolve(r);
658
+ return s.children.get(id);
659
+ }
660
+ const { launchPiSubagent } = await import("./pi-extension/subagents/launch.js");
661
+ const effectiveAutoExit = role ? role.autoExit ?? false : true;
662
+ const running = await launchPiSubagent({
663
+ kind: "fresh",
664
+ id,
665
+ name: o.name,
666
+ task: o.task,
667
+ agent: o.agent,
668
+ cwd: o.cwd,
669
+ worktree: o.worktree,
670
+ parent: {
671
+ cwd: parent.cwd,
672
+ invocationCwd: process.cwd(),
673
+ sessionFile: parent.sessionFile,
674
+ sessionId: parent.sessionId,
675
+ sessionDir: parent.sessionDir,
676
+ agentDir: parent.agentDir
677
+ },
678
+ runtimePlan: plan,
679
+ environment: {
680
+ PI_SUBAGENT_TREE_CONTEXT: contextPath,
681
+ PI_SUBAGENT_TREE_OWNER: "1"
682
+ },
683
+ signal: controller.signal,
684
+ behavior: {
685
+ tools: tools.join(","),
686
+ skills: o.skills ?? role?.skills,
687
+ deniedTools: [],
688
+ autoExit: effectiveAutoExit,
689
+ interactive: role?.interactive ?? !effectiveAutoExit,
690
+ includeControlTools: false,
691
+ identity: role?.body,
692
+ systemPromptMode: role?.systemPromptMode,
693
+ sessionMode: role?.sessionMode ?? "standalone",
694
+ cwd: role?.cwd
695
+ }
696
+ });
697
+ node.surface = running.surface;
698
+ node.sessionFile = running.sessionFile;
699
+ node.status = "running";
700
+ writeNode(s, node);
701
+ if (isCancelled(s)) {
702
+ try {
703
+ const terminal = await import("./pi-extension/subagents/terminal.js");
704
+ terminal.closePane(running.surface);
705
+ node.open = !await terminal.waitForPaneAbsence(running.surface, {
706
+ timeoutMs: 5000
707
+ });
708
+ } catch {
709
+ node.open = true;
710
+ }
711
+ const r = writeResult(s, node, failedResult("tree_cancelled", "subagent tree cancellation requested", childMetadata));
712
+ pending.resolve(r);
713
+ return s.children.get(id);
714
+ }
715
+ void watch(s, node, running, pending);
716
+ } catch (e) {
717
+ const r = writeResult(s, node, failedResult(isCancelled(s) ? "tree_cancelled" : "launch_failed", message(e), childMetadata));
718
+ pending.resolve(r);
719
+ maybeFinish(s);
720
+ return s.children.get(id);
721
+ } finally{
722
+ clearInflight(s, id, inflightToken);
723
+ transaction.resolve();
724
+ s.launchTransactions.delete(transaction.promise);
725
+ s.launchControllers.delete(controller);
726
+ }
727
+ return s.children.get(id);
728
+ }
729
+ async function waitTerminal(s) {
730
+ for(;;){
731
+ const r = readJson(join(s.meta.treeDir, "terminal.json"));
732
+ if (r) {
733
+ adopt(s, r);
734
+ return r;
735
+ }
736
+ await new Promise((r)=>setTimeout(r, 25));
737
+ }
738
+ }
739
+ async function waitBranchAcks(s, failures, includeRoot = true) {
740
+ const owners = new Set();
741
+ if (includeRoot) owners.add(s.meta.callerId);
742
+ for (const f of readdirSync(join(s.meta.treeDir, "nodes")).filter((x)=>x.endsWith(".json"))){
743
+ const n = readJson(join(s.meta.treeDir, "nodes", f));
744
+ if (n) owners.add(n.ownerId);
745
+ }
746
+ if (!includeRoot) owners.delete(s.meta.callerId);
747
+ const inspect = ()=>{
748
+ for (const owner of owners){
749
+ const ack = readJson(join(s.meta.treeDir, "branches", `${owner}.cancelled.json`));
750
+ if (!ack) {
751
+ failures.push(`branch cancellation acknowledgement missing: ${owner}`);
752
+ continue;
753
+ }
754
+ const pending = pendingBranchEvidence(s, owner);
755
+ if (ack.transactionsSettled === false) failures.push(`branch cancellation transaction did not settle: ${owner}`);
756
+ if ((ack.inflight ?? 0) > 0 || pending.inflight > 0) failures.push(`branch cancellation reservation remains: ${owner}`);
757
+ if ((ack.unresolvedQueued ?? 0) > 0 || pending.queued > 0) failures.push(`branch cancellation queued launch remains: ${owner}`);
758
+ if (ack.synthesized && (pending.inflight > 0 || pending.queued > 0)) failures.push(`synthesized branch cancellation evidence is incomplete: ${owner}`);
759
+ if ((ack.remainingPids ?? []).length) failures.push(`branch processes remain: ${(ack.remainingPids ?? []).join(",")}`);
760
+ }
761
+ };
762
+ const deadline = Date.now() + CANCEL_TIMEOUT_MS;
763
+ while(Date.now() < deadline){
764
+ const missing = [...owners].some((owner)=>!existsSync(join(s.meta.treeDir, "branches", `${owner}.cancelled.json`)));
765
+ if (!missing) {
766
+ inspect();
767
+ return;
768
+ }
769
+ await new Promise((r)=>setTimeout(r, 25));
770
+ }
771
+ inspect();
772
+ }
773
+ async function cancelWork(s) {
774
+ const mux = await import("./pi-extension/subagents/terminal.js");
775
+ atomic(join(s.meta.treeDir, "cancel.request"), {
776
+ process: processIdentity(),
777
+ at: Date.now()
778
+ });
779
+ const failures = [], pids = new Set();
780
+ await cancelBranch(s);
781
+ await waitBranchAcks(s, failures);
782
+ const deadline = Date.now() + CANCEL_TIMEOUT_MS;
783
+ for (const row of rows(s)){
784
+ const n = readJson(join(s.meta.treeDir, "nodes", `${row.nodeId}.json`));
785
+ if (!n || n.open === false) continue;
786
+ if (!n.surface) {
787
+ if (n.status === "running") {
788
+ failures.push(`missing pane identity for ${n.nodeId}`);
789
+ n.open = true;
790
+ } else n.open = false;
791
+ const result = writeResult(s, n, failedResult("tree_cancelled", n.status === "running" ? "running child lost its pane" : "queued child cancelled", n.metadata));
792
+ s.pending.get(n.nodeId)?.resolve(result);
793
+ continue;
794
+ }
795
+ try {
796
+ const info = mux.getPaneProcessInfo(n.surface);
797
+ if (!info.pids.length && n.status !== "settled") failures.push(`missing process identity for ${n.nodeId}`);
798
+ for (const pid of info.pids)pids.add(pid);
799
+ } catch (e) {
800
+ if (!/pane_not_found|pane not found/i.test(message(e))) failures.push(message(e));
801
+ }
802
+ try {
803
+ mux.closePane(n.surface);
804
+ } catch {}
805
+ }
806
+ for (const row of rows(s)){
807
+ const n = readJson(join(s.meta.treeDir, "nodes", `${row.nodeId}.json`));
808
+ if (!n?.surface || n.open === false) continue;
809
+ try {
810
+ let absent = false;
811
+ for(let attempt = 0; attempt < 3 && !absent; attempt++){
812
+ if (attempt > 0) try {
813
+ mux.closePane(n.surface);
814
+ } catch {}
815
+ absent = await mux.waitForPaneAbsence(n.surface, {
816
+ timeoutMs: 5000
817
+ });
818
+ }
819
+ n.open = !absent;
820
+ writeNode(s, n);
821
+ if (!absent) failures.push(`pane ${n.surface} remains`);
822
+ } catch (e) {
823
+ failures.push(message(e));
824
+ }
825
+ }
826
+ try {
827
+ const left = await mux.waitForProcessesExit([
828
+ ...pids
829
+ ], {
830
+ timeoutMs: 5000
831
+ });
832
+ if (left.length) failures.push(`processes remain: ${left.join(",")}`);
833
+ } catch (e) {
834
+ failures.push(message(e));
835
+ }
836
+ while(Date.now() < deadline && pendingInflightRecords(s).some((x)=>typeof x.pid !== "number" || processAlive(x.pid)))await new Promise((r)=>setTimeout(r, 25));
837
+ if (pendingInflightRecords(s).some((x)=>typeof x.pid !== "number" || processAlive(x.pid))) failures.push("in-flight launch reservation remains");
838
+ const error = failures.length ? {
839
+ code: "cancel_termination_failed",
840
+ message: bounded(failures.join("; "), MAX_ERROR)
841
+ } : undefined;
842
+ if (!error) for (const inFlight of inflightRecords(s)) {
843
+ if (typeof inFlight.pid !== "number" || !processAlive(inFlight.pid)) {
844
+ try {
845
+ unlinkSync(inFlight.path);
846
+ } catch {}
847
+ }
848
+ }
849
+ const terminal = {
850
+ terminalId: randomUUID(),
851
+ state: error ? "failed" : "cancelled",
852
+ callerMetadata: s.meta.callerMetadata,
853
+ rootResult: failedResult(error ? error.code : "tree_cancelled", error?.message ?? "tree cancelled"),
854
+ ...error ? {
855
+ error
856
+ } : {},
857
+ nodes: error ? rows(s).map((r)=>({
858
+ ...r,
859
+ open: true
860
+ })) : rows(s)
861
+ };
862
+ await finish(s, terminal);
863
+ return s.terminal ?? terminal;
864
+ }
865
+ function cancel(s) {
866
+ const existing = readJson(join(s.meta.treeDir, "terminal.json"));
867
+ if (existing) {
868
+ adopt(s, existing);
869
+ return Promise.resolve(existing);
870
+ }
871
+ if (s.cancellation) return s.cancellation;
872
+ if (currentTerminalIntent(s) === "completed") {
873
+ s.cancellation = waitTerminal(s);
874
+ return s.cancellation;
875
+ }
876
+ if (!claimTerminalIntent(s, "cancelled")) {
877
+ if (currentTerminalIntent(s) !== "cancelled") {
878
+ s.cancellation = waitTerminal(s);
879
+ return s.cancellation;
880
+ }
881
+ }
882
+ s.cancellationRequested = true;
883
+ s.cancellation = cancelWork(s);
884
+ return s.cancellation;
885
+ }
886
+ function sync(s) {
887
+ const terminal = readJson(join(s.meta.treeDir, "terminal.json"));
888
+ if (terminal) adopt(s, terminal);
889
+ for (const [id, p] of s.pending){
890
+ const n = readJson(nodePath(s, id));
891
+ const r = n ? readJson(n.resultPath) : undefined;
892
+ if (r) p.resolve(deepFreeze(r));
893
+ }
894
+ if (isCancelled(s) && !s.branchCancellation && s.ownerId !== s.meta.callerId) void cancelBranch(s);
895
+ maybeFinish(s);
896
+ }
897
+ function makeState(meta, pi, ctx, ownerId) {
898
+ claimBranch(meta, ownerId);
899
+ let resolveTree;
900
+ const treePromise = new Promise((r)=>{
901
+ resolveTree = r;
902
+ });
903
+ const s = {
904
+ meta,
905
+ ownerId,
906
+ pi,
907
+ ctx,
908
+ boundSessionFile: ownerSession(meta, ownerId) ?? context(ctx).sessionFile,
909
+ children: new Map(),
910
+ pending: new Map(),
911
+ launchTransactions: new Set(),
912
+ launchControllers: new Set(),
913
+ callbackVersion: 0,
914
+ deliverySerial: Promise.resolve(),
915
+ resolveTree,
916
+ treePromise,
917
+ cancellationRequested: false
918
+ };
919
+ const dir = join(meta.treeDir, "nodes");
920
+ if (existsSync(dir)) for (const f of readdirSync(dir).filter((x)=>x.endsWith(".json"))){
921
+ const n = readJson(join(dir, f));
922
+ if (n?.ownerId === ownerId) {
923
+ const p = deferred();
924
+ s.pending.set(n.nodeId, p);
925
+ const r = readJson(n.resultPath);
926
+ if (r) p.resolve(deepFreeze(r));
927
+ s.children.set(n.nodeId, Object.freeze({
928
+ nodeId: n.nodeId,
929
+ parentId: n.parentId,
930
+ result: p.promise
931
+ }));
932
+ }
933
+ }
934
+ const terminal = readJson(join(meta.treeDir, "terminal.json"));
935
+ if (terminal) adopt(s, terminal);
936
+ const timer = setInterval(()=>sync(s), 100);
937
+ timer.unref();
938
+ return s;
939
+ }
940
+ function readonlyChildren(s) {
941
+ const view = {
942
+ get size () {
943
+ return s.children.size;
944
+ },
945
+ get: (key)=>s.children.get(key),
946
+ has: (key)=>s.children.has(key),
947
+ keys: ()=>s.children.keys(),
948
+ values: ()=>s.children.values(),
949
+ entries: ()=>s.children.entries(),
950
+ forEach: (fn)=>s.children.forEach((value, key)=>fn(value, key, view)),
951
+ [Symbol.iterator]: ()=>s.children[Symbol.iterator]()
952
+ };
953
+ return Object.freeze(view);
954
+ }
955
+ function expose(s) {
956
+ return Object.freeze({
957
+ get callerId () {
958
+ return s.meta.callerId;
959
+ },
960
+ get treeId () {
961
+ return s.meta.treeId;
962
+ },
963
+ get ownershipToken () {
964
+ return s.meta.ownershipToken;
965
+ },
966
+ get ownerId () {
967
+ return s.ownerId;
968
+ },
969
+ get children () {
970
+ return readonlyChildren(s);
971
+ },
972
+ get result () {
973
+ sync(s);
974
+ return s.treePromise;
975
+ },
976
+ launchChild: (o)=>launch(s, o),
977
+ bindFinalCallback: (cb)=>{
978
+ if (s.ownerId !== s.meta.callerId) throw new SubagentTreeOwnershipError("only the original caller may bind the final callback");
979
+ s.callbackVersion++;
980
+ s.callback = cb;
981
+ if (s.terminal) void deliver(s, s.terminal);
982
+ },
983
+ cancel: ()=>cancel(s)
984
+ });
985
+ }
986
+ export function createSubagentTree(options) {
987
+ const baseMetadata = metadata(options.metadata === undefined ? null : options.metadata), c = context(options.ctx), treeId = randomUUID(), callerId = randomUUID(), token = randomUUID(), treeDir = join(c.sessionDir, "artifacts", c.sessionId, "subagent-trees", treeId), callerMetadata = deepFreeze(identityMetadata(baseMetadata, {
988
+ callerId,
989
+ treeId
990
+ }, callerId, callerId)), meta = {
991
+ version: 2,
992
+ treeId,
993
+ callerId,
994
+ ownershipToken: token,
995
+ artifactDir: join(c.sessionDir, "artifacts", c.sessionId),
996
+ treeDir,
997
+ callerMetadata,
998
+ boundSessionFile: c.sessionFile,
999
+ acceptedSessionFiles: {
1000
+ [callerId]: [
1001
+ c.sessionFile
1002
+ ]
1003
+ }
1004
+ };
1005
+ mkdirSync(join(treeDir, "nodes"), {
1006
+ recursive: true
1007
+ });
1008
+ atomic(join(treeDir, "tree.json"), meta);
1009
+ const s = makeState(meta, options.pi, options.ctx, callerId);
1010
+ registry().set(stateKey(treeId, callerId), s);
1011
+ return expose(s);
1012
+ }
1013
+ export function attachSubagentTree(options) {
1014
+ const contextPath = process.env.PI_SUBAGENT_TREE_CONTEXT, ctxMeta = contextPath ? readJson(contextPath) : undefined, values = [
1015
+ options.callerId,
1016
+ options.treeId,
1017
+ options.ownershipToken
1018
+ ], has = values.some((x)=>x !== undefined);
1019
+ if (has && values.some((x)=>x === undefined)) throw new SubagentTreeOwnershipError("callerId, treeId, and ownershipToken must be supplied together");
1020
+ const treeId = options.treeId ?? ctxMeta?.treeId, callerId = options.callerId ?? ctxMeta?.callerId, token = options.ownershipToken ?? ctxMeta?.ownershipToken;
1021
+ if (!treeId || !callerId || !token) throw new SubagentTreeOwnershipError("tree credentials are required");
1022
+ if (ctxMeta && (treeId !== ctxMeta.treeId || callerId !== ctxMeta.callerId || token !== ctxMeta.ownershipToken)) throw new SubagentTreeOwnershipError("invalid tree credentials");
1023
+ const meta = readJson(join(ctxMeta?.treeDir ?? "", "tree.json")) ?? ctxMeta ?? findTreeMeta(context(options.ctx).sessionDir, treeId);
1024
+ if (!meta || meta.treeId !== treeId || meta.callerId !== callerId || meta.ownershipToken !== token) throw new SubagentTreeOwnershipError("invalid tree credentials");
1025
+ const ownerId = ctxMeta?.ownerId ?? callerId;
1026
+ if (ownerId !== callerId && !readJson(join(meta.treeDir, "nodes", `${ownerId}.json`))) throw new SubagentTreeOwnershipError("unknown branch owner");
1027
+ const keyValue = stateKey(treeId, ownerId), existing = registry().get(keyValue) ?? [
1028
+ ...registry().values()
1029
+ ].find((v)=>v.meta.treeId === treeId && v.meta.callerId === callerId && v.ownerId === ownerId), freshSession = context(options.ctx).sessionFile, prior = existing?.boundSessionFile ?? ownerSession(meta, ownerId), initialContext = Boolean(ctxMeta && !existing), knownSession = meta.acceptedSessionFiles?.[ownerId]?.includes(freshSession) === true;
1030
+ if (!initialContext && !knownSession && options.forkLineage === undefined && header(freshSession) && prior !== freshSession) throw new SubagentTreeOwnershipError("fork lineage is required for a fork session");
1031
+ const fresh = initialContext || knownSession ? freshSession : validateLineage(meta, ownerId, options.ctx, options.forkLineage, prior);
1032
+ if (initialContext) {
1033
+ const claimPath = `${contextPath}.claim`;
1034
+ if (!exclusiveAtomic(claimPath, {
1035
+ process: processIdentity(),
1036
+ ownerId,
1037
+ token: randomUUID()
1038
+ })) throw new SubagentTreeOwnershipError("package tree context was already claimed");
1039
+ }
1040
+ const s = existing ?? makeState(meta, options.pi, options.ctx, ownerId);
1041
+ s.pi = options.pi;
1042
+ s.ctx = options.ctx;
1043
+ s.boundSessionFile = fresh;
1044
+ meta.acceptedSessionFiles ??= {};
1045
+ meta.acceptedSessionFiles[ownerId] ??= [];
1046
+ if (!meta.acceptedSessionFiles[ownerId].includes(fresh)) meta.acceptedSessionFiles[ownerId].push(fresh);
1047
+ if (ownerId === callerId) {
1048
+ meta.boundSessionFile = fresh;
1049
+ atomic(join(meta.treeDir, "tree.json"), meta);
1050
+ } else {
1051
+ const node = readJson(join(meta.treeDir, "nodes", `${ownerId}.json`));
1052
+ if (node) {
1053
+ node.sessionFile = fresh;
1054
+ atomic(join(meta.treeDir, "nodes", `${ownerId}.json`), node);
1055
+ }
1056
+ }
1057
+ registry().set(keyValue, s);
1058
+ return expose(s);
1059
+ }
1060
+ function deepFreeze(value) {
1061
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
1062
+ for (const child of Object.values(value))deepFreeze(child);
1063
+ return Object.freeze(value);
1064
+ }