@fitchmultz/pi-subagents 0.50.0 → 0.52.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 (180) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/README.md +36 -5
  3. package/dist/agent/child-runtime.js +80 -0
  4. package/dist/agent/contracts.js +15 -24
  5. package/dist/agent/host.js +3 -16
  6. package/dist/agent/index.js +212 -192
  7. package/dist/agent/journal-tail.js +115 -156
  8. package/dist/agent/launch.js +20 -8
  9. package/dist/agent/legacy.js +65 -23
  10. package/dist/agent/notifications.js +138 -47
  11. package/dist/agent/observation-state.js +246 -0
  12. package/dist/agent/observation-view.js +258 -0
  13. package/dist/agent/observation.js +350 -0
  14. package/dist/agent/observe-card.js +243 -0
  15. package/dist/agent/observe-chrome.js +85 -0
  16. package/dist/agent/observe-client.js +285 -0
  17. package/dist/agent/observe-layout.js +65 -0
  18. package/dist/agent/observe-reading.js +84 -0
  19. package/dist/agent/observe-screen.js +400 -96
  20. package/dist/agent/observe-transcript.js +382 -0
  21. package/dist/agent/observe.js +250 -43
  22. package/dist/agent/parking.js +253 -0
  23. package/dist/agent/process.js +228 -28
  24. package/dist/agent/receipts.js +92 -20
  25. package/dist/agent/runner.js +229 -126
  26. package/dist/agent/shell.js +2 -1
  27. package/dist/agent/storage.js +47 -12
  28. package/dist/agent/tool-schemas.js +20 -0
  29. package/dist/agent/typebox.js +10 -5
  30. package/dist/agent/usage.js +0 -115
  31. package/dist/agent/view.js +46 -16
  32. package/dist/agent/wake.js +99 -0
  33. package/dist/build-info.js +1 -1
  34. package/dist/intercom-client.d.ts +26 -0
  35. package/dist/intercom-client.js +45 -0
  36. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  37. package/dist/pi-intercom/bridge-session.js +2 -1
  38. package/dist/pi-intercom/broker/broker.js +62 -43
  39. package/dist/pi-intercom/broker/client-last.d.ts +24 -0
  40. package/dist/pi-intercom/broker/client-last.js +42 -5
  41. package/dist/pi-intercom/broker/client-park.d.ts +9 -0
  42. package/dist/pi-intercom/broker/client-park.js +21 -0
  43. package/dist/pi-intercom/broker/client.d.ts +59 -0
  44. package/dist/pi-intercom/broker/client.js +55 -29
  45. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  46. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  47. package/dist/pi-intercom/broker/framing.js +10 -7
  48. package/dist/pi-intercom/broker/guard.js +23 -0
  49. package/dist/pi-intercom/broker/last.d.ts +30 -0
  50. package/dist/pi-intercom/broker/last.js +87 -40
  51. package/dist/pi-intercom/broker/parked.js +107 -0
  52. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  53. package/dist/pi-intercom/broker/paths.js +35 -12
  54. package/dist/pi-intercom/broker/pid.js +5 -1
  55. package/dist/pi-intercom/broker/server-connection.js +20 -1
  56. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  57. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  58. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  59. package/dist/pi-intercom/connection.js +71 -20
  60. package/dist/pi-intercom/identity.js +6 -5
  61. package/dist/pi-intercom/inbound-delivery.js +16 -60
  62. package/dist/pi-intercom/inbound-journal.js +83 -6
  63. package/dist/pi-intercom/inbound-record.js +18 -1
  64. package/dist/pi-intercom/inbound.js +22 -37
  65. package/dist/pi-intercom/index.js +41 -0
  66. package/dist/pi-intercom/last-message.js +4 -16
  67. package/dist/pi-intercom/lifecycle.js +88 -3
  68. package/dist/pi-intercom/outbound.js +5 -15
  69. package/dist/pi-intercom/reply-tracker.js +7 -6
  70. package/dist/pi-intercom/reply-wait.js +2 -2
  71. package/dist/pi-intercom/session-targets.d.ts +24 -0
  72. package/dist/pi-intercom/tool-registration.js +4 -1
  73. package/dist/pi-intercom/tool-results.js +5 -7
  74. package/dist/pi-intercom/tool-schemas.js +12 -26
  75. package/dist/pi-intercom/types.d.ts +212 -0
  76. package/dist/pi-intercom/types.js +54 -3
  77. package/dist/pi-intercom/validation.d.ts +12 -0
  78. package/dist/pi-intercom/validation.js +3 -0
  79. package/dist/shared/bounded-json.js +151 -0
  80. package/dist/shared/compact-cards.js +66 -0
  81. package/dist/shared/journal-frames.js +3 -6
  82. package/dist/shared/json-projection.js +267 -129
  83. package/dist/shared/native-journal.js +123 -56
  84. package/dist/shared/parking.js +12 -0
  85. package/dist/shared/post-exit-stdio-guard.js +120 -7
  86. package/dist/shared/tui-action.js +29 -0
  87. package/dist/shared/unknown.d.ts +9 -0
  88. package/docs/code-quality.md +5 -3
  89. package/docs/development.md +42 -3
  90. package/docs/intercom-bridge.md +9 -4
  91. package/docs/intercom.md +228 -7
  92. package/docs/performance.md +118 -14
  93. package/docs/reference.md +168 -20
  94. package/docs/retired-history-maintenance.md +252 -0
  95. package/package.json +28 -15
  96. package/scripts/archive-retired-history.mjs +464 -0
  97. package/scripts/build.mjs +48 -28
  98. package/scripts/measure-size.mjs +1 -1
  99. package/scripts/native-package-smoke.mjs +153 -12
  100. package/scripts/package-smoke.mjs +80 -11
  101. package/scripts/prepare.mjs +2 -2
  102. package/scripts/real-pi-live.mjs +377 -0
  103. package/scripts/real-pi-processes.mjs +6 -1
  104. package/scripts/real-pi-smoke.mjs +6 -282
  105. package/scripts/run-tests.mjs +4 -2
  106. package/scripts/typescript.mjs +35 -0
  107. package/skills/pi-intercom/SKILL.md +2 -1
  108. package/skills/pi-subagents/SKILL.md +23 -5
  109. package/src/agent/child-runtime.ts +105 -0
  110. package/src/agent/contracts.ts +18 -34
  111. package/src/agent/host.ts +39 -20
  112. package/src/agent/index.ts +234 -207
  113. package/src/agent/journal-tail.ts +141 -153
  114. package/src/agent/launch.ts +33 -9
  115. package/src/agent/legacy.ts +85 -31
  116. package/src/agent/notifications.ts +192 -67
  117. package/src/agent/observation-state.ts +277 -0
  118. package/src/agent/observation-view.ts +267 -0
  119. package/src/agent/observation.ts +409 -0
  120. package/src/agent/observe-card.ts +304 -0
  121. package/src/agent/observe-chrome.ts +170 -0
  122. package/src/agent/observe-client.ts +318 -0
  123. package/src/agent/observe-layout.ts +79 -0
  124. package/src/agent/observe-reading.ts +97 -0
  125. package/src/agent/observe-screen.ts +443 -129
  126. package/src/agent/observe-transcript.ts +434 -0
  127. package/src/agent/observe.ts +304 -71
  128. package/src/agent/parking.ts +341 -0
  129. package/src/agent/process.ts +280 -37
  130. package/src/agent/receipts.ts +114 -24
  131. package/src/agent/runner.ts +227 -149
  132. package/src/agent/shell.ts +2 -1
  133. package/src/agent/storage.ts +44 -11
  134. package/src/agent/tool-schemas.ts +33 -0
  135. package/src/agent/typebox.ts +12 -7
  136. package/src/agent/usage.ts +0 -130
  137. package/src/agent/view.ts +70 -20
  138. package/src/agent/wake.ts +107 -0
  139. package/src/intercom-client.ts +108 -0
  140. package/src/pi-intercom/bridge-session.ts +2 -2
  141. package/src/pi-intercom/broker/broker.ts +64 -53
  142. package/src/pi-intercom/broker/client-last.ts +65 -12
  143. package/src/pi-intercom/broker/client-park.ts +23 -0
  144. package/src/pi-intercom/broker/client.ts +63 -45
  145. package/src/pi-intercom/broker/framing.ts +11 -8
  146. package/src/pi-intercom/broker/guard.ts +33 -0
  147. package/src/pi-intercom/broker/last.ts +131 -43
  148. package/src/pi-intercom/broker/parked.ts +130 -0
  149. package/src/pi-intercom/broker/paths.ts +39 -12
  150. package/src/pi-intercom/broker/pid.ts +6 -1
  151. package/src/pi-intercom/broker/server-connection.ts +27 -2
  152. package/src/pi-intercom/connection.ts +103 -39
  153. package/src/pi-intercom/identity.ts +8 -5
  154. package/src/pi-intercom/inbound-delivery.ts +20 -66
  155. package/src/pi-intercom/inbound-journal.ts +103 -7
  156. package/src/pi-intercom/inbound-record.ts +21 -4
  157. package/src/pi-intercom/inbound.ts +32 -49
  158. package/src/pi-intercom/index.ts +44 -0
  159. package/src/pi-intercom/last-message.ts +11 -20
  160. package/src/pi-intercom/lifecycle.ts +117 -14
  161. package/src/pi-intercom/outbound.ts +7 -16
  162. package/src/pi-intercom/reply-tracker.ts +8 -6
  163. package/src/pi-intercom/reply-wait.ts +3 -2
  164. package/src/pi-intercom/runtime-types.ts +0 -1
  165. package/src/pi-intercom/tool-registration.ts +4 -1
  166. package/src/pi-intercom/tool-results.ts +10 -9
  167. package/src/pi-intercom/tool-schemas.ts +12 -44
  168. package/src/pi-intercom/types.ts +115 -4
  169. package/src/pi-intercom/validation.ts +4 -0
  170. package/src/shared/bounded-json.ts +162 -0
  171. package/src/shared/compact-cards.ts +118 -0
  172. package/src/shared/journal-frames.ts +3 -19
  173. package/src/shared/json-projection.ts +267 -147
  174. package/src/shared/native-journal.ts +157 -75
  175. package/src/shared/parking.ts +22 -0
  176. package/src/shared/post-exit-stdio-guard.ts +165 -7
  177. package/src/shared/tui-action.ts +39 -0
  178. package/tsconfig.client.json +8 -0
  179. package/tsconfig.json +1 -0
  180. package/src/shared/stream-json.d.ts +0 -10
