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