@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,11 +1,16 @@
1
1
  import * as path from "node:path";
2
+ import * as fs from "node:fs";
3
+ import { SettingsManager, } from "@earendil-works/pi-coding-agent";
4
+ import { Container, TruncatedText } from "@earendil-works/pi-tui";
5
+ import { actionText } from "../shared/tui-action.js";
2
6
  import { getPiAgentDir } from "../pi-intercom/agent-dir.js";
3
7
  import { isChild, errorText } from "./host.js";
4
8
  import { launchAt, list, ownerDirectory, runDirectory, read } from "./storage.js";
5
9
  import { successorSchema } from "./contracts.js";
6
10
  import { readCanonicalResult } from "./usage.js";
7
- import { actor, alive } from "./process.js";
8
- import { JournalTail, plainText } from "./journal-tail.js";
11
+ import { actor, observeAlive } from "./process.js";
12
+ import { plainText } from "./journal-tail.js";
13
+ import { ObservationClient } from "./observe-client.js";
9
14
  import { adoptedJournal } from "./saved.js";
10
15
  const STATUS_KEY = "agent-observer";
11
16
  class Observer {
@@ -13,9 +18,16 @@ class Observer {
13
18
  owner = "";
14
19
  base = "";
15
20
  timer;
21
+ scanning;
22
+ rescan = false;
16
23
  close;
17
24
  refresh;
18
25
  rows = new Map();
26
+ peers = new Map();
27
+ watcher;
28
+ widgetVisible = false;
29
+ widgetSignature = "";
30
+ widgetRender;
19
31
  launches = new Map();
20
32
  finished = new Set();
21
33
  pending = [];
@@ -27,10 +39,22 @@ class Observer {
27
39
  }
28
40
  dispose() {
29
41
  clearInterval(this.timer);
42
+ this.timer = undefined;
43
+ this.scanning = undefined;
44
+ this.rescan = false;
45
+ this.watcher?.close();
46
+ this.watcher = undefined;
47
+ for (const peer of this.peers.values()) {
48
+ peer.client.close();
49
+ }
50
+ this.peers.clear();
51
+ this.widgetVisible = false;
52
+ this.widgetSignature = "";
53
+ this.widgetRender = undefined;
30
54
  this.close?.();
31
55
  this.close = undefined;
32
56
  this.refresh = undefined;
33
- this.ctx?.ui.setStatus(STATUS_KEY, undefined);
57
+ this.ctx?.ui.setWidget(STATUS_KEY, undefined);
34
58
  this.ctx = undefined;
35
59
  this.rows.clear();
36
60
  this.launches.clear();
@@ -49,57 +73,75 @@ class Observer {
49
73
  this.ctx = ctx;
50
74
  this.owner = ctx.sessionManager.getSessionId();
51
75
  this.base = ownerDirectory(path.resolve(getPiAgentDir()), this.owner);
76
+ fs.mkdirSync(this.base, { recursive: true, mode: 0o700 });
77
+ try {
78
+ const watcher = fs.watch(this.base, { recursive: true }, (_event, file) => {
79
+ if (this.watcher === watcher &&
80
+ this.timer === undefined &&
81
+ (file === null || /(?:launch|result|successor|process)\.json$/.test(file))) {
82
+ this.scan();
83
+ }
84
+ });
85
+ this.watcher = watcher;
86
+ watcher.on("error", () => {
87
+ if (this.watcher !== watcher) {
88
+ return;
89
+ }
90
+ watcher.close();
91
+ this.watcher = undefined;
92
+ this.scan();
93
+ });
94
+ }
95
+ catch {
96
+ /* Sparse native execution events still wake inventory without an idle poll. */
97
+ }
52
98
  this.scan();
53
- this.timer = setInterval(() => this.scan(), 1000);
54
- this.timer.unref();
55
99
  }
56
100
  launch(id) {
57
101
  const launch = this.launches.get(id) ?? launchAt(this.base, id, this.owner);
58
102
  this.launches.set(id, launch);
59
103
  return launch;
60
104
  }
61
- live(launch, roots) {
62
- const cached = roots.get(launch.rootId);
63
- if (cached !== undefined) {
64
- return cached;
65
- }
66
- const identity = actor(this.base, launch);
67
- const live = identity !== undefined && alive(identity);
68
- roots.set(launch.rootId, live);
69
- return live;
70
- }
105
+ /** Reads durable metadata now; unfinished rows commit after their root's batched liveness. */
71
106
  load(id, roots) {
72
107
  if (this.finished.has(id)) {
73
- return;
108
+ return undefined;
74
109
  }
75
110
  const launch = this.launch(id);
76
111
  const result = readCanonicalResult(this.base, launch);
77
112
  const finished = result !== undefined || this.hasSuccessor(id);
78
- const live = !finished && this.live(launch, roots);
79
113
  if (finished) {
80
114
  this.finished.add(id);
81
115
  }
82
- this.rows.set(id, {
83
- id,
84
- name: plainText(launch.name ?? id, 120),
85
- status: result?.status ?? (live ? "running" : "interrupted"),
86
- live,
87
- createdAt: launch.createdAt,
88
- });
116
+ else if (!roots.has(launch.rootId)) {
117
+ roots.set(launch.rootId, actor(this.base, launch));
118
+ }
119
+ return (live) => {
120
+ const running = !finished && live.get(launch.rootId) === true;
121
+ this.rows.set(id, {
122
+ id,
123
+ name: plainText(launch.name ?? id, 120),
124
+ status: result?.status ?? (running ? "running" : "interrupted"),
125
+ live: running,
126
+ createdAt: launch.createdAt,
127
+ activity: this.peers.get(id)?.client.activity,
128
+ });
129
+ };
89
130
  }
90
131
  hasSuccessor(id) {
91
132
  return (read(path.join(runDirectory(this.base, id), "successor.json"), successorSchema) !== undefined);
92
133
  }
93
134
  loadVisible(id, roots) {
94
135
  try {
95
- this.load(id, roots);
136
+ return this.load(id, roots);
96
137
  }
97
138
  catch (error) {
98
139
  this.rows.set(id, { id, name: id, status: "unavailable", live: false, createdAt: 0 });
99
140
  this.error = plainText(errorText(error));
141
+ return undefined;
100
142
  }
101
143
  }
102
- scan() {
144
+ loadBatch(roots) {
103
145
  try {
104
146
  const ids = list(this.base);
105
147
  const present = new Set(ids);
@@ -118,14 +160,161 @@ class Observer {
118
160
  this.pending = ids.filter((id) => !this.finished.has(id));
119
161
  }
120
162
  this.error = "";
121
- const roots = new Map();
122
- this.pending.splice(0, 50).forEach((id) => this.loadVisible(id, roots));
163
+ return this.pending.splice(0, 50).flatMap((id) => this.loadVisible(id, roots) ?? []);
123
164
  }
124
165
  catch (error) {
125
166
  this.error = plainText(errorText(error));
167
+ return [];
168
+ }
169
+ }
170
+ /** One scan at a time; triggers during a scan coalesce into one follow-up. */
171
+ scan() {
172
+ if (this.scanning !== undefined) {
173
+ this.rescan = true;
174
+ return;
175
+ }
176
+ const scanning = this.update()
177
+ .catch((error) => {
178
+ this.error = plainText(errorText(error));
179
+ })
180
+ .finally(() => {
181
+ if (this.scanning !== scanning) {
182
+ return;
183
+ }
184
+ this.scanning = undefined;
185
+ if (this.rescan) {
186
+ this.rescan = false;
187
+ this.scan();
188
+ }
189
+ });
190
+ this.scanning = scanning;
191
+ }
192
+ async update() {
193
+ const ctx = this.ctx;
194
+ const roots = new Map();
195
+ const commits = this.loadBatch(roots);
196
+ const claims = [...roots].flatMap(([root, identity]) => (identity ? [{ root, identity }] : []));
197
+ const verdicts = await observeAlive(claims.map(({ identity }) => identity));
198
+ if (this.ctx !== ctx) {
199
+ return;
200
+ }
201
+ const live = new Map(claims.map(({ root }, index) => [root, verdicts[index]]));
202
+ for (const commit of commits) {
203
+ commit(live);
126
204
  }
127
- this.ctx?.ui.setStatus(STATUS_KEY, this.status());
205
+ this.activity();
206
+ this.widget();
128
207
  this.refresh?.();
208
+ const periodic = this.pending.length > 0 || [...this.rows.values()].some((row) => row.live);
209
+ if (periodic && !this.timer) {
210
+ this.timer = setInterval(() => this.scan(), 1000);
211
+ this.timer.unref();
212
+ }
213
+ else if (!periodic) {
214
+ clearInterval(this.timer);
215
+ this.timer = undefined;
216
+ }
217
+ }
218
+ activity() {
219
+ const visible = this.strip()
220
+ .filter((entry) => entry.live)
221
+ .slice(0, 4);
222
+ const ids = new Set(visible.map((entry) => entry.id));
223
+ for (const [id, peer] of this.peers) {
224
+ if (!ids.has(id) || peer.client.closed) {
225
+ peer.client.close();
226
+ this.peers.delete(id);
227
+ }
228
+ }
229
+ for (const row of visible) {
230
+ if (this.peers.has(row.id)) {
231
+ continue;
232
+ }
233
+ try {
234
+ const launch = this.launch(row.id);
235
+ const identity = actor(this.base, launch);
236
+ if (!identity) {
237
+ continue;
238
+ }
239
+ const client = new ObservationClient({ token: identity.token, rootId: launch.rootId, sessionId: launch.sessionId }, "activity", () => {
240
+ if (this.peers.get(row.id)?.client !== client) {
241
+ return;
242
+ }
243
+ const current = this.rows.get(row.id);
244
+ if (current) {
245
+ current.activity = client.connected ? client.activity : "Reconnecting";
246
+ }
247
+ this.widget();
248
+ });
249
+ this.peers.set(row.id, { token: identity.token, client });
250
+ }
251
+ catch {
252
+ // Older or not-yet-ready actors still have a known run status, but no live phase.
253
+ }
254
+ }
255
+ }
256
+ widget() {
257
+ const ctx = this.ctx;
258
+ if (!ctx) {
259
+ return;
260
+ }
261
+ const rows = this.strip();
262
+ if (rows.length === 0 && this.rows.size === this.total && this.error.length === 0) {
263
+ if (this.widgetVisible) {
264
+ ctx.ui.setWidget(STATUS_KEY, undefined);
265
+ }
266
+ this.widgetVisible = false;
267
+ this.widgetSignature = "";
268
+ return;
269
+ }
270
+ const signature = JSON.stringify([this.status(), rows.slice(0, 4), rows.length]);
271
+ if (signature === this.widgetSignature) {
272
+ return;
273
+ }
274
+ this.widgetSignature = signature;
275
+ if (this.widgetVisible) {
276
+ this.widgetRender?.();
277
+ return;
278
+ }
279
+ this.widgetVisible = true;
280
+ ctx.ui.setWidget(STATUS_KEY, (tui, theme) => {
281
+ this.widgetRender = () => {
282
+ tui.requestRender();
283
+ };
284
+ const content = new Container();
285
+ const open = (id) => {
286
+ if (ctx !== this.ctx) {
287
+ return;
288
+ }
289
+ void this.show(id, ctx).catch((error) => {
290
+ ctx.ui.notify(errorText(error), "error");
291
+ });
292
+ };
293
+ return {
294
+ render: (width) => {
295
+ const strip = this.strip();
296
+ const visible = strip.slice(0, 4);
297
+ content.clear();
298
+ content.addChild(actionText(theme.fg("accent", this.status()), () => open("")));
299
+ for (const row of visible) {
300
+ const glyph = row.live ? "●" : "○";
301
+ const color = row.live ? "accent" : "warning";
302
+ content.addChild(actionText(`${theme.fg(color, glyph)} ${theme.bold(row.name)} · ${row.activity ?? row.status}`, () => open(row.id)));
303
+ }
304
+ if (strip.length > visible.length) {
305
+ content.addChild(actionText(theme.fg("dim", `+${strip.length - visible.length} more · /agents`), () => open("")));
306
+ }
307
+ if (this.error.length > 0) {
308
+ content.addChild(new TruncatedText(theme.fg("warning", this.error), 0, 0));
309
+ }
310
+ return content.render(width);
311
+ },
312
+ handleMouse: (event) => content.handleMouse(event),
313
+ invalidate: () => {
314
+ // Render reads current rows, expansion and theme on every frame.
315
+ },
316
+ };
317
+ }, { placement: "aboveEditor" });
129
318
  }
130
319
  status() {
131
320
  const active = [...this.rows.values()].filter((row) => row.live).length;
@@ -134,7 +323,9 @@ class Observer {
134
323
  return `Agents: ${active}${checking ? "+" : ""} active${checking ? " (checking…)" : ""} · ${this.total} runs${error} · /agents`;
135
324
  }
136
325
  inventory() {
137
- return [...this.rows.values()].sort((a, b) => {
326
+ return [...this.rows.values()]
327
+ .filter((row) => row.status !== "completed")
328
+ .sort((a, b) => {
138
329
  const order = Number(b.live) - Number(a.live);
139
330
  if (order !== 0) {
140
331
  return order;
@@ -143,6 +334,10 @@ class Observer {
143
334
  return created !== 0 ? created : a.id.localeCompare(b.id);
144
335
  });
145
336
  }
337
+ /** The always-visible strip shows active work and unreadable runs; /agents keeps history. */
338
+ strip() {
339
+ return this.inventory().filter((row) => row.live || row.status === "unavailable");
340
+ }
146
341
  watch(id) {
147
342
  const matches = list(this.base).filter((candidate) => candidate.startsWith(id));
148
343
  const exact = matches.includes(id);
@@ -171,11 +366,14 @@ class Observer {
171
366
  !adoptedJournal(this.base, launch)) {
172
367
  throw new Error("Child journal path is outside its saved conversation.");
173
368
  }
369
+ const identity = actor(this.base, launch);
174
370
  return {
175
- tail: new JournalTail(launch.sessionFile, launch.sessionId),
176
- file: launch.sessionFile,
177
- id: launch.id,
178
- inherited: launch.context === "fork",
371
+ launch,
372
+ identity: identity && {
373
+ token: identity.token,
374
+ rootId: launch.rootId,
375
+ sessionId: launch.sessionId,
376
+ },
179
377
  };
180
378
  }
181
379
  async show(args, ctx) {
@@ -193,6 +391,7 @@ class Observer {
193
391
  const screen = new ObserverScreen({
194
392
  data: {
195
393
  inventory: () => this.inventory(),
394
+ checking: () => this.rows.size < this.total,
196
395
  row: (id) => this.rows.get(id),
197
396
  status: () => this.status(),
198
397
  error: () => this.error,
@@ -203,24 +402,32 @@ class Observer {
203
402
  keys,
204
403
  done: () => done(true),
205
404
  selected: args.trim(),
405
+ hideThinking: SettingsManager.create(ctx.cwd, getPiAgentDir()).getHideThinkingBlock(),
206
406
  });
207
407
  this.close = () => done(true);
208
408
  this.refresh = () => screen.update();
209
- return {
210
- render: (width) => screen.render(width),
211
- handleInput: (data) => screen.handleInput(data),
212
- invalidate: () => screen.invalidate(),
213
- dispose: () => {
214
- screen.dispose();
215
- this.close = undefined;
216
- this.refresh = undefined;
217
- },
409
+ const dispose = screen.dispose.bind(screen);
410
+ screen.dispose = () => {
411
+ dispose();
412
+ this.close = undefined;
413
+ this.refresh = undefined;
218
414
  };
415
+ return screen;
416
+ }, {
417
+ overlay: true,
418
+ overlayOptions: { width: "100%", maxHeight: "100%", anchor: "top-left", margin: 0 },
219
419
  });
220
420
  }
221
421
  register() {
222
422
  this.pi.on("session_start", (_event, ctx) => this.attach(ctx));
223
423
  this.pi.on("session_shutdown", () => this.dispose());
424
+ this.pi.on("tool_execution_end", (event) => {
425
+ if (this.ctx &&
426
+ this.watcher === undefined &&
427
+ (event.toolName === "delegate" || event.toolName === "agent_runs")) {
428
+ this.scan();
429
+ }
430
+ });
224
431
  this.pi.registerCommand("agents", {
225
432
  description: "List this parent's children; /agents <run-id or unique 8+ prefix> watches native activity read-only",
226
433
  handler: (args, ctx) => this.show(args, ctx),
@@ -0,0 +1,253 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { parkedSchema } from "./contracts.js";
5
+ import { errorText, loadSDK } from "./host.js";
6
+ import { conversation, latest, launchAt, list, read, runDirectory, write } from "./storage.js";
7
+ import { alive, shells, wakeActor } from "./process.js";
8
+ import { isHistoryRequest, PARK, PARKED_HISTORY, WAKE, } from "../shared/parking.js";
9
+ import { nativePeerId } from "../pi-intercom/session-targets.js";
10
+ function parkable(session) {
11
+ return typeof Reflect.get(session, "acquireWorkingSession") === "function";
12
+ }
13
+ function finalizing(runtime) {
14
+ return typeof Reflect.get(runtime, "setFinalization") === "function";
15
+ }
16
+ function fence(pi, park) {
17
+ let fenced = Promise.resolve(false);
18
+ pi.events.emit(PARK, {
19
+ park,
20
+ reply: (parked) => {
21
+ fenced = parked;
22
+ },
23
+ });
24
+ return fenced;
25
+ }
26
+ async function cut(pi, session, { quiet, errors, signal }) {
27
+ try {
28
+ // Save handlers may legitimately append entries; only the pre-cut leaf proves idleness.
29
+ if ((await fence(pi, true)) && quiet() && session.isIdle) {
30
+ return await session.acquireWorkingSession({ boundary: "settled", signal });
31
+ }
32
+ }
33
+ catch (error) {
34
+ errors.push(errorText(error));
35
+ }
36
+ return undefined;
37
+ }
38
+ const PARK_AFTER_MS = 5 * 60_000;
39
+ const digest = (file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
40
+ /** Parked authority names this session and exactly the saved generation's state bytes. */
41
+ function exact(restore, parked, state, launch) {
42
+ if (parked.sessionId !== launch.sessionId ||
43
+ parked.state !== state ||
44
+ digest(state) !== parked.digest) {
45
+ return undefined;
46
+ }
47
+ const saved = restore(state);
48
+ return saved.sessionFile === launch.sessionFile &&
49
+ saved.header.id === launch.sessionId &&
50
+ saved.leafId === parked.leafId
51
+ ? saved
52
+ : undefined;
53
+ }
54
+ /** This child's exact parked generation, if any; mismatched authority restores nothing. */
55
+ export function parkedState(dir, launch, restore) {
56
+ const parked = read(path.join(dir, "parked.json"), parkedSchema);
57
+ if (!parked) {
58
+ return undefined;
59
+ }
60
+ if (!restore) {
61
+ throw new Error("Parked working state needs the Pi fork that saved it; nothing was reopened.");
62
+ }
63
+ // Native working state requires a canonical parent, not a symlinked alias like macOS /var.
64
+ const state = path.join(fs.realpathSync(dir), `working-${parked.token}.json`);
65
+ const saved = exact(restore, parked, state, launch);
66
+ if (!saved) {
67
+ throw new Error("Parked working state is not this child's saved generation; nothing was reopened.");
68
+ }
69
+ return { state, saved };
70
+ }
71
+ /** One actor generation's idle parking and exact restore; commands stay with the runner. */
72
+ export class Parking {
73
+ saved;
74
+ restored;
75
+ since = 0;
76
+ leaf;
77
+ base;
78
+ launch;
79
+ token;
80
+ sdk;
81
+ constructor(base, launch, token, sdk) {
82
+ this.base = base;
83
+ this.launch = launch;
84
+ this.token = token;
85
+ this.sdk = sdk;
86
+ }
87
+ get dir() {
88
+ return runDirectory(this.base, this.launch.rootId);
89
+ }
90
+ get file() {
91
+ return path.join(this.dir, "parked.json");
92
+ }
93
+ /** Parked authority is exact state or nothing; never a journal-only reopen. */
94
+ open() {
95
+ const { sdk, launch } = this;
96
+ const { openWorkingSession: restore } = sdk;
97
+ const parked = parkedState(this.dir, launch, restore && sdk.readWorkingSession);
98
+ if (!parked || !restore) {
99
+ return sdk.SessionManager.open(launch.sessionFile, undefined, launch.cwd);
100
+ }
101
+ this.saved = parked.saved;
102
+ this.restored = parked.state;
103
+ return restore(parked.saved);
104
+ }
105
+ /** The ready restored generation owns this state now; a later park writes a new cut. */
106
+ adopt() {
107
+ if (this.restored !== undefined) {
108
+ fs.rmSync(this.file);
109
+ fs.rmSync(this.restored, { force: true });
110
+ }
111
+ }
112
+ /** True once this generation has exited its runtime with or without parked authority. */
113
+ async park(runtime, actor) {
114
+ if (!actor.pi ||
115
+ !this.due(runtime.session, actor.busy) ||
116
+ shells(this.base, this.launch, this.token).some(alive)) {
117
+ return false;
118
+ }
119
+ const hold = await this.prepare(runtime, actor.pi, actor);
120
+ if (hold) {
121
+ await this.commit(runtime, hold, actor.errors);
122
+ }
123
+ return hold !== undefined;
124
+ }
125
+ /** Final idle: no run, turn, journal change or viewer since the period began. */
126
+ due(session, busy) {
127
+ const leaf = session.sessionManager.getLeafId();
128
+ if (busy || !session.isIdle || leaf !== this.leaf) {
129
+ this.leaf = leaf;
130
+ this.since = performance.now();
131
+ return false;
132
+ }
133
+ return performance.now() - this.since >= PARK_AFTER_MS;
134
+ }
135
+ /**
136
+ * Fence broker input, then hold a settled sleep-ready cut of the same quiet idle leaf. Any
137
+ * failure or raced command releases, unfences and keeps this actor resident.
138
+ */
139
+ async prepare(runtime, pi, { quiet, errors, signal }) {
140
+ const { session } = runtime;
141
+ this.since = performance.now();
142
+ if (!parkable(session) || !this.sdk.writeWorkingSession || !finalizing(runtime)) {
143
+ return undefined;
144
+ }
145
+ // A launcher that enqueues after this marker waits for exit, then wakes the saved state.
146
+ const marker = path.join(this.dir, "actor.lock", "parking.json");
147
+ write(marker, { at: Date.now() });
148
+ const hold = await cut(pi, session, {
149
+ quiet: () => quiet() && session.sessionManager.getLeafId() === this.leaf,
150
+ errors,
151
+ signal,
152
+ });
153
+ // Revalidate after the awaited fence and save, immediately before the irreversible commit.
154
+ if (hold?.sleepReady === true && quiet()) {
155
+ return hold;
156
+ }
157
+ await hold?.release();
158
+ await fence(pi, false);
159
+ fs.rmSync(marker, { force: true });
160
+ return undefined;
161
+ }
162
+ /** Commit: native strict exit, then capture; failure leaves no parked authority. */
163
+ async commit(runtime, hold, errors) {
164
+ const save = this.sdk.writeWorkingSession;
165
+ if (save && finalizing(runtime)) {
166
+ runtime.setFinalization((final) => this.capture(final, save), { strictShutdown: true });
167
+ }
168
+ // No await between release and dispose: native final exit fences new turns in this tick.
169
+ const released = hold.release();
170
+ const disposed = runtime.dispose();
171
+ try {
172
+ await Promise.all([released, disposed]);
173
+ }
174
+ catch (error) {
175
+ errors.push(errorText(error));
176
+ }
177
+ if (read(this.file, parkedSchema)?.token !== this.token) {
178
+ // Truthful boundary: retained input stays queued and nothing restarts implicitly.
179
+ write(path.join(this.dir, "park-failed.json"), { token: this.token, errors });
180
+ }
181
+ }
182
+ async capture(session, save) {
183
+ if (!parkable(session)) {
184
+ throw new Error("Native working state is unavailable.");
185
+ }
186
+ const hold = await session.acquireWorkingSession({ boundary: "settled" });
187
+ try {
188
+ if (!hold.sleepReady) {
189
+ throw new Error(`Final working state is not sleep-ready: ${hold.blockers.join("; ")}`);
190
+ }
191
+ const state = path.join(fs.realpathSync(this.dir), `working-${this.token}.json`);
192
+ save(state, hold.state);
193
+ hold.assertHeld();
194
+ write(this.file, {
195
+ token: this.token,
196
+ state,
197
+ digest: digest(state),
198
+ sessionId: this.launch.sessionId,
199
+ leafId: hold.state.leafId,
200
+ });
201
+ }
202
+ finally {
203
+ await hold.release();
204
+ }
205
+ }
206
+ }
207
+ /** The broker names a parked child by native peer ID; only this owner's conversations match. */
208
+ function ownedRun(runs, peer) {
209
+ const launch = runs && typeof peer === "string"
210
+ ? list(runs.base)
211
+ .map((id) => launchAt(runs.base, id, runs.owner))
212
+ .find((candidate) => nativePeerId(candidate.sessionId) === peer)
213
+ : undefined;
214
+ return launch && runs && latest(runs.base, launch);
215
+ }
216
+ /** The captured leaf's root-to-leaf path, as a live session manager reports it. */
217
+ function branchOf(state) {
218
+ const entries = new Map(state.entries.map((entry) => [entry.id, entry]));
219
+ const branch = [];
220
+ for (let entry = entries.get(state.leafId ?? ""); entry !== undefined; entry = entries.get(entry.parentId ?? "")) {
221
+ branch.unshift(entry);
222
+ }
223
+ return branch;
224
+ }
225
+ async function history(runs, peer) {
226
+ const launch = ownedRun(runs, peer);
227
+ const restore = launch && (await loadSDK(launch.sdk)).readWorkingSession;
228
+ const state = runs && launch && parkedState(conversation(runs.base, launch), launch, restore)?.saved;
229
+ return state && { getBranch: () => branchOf(state) };
230
+ }
231
+ /**
232
+ * Wakes this owner's parked children for broker input and reads their saved history.
233
+ * `runs` is undefined while this session owns no children.
234
+ */
235
+ export function serveParked(pi, runs, signal) {
236
+ pi.events.on(WAKE, (peer) => {
237
+ // On failure, retained input stays queued at the broker until the next main registration.
238
+ (async () => {
239
+ const current = runs();
240
+ const launch = ownedRun(current, peer);
241
+ if (current && launch) {
242
+ await wakeActor(current.base, launch, signal);
243
+ }
244
+ })().catch((error) => {
245
+ console.error(`Agent wake: ${errorText(error)}`);
246
+ });
247
+ });
248
+ pi.events.on(PARKED_HISTORY, (request) => {
249
+ if (isHistoryRequest(request)) {
250
+ request.reply(history(runs(), request.peer));
251
+ }
252
+ });
253
+ }