@@ -1,25 +1,64 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
- import { setTimeout as delay } from "node:timers/promises";
5
- import { actor, alive, ensureActor, ownIdentity, signalProcess, verifyProcess, shells, } from "./process.js";
4
+ import { actor, alive, ensureActor, ownIdentity, parkFailure, requestStop, signalProcess, stopChildren, } from "./process.js";
6
5
  import { isChild, errorText } from "./host.js";
7
- import { delegateSchema, runsSchema, resultSchema, attachmentSchema, admissionSchema, commandSchema, } from "./contracts.js";
8
- import { enqueue, latest, launchAt, list, read, runDirectory, ownerDirectory, conversation, write, } from "./storage.js";
9
- import { hold, notify, settle, release } from "./notifications.js";
6
+ import { serveParked } from "./parking.js";
7
+ import { resultSchema, attachmentSchema, admissionSchema, commandSchema, } from "./contracts.js";
8
+ import { delegateSchema, runsSchema } from "./tool-schemas.js";
9
+ import { enqueue, latest, launchAt, list, read, runDirectory, ownerDirectory, conversation, successor, write, } from "./storage.js";
10
+ import { notifications, deliver, settle } from "./notifications.js";
10
11
  import { prepareLaunch } from "./launch.js";
11
- import { toolResult, projection, render, summaries } from "./view.js";
12
- import { registerUsage } from "./usage.js";
13
- import { notifyLegacy } from "./legacy.js";
12
+ import { renderReceipt, renderInventory, renderInspection } from "./view.js";
13
+ import { legacyDirectory, legacyNotifications } from "./legacy.js";
14
+ import { DirectoryWatch, pathStamp } from "./wake.js";
14
15
  import { savedContinuation } from "./saved.js";
15
16
  import { executionCwd } from "./cwd.js";
16
17
  import { getPiAgentDir } from "../pi-intercom/agent-dir.js";
18
+ import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.js";
19
+ /** `handoff` and `provisional` mean another worker under the same launcher may still own these children. */
20
+ function retainsChildren(event) {
21
+ const disposition = Reflect.get(event, "disposition");
22
+ return disposition === "handoff" || disposition === "provisional";
23
+ }
24
+ function managedHost(context) {
25
+ const host = Reflect.get(context, "managedHost");
26
+ if (typeof host !== "object" || host === null) {
27
+ return undefined;
28
+ }
29
+ const owner = Reflect.get(host, "owner");
30
+ const pid = typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
31
+ const ready = Reflect.get(host, "ready");
32
+ return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
33
+ ? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
34
+ : undefined;
35
+ }
36
+ /** Legacy results are optional; their reconciliation reports an invalid configured root. */
37
+ function legacyInput() {
38
+ try {
39
+ return [legacyDirectory()];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
17
45
  class AgentRuntime {
18
46
  ctx;
47
+ // A managed launcher owns children across worker replacement; otherwise this process does.
48
+ host;
49
+ accepted = false;
19
50
  base = "";
20
51
  owner = "";
21
52
  generation = "";
53
+ // Polling remains only while change hints are unavailable or a reconciliation failed.
22
54
  timer;
55
+ woken;
56
+ // Change hints for the owner directory, legacy results and every awaited canonical file.
57
+ watches = new Map();
58
+ awaited = new Set();
59
+ // Input stamps the last reconciliation saw; a one-second probe recovers dropped hints.
60
+ observed = new Map();
61
+ probe;
23
62
  closing = false;
24
63
  reset = Promise.resolve();
25
64
  shutdown = new AbortController();
@@ -37,24 +76,120 @@ class AgentRuntime {
37
76
  throw new Error("Agent runtime is not attached.");
38
77
  }
39
78
  }
79
+ wake = () => {
80
+ this.woken ??= setImmediate(() => {
81
+ this.woken = undefined;
82
+ this.scan();
83
+ }).unref();
84
+ };
85
+ recover = () => {
86
+ for (const [file, seen] of this.observed) {
87
+ if (pathStamp(file) !== seen) {
88
+ this.wake();
89
+ return;
90
+ }
91
+ }
92
+ };
40
93
  scan = () => {
94
+ clearTimeout(this.timer);
41
95
  if (this.closing || !this.ctx || this.child) {
42
96
  return;
43
97
  }
98
+ const inputs = () => [this.base, ...legacyInput(), ...this.awaited];
99
+ const before = new Map(inputs().map((file) => [file, pathStamp(file)]));
100
+ let reconciled = true;
44
101
  try {
45
- notify(this.pi, this.ctx, this.base, this.owner);
46
- notifyLegacy(this.pi, this.ctx);
102
+ const { notices, awaited } = notifications(this.ctx, this.base, this.owner);
103
+ this.awaited = awaited;
104
+ try {
105
+ notices.unshift(...legacyNotifications(this.ctx));
106
+ }
107
+ catch (error) {
108
+ console.error(`Legacy completion reconciliation: ${errorText(error)}`);
109
+ }
110
+ deliver(this.pi, this.ctx, notices);
47
111
  }
48
112
  catch (error) {
113
+ reconciled = false;
49
114
  console.error(`Agent completion reconciliation: ${errorText(error)}`);
50
115
  }
116
+ // Newly awaited files were absent when read, or are rechecked once by the probe.
117
+ this.observed = new Map(inputs().map((file) => [file, before.get(file) ?? ""]));
118
+ const next = this.watch();
119
+ if (!reconciled || next === "poll") {
120
+ this.timer = setTimeout(this.scan, 200).unref();
121
+ }
122
+ else if (next === "rescan") {
123
+ // Publications before arming had no hint; reconcile once more after it.
124
+ this.wake();
125
+ }
51
126
  };
127
+ /** Watches every directory that can publish an awaited input; legacy results may never exist. */
128
+ watch() {
129
+ const legacy = legacyInput();
130
+ const directories = [
131
+ ...new Set([this.base, ...legacy, ...[...this.awaited].map((file) => path.dirname(file))]),
132
+ ];
133
+ for (const [directory, watch] of this.watches) {
134
+ if (!directories.includes(directory)) {
135
+ watch.close();
136
+ this.watches.delete(directory);
137
+ }
138
+ }
139
+ const states = directories.map((directory) => {
140
+ let watch = this.watches.get(directory);
141
+ if (watch === undefined) {
142
+ const every = directory === this.base || legacy.includes(directory);
143
+ watch = new DirectoryWatch(directory, (file) => every || file === null || this.awaited.has(path.join(directory, file)), this.wake);
144
+ this.watches.set(directory, watch);
145
+ }
146
+ return [directory, watch.ensure()];
147
+ });
148
+ if (states.some(([directory, state]) => state === "unwatched" && !legacy.includes(directory))) {
149
+ return "poll";
150
+ }
151
+ return states.some(([, state]) => state === "armed") ? "rescan" : "hints";
152
+ }
153
+ unwatch() {
154
+ clearTimeout(this.timer);
155
+ clearInterval(this.probe);
156
+ this.observed.clear();
157
+ clearImmediate(this.woken);
158
+ this.woken = undefined;
159
+ for (const watch of this.watches.values()) {
160
+ watch.close();
161
+ }
162
+ this.watches.clear();
163
+ this.awaited = new Set();
164
+ }
52
165
  start(context) {
53
- this.reset = this.reset.then(() => this.attach(context));
54
- return this.reset;
166
+ this.host = managedHost(context);
167
+ if (this.host === undefined || this.accepted || this.child) {
168
+ this.reset = this.reset.then(() => this.attach(context));
169
+ return this.reset;
170
+ }
171
+ // Attach and publish only after the launcher retires rollback: an unready replacement
172
+ // must neither adopt children nor append completions to a journal it may abandon.
173
+ // Readiness follows session_start, so never await it here.
174
+ this.owner = context.sessionManager.getSessionId();
175
+ this.base = ownerDirectory(this.agentDir, this.owner);
176
+ this.host.ready
177
+ .then(async (accepted) => {
178
+ if (accepted && !this.closing) {
179
+ this.accepted = true;
180
+ await this.start(context);
181
+ }
182
+ })
183
+ .catch((error) => {
184
+ console.error(`Agent runtime attachment: ${errorText(error)}`);
185
+ });
186
+ return undefined;
187
+ }
188
+ ownerIdentity() {
189
+ return ownIdentity(this.host?.owner.pid);
55
190
  }
56
191
  async attach(context) {
57
- clearInterval(this.timer);
192
+ this.unwatch();
58
193
  this.ctx = context;
59
194
  if (this.child) {
60
195
  return;
@@ -64,54 +199,52 @@ class AgentRuntime {
64
199
  this.generation = randomUUID();
65
200
  await fs.promises.mkdir(this.base, { recursive: true, mode: 0o700 });
66
201
  write(path.join(this.base, "attachment.json"), {
67
- ...ownIdentity(),
202
+ ...this.ownerIdentity(),
68
203
  generation: this.generation,
69
204
  attached: true,
70
205
  });
71
206
  if (this.closing) {
72
207
  return;
73
208
  }
74
- this.timer = setInterval(this.scan, 200);
75
- this.timer.unref();
76
- this.scan();
209
+ // Recovery and watch setup stay off the awaited startup path.
210
+ this.timer = setTimeout(this.scan, 200).unref();
211
+ this.probe = setInterval(this.recover, 1000).unref();
77
212
  }
78
- async dispose() {
213
+ async dispose(retain) {
79
214
  this.closing = true;
80
- clearInterval(this.timer);
215
+ this.unwatch();
81
216
  this.shutdown.abort();
82
217
  await this.reset;
83
- if (this.base.length > 0) {
218
+ if (!retain && this.base.length > 0) {
84
219
  const file = path.join(this.base, "attachment.json");
85
- if (read(file, attachmentSchema)?.generation === this.generation) {
86
- write(file, { ...ownIdentity(), generation: this.generation, attached: false });
220
+ const attachment = read(file, attachmentSchema);
221
+ if (attachment !== undefined && this.releases(attachment)) {
222
+ write(file, {
223
+ ...this.ownerIdentity(),
224
+ generation: attachment.generation,
225
+ attached: false,
226
+ });
227
+ await stopChildren(this.base, this.owner);
87
228
  }
88
229
  }
89
230
  this.ctx = undefined;
90
231
  }
232
+ releases(attachment) {
233
+ if (attachment.generation === this.generation) {
234
+ return true;
235
+ }
236
+ // An unready replacement never attached, but exiting still releases the launcher-owned children.
237
+ return (this.generation.length === 0 && attachment.attached && attachment.pid === this.host?.owner.pid);
238
+ }
91
239
  stop(launch) {
92
240
  const base = this.base;
93
- const identity = actor(base, launch);
94
- const targets = identity ? [identity, ...shells(base, launch, identity.token)] : [];
95
- // Verify all targets before admitting a request or signaling any process.
96
- const live = targets.filter(verifyProcess);
241
+ const { targets: live, command: id } = requestStop(base, launch);
97
242
  if (live.length === 0) {
98
- return {
99
- ...projection(base, launch, this.ctx),
243
+ return renderReceipt(base, launch, this.ctx, {
100
244
  stopAcknowledged: false,
101
245
  note: "No matching live actor or owned shell; process exit code remains unknown.",
102
- };
103
- }
104
- const id = identity && verifyProcess(identity)
105
- ? enqueue(base, launch, "stop", { actorToken: identity.token })
106
- : undefined;
107
- if (identity) {
108
- write(path.join(conversation(base, launch), `stopping-${identity.token}.json`), {
109
- at: Date.now(),
110
246
  });
111
247
  }
112
- live.forEach((target) => {
113
- signalProcess(target, "SIGTERM");
114
- });
115
248
  const escalation = setTimeout(() => {
116
249
  try {
117
250
  live.forEach((target) => {
@@ -123,62 +256,11 @@ class AgentRuntime {
123
256
  }
124
257
  }, 3000);
125
258
  escalation.unref();
126
- return {
127
- ...projection(base, launch, this.ctx),
259
+ return renderReceipt(base, launch, this.ctx, {
128
260
  stopRequest: id,
129
261
  stopAcknowledged: false,
130
262
  note: "Stop requested; neither native admission nor process exit is implied.",
131
- };
132
- }
133
- interrupted(launch, signal, owned) {
134
- if (this.shutdown.signal.aborted) {
135
- return toolResult({
136
- ...projection(this.base, launch, this.ctx),
137
- wait: "detached",
138
- note: "Parent runtime disposed; producer was not stopped.",
139
- });
140
- }
141
- if (signal?.aborted === true) {
142
- if (owned) {
143
- this.stop(launch);
144
- }
145
- return toolResult({
146
- ...projection(this.base, launch, this.ctx),
147
- wait: "cancelled",
148
- note: owned
149
- ? "Owned foreground work was asked to stop; exit is unconfirmed."
150
- : "Only the wait was cancelled; producer was not stopped.",
151
- });
152
- }
153
- const identity = actor(this.base, launch);
154
- if (identity && !alive(identity)) {
155
- return toolResult({
156
- ...projection(this.base, launch, this.ctx),
157
- wait: "interrupted",
158
- note: "Actor is not live; no completion or exit code was invented.",
159
- });
160
- }
161
- return;
162
- }
163
- async wait(launch, signal, owned) {
164
- const held = hold(this.owner, launch.id);
165
- for (;;) {
166
- const result = read(path.join(runDirectory(this.base, launch.id), "result.json"), resultSchema);
167
- if (result) {
168
- hold(this.owner, launch.id);
169
- return render(this.base, launch, this.ctx, { receivedResult: true });
170
- }
171
- const interruption = this.interrupted(launch, signal, owned);
172
- if (interruption) {
173
- if (held) {
174
- release(this.owner, launch.id);
175
- }
176
- return interruption;
177
- }
178
- // Cancellation of an observer never removes canonical producer state.
179
- // oxlint-disable-next-line no-await-in-loop
180
- await delay(50);
181
- }
263
+ });
182
264
  }
183
265
  async delegate(input, signal, context) {
184
266
  this.checkMain();
@@ -197,45 +279,17 @@ class AgentRuntime {
197
279
  });
198
280
  this.checkMain();
199
281
  enqueue(this.base, launch, "run");
200
- await ensureActor(this.base, launch);
201
- if (input.async === false) {
202
- return this.wait(launch, signal, true);
203
- }
204
- return toolResult({
205
- ...projection(this.base, launch, this.ctx),
282
+ await ensureActor(this.base, launch, this.shutdown.signal);
283
+ return renderReceipt(this.base, launch, this.ctx, {
206
284
  receipt: "launch",
207
- note: "Launch accepted, not actual completion or native receipt publication.",
285
+ note: "Launch accepted, not completion or native receipt publication. Completion arrives as a notification; do other work or end the turn instead of polling.",
208
286
  });
209
287
  }
210
288
  list(input) {
211
289
  const all = list(this.base)
212
290
  .map((id) => launchAt(this.base, id, this.owner))
213
291
  .sort((a, b) => a.createdAt - b.createdAt);
214
- const offset = input.offset ?? 0;
215
- const page = summaries(this.base, all.slice(offset, offset + 50), this.ctx);
216
- return toolResult({
217
- runs: page,
218
- offset,
219
- nextOffset: offset + page.length < all.length ? offset + page.length : undefined,
220
- total: all.length,
221
- });
222
- }
223
- successor(launch, prompt) {
224
- const id = randomUUID();
225
- const next = { ...launch, id, predecessor: launch.id, prompt, createdAt: Date.now() };
226
- fs.mkdirSync(runDirectory(this.base, id), { mode: 0o700 });
227
- write(path.join(runDirectory(this.base, id), "launch.json"), next, true);
228
- try {
229
- write(path.join(runDirectory(this.base, launch.id), "successor.json"), { id }, true);
230
- }
231
- catch (error) {
232
- fs.rmSync(runDirectory(this.base, id), { recursive: true });
233
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
234
- throw error;
235
- }
236
- return undefined;
237
- }
238
- return next;
292
+ return renderInventory(this.base, all, this.ctx, input);
239
293
  }
240
294
  continuationResult(launch) {
241
295
  const result = read(path.join(runDirectory(this.base, launch.id), "result.json"), resultSchema);
@@ -248,7 +302,7 @@ class AgentRuntime {
248
302
  }
249
303
  return;
250
304
  }
251
- async retryContinuation(launch, input, signal) {
305
+ async retryContinuation(launch, input) {
252
306
  const identity = actor(this.base, launch);
253
307
  const command = read(path.join(conversation(this.base, launch), "commands", `${launch.id}.json`), commandSchema);
254
308
  if (input.prompt !== launch.prompt ||
@@ -256,16 +310,13 @@ class AgentRuntime {
256
310
  command?.id !== launch.id ||
257
311
  command.runId !== launch.id ||
258
312
  command.action !== "run") {
259
- throw new Error(`Run ${launch.id} is awaiting native admission. Retry requires the exact saved prompt, its existing queued Run command, and no live actor. Missing commands or ambiguous process claims require inspection; wait for admission or a canonical result before sending another prompt. No new continuation was launched.`);
313
+ throw new Error(`Run ${launch.id} is awaiting native admission. Retry requires the exact saved prompt, its existing queued Run command, and no live actor. Missing commands or ambiguous process claims require inspection. Admission or a canonical result arrives without polling: end the turn or do other work until the completion notification, then inspect once before sending another prompt. No new continuation was launched.`);
260
314
  }
261
315
  // Only an unadmitted durable command is retryable; keep all process/shell claim guards.
262
- await ensureActor(this.base, launch);
263
- if (input.async === false) {
264
- return this.wait(launch, signal, true);
265
- }
266
- return toolResult({ ...projection(this.base, launch, this.ctx), receipt: "continuation" });
316
+ await ensureActor(this.base, launch, this.shutdown.signal);
317
+ return renderReceipt(this.base, launch, this.ctx, { receipt: "continuation" });
267
318
  }
268
- async continue(launch, input, signal) {
319
+ async continue(launch, input) {
269
320
  if (input.prompt === undefined) {
270
321
  throw new Error("continue requires the entire new prompt.");
271
322
  }
@@ -274,61 +325,30 @@ class AgentRuntime {
274
325
  }
275
326
  const completed = this.continuationResult(launch);
276
327
  if (completed === "pending") {
277
- return this.retryContinuation(launch, input, signal);
328
+ return this.retryContinuation(launch, input);
278
329
  }
279
330
  const identity = actor(this.base, launch);
280
331
  if (!completed && identity && alive(identity)) {
281
332
  const command = enqueue(this.base, launch, "steer", { prompt: input.prompt });
282
- if (input.async === false) {
283
- return this.guidance(launch, command, signal);
284
- }
285
- return toolResult({
286
- ...projection(this.base, launch, this.ctx),
333
+ return renderReceipt(this.base, launch, this.ctx, {
287
334
  receipt: "guidance",
288
335
  command,
289
336
  note: "Guidance queued, not actual completion or model consumption.",
290
337
  });
291
338
  }
292
- const next = this.successor(launch, input.prompt);
339
+ parkFailure(this.base, launch);
340
+ const next = successor(this.base, launch, input.prompt);
293
341
  if (!next) {
294
- return toolResult({
295
- ...projection(this.base, latest(this.base, launch), this.ctx),
342
+ return renderReceipt(this.base, latest(this.base, launch), this.ctx, {
296
343
  receipt: "existing-successor",
297
344
  });
298
345
  }
299
346
  // Retain successor claims on ambiguous after-launch failures: no replay.
300
347
  enqueue(this.base, next, "run");
301
- await ensureActor(this.base, next);
302
- if (input.async === false) {
303
- return this.wait(next, signal, true);
304
- }
305
- return toolResult({ ...projection(this.base, next, this.ctx), receipt: "continuation" });
348
+ await ensureActor(this.base, next, this.shutdown.signal);
349
+ return renderReceipt(this.base, next, this.ctx, { receipt: "continuation" });
306
350
  }
307
- async guidance(launch, command, signal) {
308
- const file = path.join(conversation(this.base, launch), "admissions", `${command}.json`);
309
- for (;;) {
310
- const admission = read(file, admissionSchema);
311
- if (admission) {
312
- if (admission.state === "stale") {
313
- return toolResult({
314
- ...projection(this.base, launch, this.ctx),
315
- command,
316
- guidance: "stale",
317
- note: "The native turn settled before guidance admission. Retry the entire prompt as a continuation; no new work was started.",
318
- });
319
- }
320
- return this.wait(launch, signal, false);
321
- }
322
- const interruption = this.interrupted(launch, signal, false);
323
- if (interruption) {
324
- return interruption;
325
- }
326
- // Observe the actor's actual admission, not a timer-based model-consumption claim.
327
- // oxlint-disable-next-line no-await-in-loop
328
- await delay(50);
329
- }
330
- }
331
- async continueSaved(input, signal, context) {
351
+ async continueSaved(input, context) {
332
352
  if (input.prompt === undefined) {
333
353
  throw new Error("continue requires the entire new prompt.");
334
354
  }
@@ -342,17 +362,13 @@ class AgentRuntime {
342
362
  if (created) {
343
363
  // The durable adoption claim survives ambiguous after-launch failures.
344
364
  enqueue(this.base, launch, "run");
345
- await ensureActor(this.base, launch);
346
- }
347
- if (input.async === false) {
348
- return this.wait(launch, signal, created);
365
+ await ensureActor(this.base, launch, this.shutdown.signal);
349
366
  }
350
- return toolResult({
351
- ...projection(this.base, launch, this.ctx),
367
+ return renderReceipt(this.base, launch, this.ctx, {
352
368
  receipt: created ? "continuation" : "existing-successor",
353
369
  });
354
370
  }
355
- async runs(input, signal, context) {
371
+ async runs(input, context) {
356
372
  this.checkMain();
357
373
  await this.reset;
358
374
  if (input.action === "list") {
@@ -364,31 +380,33 @@ class AgentRuntime {
364
380
  }
365
381
  if (input.action === "continue" &&
366
382
  !fs.existsSync(path.join(runDirectory(this.base, input.id), "launch.json"))) {
367
- return this.continueSaved({ ...input, id: input.id }, signal, context);
383
+ return this.continueSaved({ ...input, id: input.id }, context);
368
384
  }
369
385
  const original = launchAt(this.base, input.id, this.owner);
370
386
  if (input.action === "inspect") {
371
- return render(this.base, original, this.ctx, input);
387
+ return renderInspection(this.base, original, this.ctx, input);
372
388
  }
373
389
  const launch = latest(this.base, original);
374
390
  if (input.action === "stop") {
375
- return toolResult(this.stop(launch));
391
+ return this.stop(launch);
376
392
  }
377
- if (input.action === "wait") {
378
- return this.wait(launch, signal, false);
379
- }
380
- return this.continue(launch, input, signal);
393
+ return this.continue(launch, input);
381
394
  }
382
395
  register() {
383
- registerUsage(this.pi);
396
+ registerCompactMessages(this.pi, ["agent-completion", "subagent-notify"]);
397
+ serveParked(this.pi, () => (this.child || !this.ctx ? undefined : { base: this.base, owner: this.owner }), this.shutdown.signal);
384
398
  this.pi.on("session_start", (_event, context) => this.start(context));
385
- this.pi.on("turn_end", (event) => {
386
- settle(this.owner, event.context.pendingMessages.length > 0);
399
+ this.pi.on("turn_end", (event, context) => {
400
+ // The public pending flag covers user input; the preview also includes custom messages.
401
+ if (settle(this.owner, context.hasPendingMessages() || event.context.pendingMessages.length > 0)) {
402
+ // Released admissions may have been dropped from the queue rather than published.
403
+ this.wake();
404
+ }
387
405
  });
388
406
  this.pi.on("agent_settled", () => {
389
407
  this.scan();
390
408
  });
391
- this.pi.on("session_shutdown", () => this.dispose());
409
+ this.pi.on("session_shutdown", (event) => this.dispose(event.reason === "reload" || retainsChildren(event)));
392
410
  if (this.child) {
393
411
  this.pi.on("tool_call", (event) => {
394
412
  if (event.toolName === "delegate" || event.toolName === "agent_runs") {
@@ -401,18 +419,20 @@ class AgentRuntime {
401
419
  this.pi.registerTool({
402
420
  name: "delegate",
403
421
  label: "Delegate",
404
- description: "Launch one child with exactly the supplied prompt. Fresh context by default; fork preserves the full native conversation. Main sessions only. Returns an asynchronous launch receipt unless async:false.",
422
+ description: "Run a child on exactly this prompt; main sessions only. Fork copies the full conversation; default is fresh. Returns a receipt; completion is notified later: do other work or end the turn, don't poll.",
405
423
  parameters: delegateSchema,
406
424
  exposure: this.child ? "hidden" : "direct",
425
+ renderResult: renderCompactResult,
407
426
  execute,
408
427
  });
409
- const runs = (...args) => this.runs(args[1], args[2], args[4]);
428
+ const runs = (...args) => this.runs(args[1], args[4]);
410
429
  this.pi.registerTool({
411
430
  name: "agent_runs",
412
431
  label: "Agent runs",
413
- description: "List, inspect, continue, wait for, or stop this native main session's durable child runs. Inspect is paged with byte offsets; full outputs and native journals remain available. Wait cancellation does not stop existing work. Continue requires the complete new prompt and reuses the saved conversation.",
432
+ description: "Manage your child runs without waiting; completions are notified. Inspect pages by byte offset, never interrupting. Continue takes the complete new prompt: it steers a live child or resumes the saved conversation.",
414
433
  parameters: runsSchema,
415
434
  exposure: this.child ? "hidden" : "direct",
435
+ renderResult: renderCompactResult,
416
436
  execute: runs,
417
437
  });
418
438
  }