@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,15 +1,16 @@
1
+ // Launches set both variables to the parent's exact native session ID. Absence means a main;
2
+ // anything else is refused, never normalized into a different identity.
1
3
  export function readIntercomLineage() {
2
- const rootSessionId = process.env.PI_INTERCOM_ROOT_SESSION_ID?.trim();
3
- const parentSessionId = process.env.PI_INTERCOM_PARENT_SESSION_ID?.trim();
4
+ const rootSessionId = process.env.PI_INTERCOM_ROOT_SESSION_ID;
5
+ const parentSessionId = process.env.PI_INTERCOM_PARENT_SESSION_ID;
4
6
  if (rootSessionId === undefined && parentSessionId === undefined) {
5
7
  return;
6
8
  }
7
9
  if (rootSessionId === undefined ||
8
- parentSessionId === undefined ||
9
10
  rootSessionId === "" ||
10
- parentSessionId === "" ||
11
+ rootSessionId !== rootSessionId.trim() ||
11
12
  rootSessionId !== parentSessionId) {
12
- throw new Error("Intercom child launch requires matching root and parent native session IDs.");
13
+ throw new Error("Child launch requires identical, unpadded root and parent native session IDs.");
13
14
  }
14
15
  return { rootSessionId, parentSessionId };
15
16
  }
@@ -1,6 +1,4 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { requestedDelivery } from "./inbound-record.js";
3
- import { isRecord } from "./validation.js";
4
2
  import { runCooperatively } from "../shared/cooperative.js";
5
3
  /** Owns cooperative inbound batches, restoration and the flush timer. */
6
4
  export class InboundDeliveryOwner {
@@ -83,6 +81,9 @@ export class InboundDeliveryOwner {
83
81
  this.timer.unref();
84
82
  }
85
83
  queue(entry, flushDelivery = "auto", delayMs = 200) {
84
+ if (!this.journal.owns(entry)) {
85
+ return;
86
+ }
86
87
  this.journal.remember(entry, "queued", flushDelivery);
87
88
  this.hooks.syncStatus();
88
89
  this.schedule(delayMs);
@@ -112,8 +113,7 @@ export class InboundDeliveryOwner {
112
113
  }
113
114
  triggerIndex(entries) {
114
115
  const canTrigger = (entry) => entry.flushDelivery !== "passive" && requestedDelivery(entry.message) !== "passive";
115
- const ask = entries.findIndex((entry) => canTrigger(entry) && entry.message.expectsReply === true);
116
- return ask === -1 ? entries.findIndex(canTrigger) : ask;
116
+ return entries.findLastIndex(canTrigger);
117
117
  }
118
118
  *sendTriggerLast(entries, generation) {
119
119
  const passives = entries.filter((entry) => entry.flushDelivery === "passive");
@@ -124,14 +124,15 @@ export class InboundDeliveryOwner {
124
124
  }
125
125
  const index = this.triggerIndex(rest);
126
126
  const trigger = index === -1 ? undefined : rest.splice(index, 1)[0];
127
- if (trigger && this.journal.get(trigger.message.id) === trigger) {
128
- this.hooks.replies.queueTurnContext({ ...trigger, receivedAt: Date.now() });
127
+ const replyTarget = entries.find((entry) => entry.message.expectsReply === true) ?? trigger;
128
+ if (replyTarget && this.journal.get(replyTarget.message.id) === replyTarget) {
129
+ this.hooks.replies.queueTurnContext({ ...replyTarget, receivedAt: Date.now() });
129
130
  }
130
131
  for (const entry of rest) {
131
132
  if (!this.live(generation)) {
132
133
  return;
133
134
  }
134
- this.send(entry, entry.flushDelivery === "steer" ? "steer" : "followUp", generation);
135
+ this.send(entry, requestedDelivery(entry.message) === "queue" ? "followUp" : "steer", generation);
135
136
  yield;
136
137
  }
137
138
  if (trigger) {
@@ -150,7 +151,7 @@ export class InboundDeliveryOwner {
150
151
  live(generation) {
151
152
  return this.lifecycle.generation === generation && this.lifecycle.live() !== null;
152
153
  }
153
- *sendSteers(entries, generation, onDetach) {
154
+ *sendSteers(entries, generation) {
154
155
  for (const entry of entries) {
155
156
  if (!this.live(generation)) {
156
157
  return;
@@ -159,9 +160,6 @@ export class InboundDeliveryOwner {
159
160
  continue;
160
161
  }
161
162
  this.send(entry, "steer", generation);
162
- if (requestedDelivery(entry.message) === "steer") {
163
- onDetach(this.detach(entry, entry.message.expectsReply !== true));
164
- }
165
163
  yield;
166
164
  }
167
165
  }
@@ -184,13 +182,7 @@ export class InboundDeliveryOwner {
184
182
  this.schedule(500);
185
183
  return;
186
184
  }
187
- return this.runBatch(async () => {
188
- const detachments = [];
189
- await runCooperatively(this.sendSteers(steers, generation, (task) => {
190
- detachments.push(task);
191
- }));
192
- await Promise.all(detachments);
193
- }, generation);
185
+ return this.runBatch(() => runCooperatively(this.sendSteers(steers, generation)), generation);
194
186
  }
195
187
  messageOptions(delivery) {
196
188
  if (delivery === "trigger") {
@@ -229,56 +221,20 @@ export class InboundDeliveryOwner {
229
221
  }
230
222
  }
231
223
  nativeMessage(entry) {
232
- const sender = entry.from.name === undefined || entry.from.name === ""
233
- ? entry.from.id.slice(0, 8)
234
- : entry.from.name;
224
+ const sender = entry.from.name ?? entry.from.id.slice(0, 8);
235
225
  const command = entry.replyCommand ?? "";
236
226
  const reply = command === "" ? "" : `\n\nTo reply, use the intercom tool: ${command}`;
227
+ const hint = entry.message.replyHint;
228
+ const publication = hint
229
+ ? `\n\nTo publish a correlated reply, use ${hint.tool} with commandId ${hint.commandId}. This is not an intercom ask.`
230
+ : "";
237
231
  return {
238
232
  customType: "intercom_message",
239
- content: `**📨 From ${sender}** (Native session cwd: ${entry.from.cwd})${reply}\n\n${entry.bodyText}`,
233
+ content: `**📨 From ${sender}** (Native session cwd: ${entry.from.cwd})${reply}${publication}\n\n${entry.bodyText}`,
240
234
  display: true,
241
235
  details: { ...entry, bodyText: entry.bodyText },
242
236
  };
243
237
  }
244
- async detach(entry, attention = false) {
245
- if (!attention && entry.message.expectsReply !== true) {
246
- return false;
247
- }
248
- const requestId = randomUUID();
249
- return await new Promise((resolve) => {
250
- let settled = false;
251
- const finish = (accepted) => {
252
- if (settled) {
253
- return;
254
- }
255
- settled = true;
256
- clearTimeout(timer);
257
- unsubscribe();
258
- resolve(accepted);
259
- };
260
- const timer = setTimeout(() => finish(false), 500);
261
- timer.unref();
262
- const unsubscribe = this.pi.events.on("pi-intercom:detach-response", (payload) => {
263
- if (isRecord(payload) && payload.requestId === requestId) {
264
- finish(payload.accepted === true);
265
- }
266
- });
267
- try {
268
- this.pi.events.emit("pi-intercom:detach-request", {
269
- requestId,
270
- ...(attention ? { reason: "attention" } : {}),
271
- });
272
- // Owned waits answer ordinary attention synchronously; an unanswered probe completes now.
273
- if (attention) {
274
- finish(false);
275
- }
276
- }
277
- catch {
278
- finish(false);
279
- }
280
- });
281
- }
282
238
  async redeliver(ctx, generation) {
283
239
  if (this.batchTask) {
284
240
  await this.batchTask;
@@ -1,5 +1,8 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isContextRequest } from "./lifecycle.js";
1
3
  import { INBOUND_CHECKPOINT_TYPE, } from "./runtime-types.js";
2
- import { isInboundCheckpoint } from "./inbound-record.js";
4
+ import { isInboundCheckpoint, publicationMetadata, requestedDelivery } from "./inbound-record.js";
5
+ import { isReceiptQuery, } from "./types.js";
3
6
  import { inboundIdFromCustomMessage } from "./message-format.js";
4
7
  import { isRecord } from "./validation.js";
5
8
  export class InboundJournal {
@@ -14,6 +17,72 @@ export class InboundJournal {
14
17
  this.pi = pi;
15
18
  this.lifecycle = lifecycle;
16
19
  this.replies = replies;
20
+ pi.events.on("pi-intercom:provenance-query", (request) => {
21
+ if (!isContextRequest(request)) {
22
+ return;
23
+ }
24
+ request.respond(request.version === 1 && isReceiptQuery(request)
25
+ ? this.receipt(request)
26
+ : { error: "publication_unavailable" });
27
+ });
28
+ }
29
+ receipt(query) {
30
+ const ctx = this.lifecycle.live();
31
+ if (!ctx || ctx.sessionManager.getSessionId() !== query.rootSessionId) {
32
+ return { error: "publication_unavailable" };
33
+ }
34
+ let receipt;
35
+ for (const item of ctx.sessionManager.getEntries()) {
36
+ receipt = this.receiptEntry(receipt, query, item);
37
+ }
38
+ return receipt ? { receipt } : { error: "receipt_not_found" };
39
+ }
40
+ receiptEntry(receipt, query, item) {
41
+ if (item.type === "custom" &&
42
+ item.customType === INBOUND_CHECKPOINT_TYPE &&
43
+ isInboundCheckpoint(item.data) &&
44
+ item.data.sessionId === query.rootSessionId) {
45
+ return this.receiptCheckpoint(receipt, query, item.id, item.data);
46
+ }
47
+ return receipt && inboundIdFromCustomMessage(item) === query.messageId
48
+ ? { ...receipt, placementEntryId: item.id }
49
+ : receipt;
50
+ }
51
+ receiptCheckpoint(receipt, query, entryId, data) {
52
+ if ("entry" in data) {
53
+ if (receipt || data.entry.message.id !== query.messageId) {
54
+ return receipt;
55
+ }
56
+ const { from, message, stage, receivedAt } = data.entry;
57
+ return {
58
+ version: 1,
59
+ rootSessionId: query.rootSessionId,
60
+ messageId: query.messageId,
61
+ sender: {
62
+ peerId: from.id,
63
+ rootSessionId: from.rootSessionId,
64
+ incarnation: from.incarnation,
65
+ },
66
+ textSha256: createHash("sha256").update(message.content.text, "utf8").digest("hex"),
67
+ delivery: requestedDelivery(message),
68
+ ...publicationMetadata(message),
69
+ checkpointEntryId: entryId,
70
+ receivedAt,
71
+ queued: stage === "queued",
72
+ nativeHandoff: stage === "native",
73
+ placementEntryId: null,
74
+ discarded: false,
75
+ };
76
+ }
77
+ if (!receipt || data.messageId !== query.messageId) {
78
+ return receipt;
79
+ }
80
+ return {
81
+ ...receipt,
82
+ queued: receipt.queued || data.stage === "queued",
83
+ nativeHandoff: receipt.nativeHandoff || data.stage === "native",
84
+ discarded: receipt.discarded || data.stage === "discarded",
85
+ };
17
86
  }
18
87
  peerLeft(id) {
19
88
  for (const pending of this.replies.listPending()) {
@@ -24,7 +93,7 @@ export class InboundJournal {
24
93
  this.replies.expireSender(id);
25
94
  for (const pending of this.queued()) {
26
95
  if (pending.from.id === id && pending.message.expectsReply === true) {
27
- this.pending.delete(pending.message.id);
96
+ this.consume(pending.message.id);
28
97
  this.checkpoint({ messageId: pending.message.id, stage: "discarded" });
29
98
  }
30
99
  }
@@ -42,7 +111,10 @@ export class InboundJournal {
42
111
  return this.pending.get(id);
43
112
  }
44
113
  owns(entry) {
45
- return !("stage" in entry) || this.pending.get(entry.message.id) === entry;
114
+ const id = entry.message.id;
115
+ return "stage" in entry
116
+ ? this.pending.get(id) === entry
117
+ : !this.pending.has(id) && !this.consumed.has(id);
46
118
  }
47
119
  checkpoint(update) {
48
120
  const sessionId = this.lifecycle.currentSessionId;
@@ -77,7 +149,7 @@ export class InboundJournal {
77
149
  if (pending.stage === "queued" &&
78
150
  pending.from.id === entry.from.id &&
79
151
  pending.message.threadId === entry.message.threadId) {
80
- this.pending.delete(pending.message.id);
152
+ this.consume(pending.message.id);
81
153
  this.replies.markReplied(pending.message.id);
82
154
  }
83
155
  }
@@ -135,7 +207,7 @@ export class InboundJournal {
135
207
  return;
136
208
  }
137
209
  if (data.stage === "discarded") {
138
- this.pending.delete(data.messageId);
210
+ this.consume(data.messageId);
139
211
  this.replies.markReplied(data.messageId);
140
212
  return;
141
213
  }
@@ -163,6 +235,11 @@ export class InboundJournal {
163
235
  if (item.customType === "intercom_sent") {
164
236
  this.restoreSent(item.data);
165
237
  }
238
+ if (item.customType === "intercom_received" &&
239
+ isRecord(item.data) &&
240
+ typeof item.data.messageId === "string") {
241
+ this.consume(item.data.messageId);
242
+ }
166
243
  }
167
244
  restoreSent(data) {
168
245
  if (isRecord(data) &&
@@ -194,7 +271,7 @@ export class InboundJournal {
194
271
  return ((entry.type === "custom_message" &&
195
272
  ["intercom_message", "subagent-human-message"].includes(entry.customType)) ||
196
273
  (entry.type === "custom" &&
197
- [INBOUND_CHECKPOINT_TYPE, "intercom_sent"].includes(entry.customType)));
274
+ [INBOUND_CHECKPOINT_TYPE, "intercom_sent", "intercom_received"].includes(entry.customType)));
198
275
  }
199
276
  recentEntries(ctx, fullScan) {
200
277
  if (fullScan) {
@@ -1,10 +1,25 @@
1
1
  import { isMessage, isSessionRegistration } from "./types.js";
2
2
  import { finiteNumber, isRecord } from "./validation.js";
3
+ export function publicationMetadata(message) {
4
+ return {
5
+ guard: message.guard && {
6
+ rootSessionId: message.guard.rootSessionId,
7
+ peerId: message.guard.peerId,
8
+ incarnation: message.guard.incarnation,
9
+ applicationGuard: message.guard.applicationGuard,
10
+ },
11
+ replyHint: message.replyHint && {
12
+ tool: message.replyHint.tool,
13
+ commandId: message.replyHint.commandId,
14
+ commandFingerprint: message.replyHint.commandFingerprint,
15
+ },
16
+ };
17
+ }
3
18
  export function requestedDelivery(message) {
4
19
  if (message.passive === true || message.delivery === "passive") {
5
20
  return "passive";
6
21
  }
7
- return message.delivery ?? (message.expectsReply === true ? "auto" : "steer");
22
+ return message.delivery ?? "steer";
8
23
  }
9
24
  function isLocalJournalMessage(value) {
10
25
  if (!isRecord(value) || !isRecord(value.content)) {
@@ -25,6 +40,8 @@ function isLocalJournalMessage(value) {
25
40
  "queueMode",
26
41
  "threadId",
27
42
  "passive",
43
+ "guard",
44
+ "replyHint",
28
45
  ].every((key) => value[key] === undefined));
29
46
  }
30
47
  function isJournalMessage(value, sender) {
@@ -45,65 +45,43 @@ export class IntercomInbound {
45
45
  }
46
46
  return true;
47
47
  }
48
- async busyNonInteractive(entry, ctx, generation) {
49
- if (entry.message.expectsReply !== true) {
50
- this.owners.delivery.send(entry, "steer", generation);
51
- return;
52
- }
53
- const active = this.owners.connection.active;
54
- if (entry.message.replyTo !== undefined || active?.isConnected() !== true) {
55
- return;
56
- }
57
- try {
58
- const result = await active.send(entry.from.id, {
59
- text: "This agent is running in non-interactive mode and cannot respond to intercom messages while it is working. It will continue its current task and exit when done.",
60
- replyTo: entry.message.id,
61
- });
62
- if (result.delivered && this.lifecycle.live(ctx, generation)) {
63
- this.owners.replies.markReplied(entry.message.id);
64
- this.lifecycle.markActivity();
65
- this.owners.connection.syncStatus();
66
- }
67
- }
68
- catch {
69
- /* Best-effort busy reply; the non-interactive task must continue either way. */
70
- }
71
- }
72
48
  async admit(entry, live, generation) {
73
49
  const active = this.lifecycle.live(live, generation);
74
50
  if (!active || this.queueAdmission(entry, active)) {
75
51
  return;
76
52
  }
77
53
  if (!this.lifecycle.isIdle(active)) {
78
- await this.busy(entry, active, generation);
54
+ this.busy(entry, generation);
79
55
  return;
80
56
  }
81
57
  if (this.lifecycle.live(live, generation)) {
82
58
  this.owners.delivery.send(entry, requestedDelivery(entry.message) === "passive" ? "passive" : "trigger", generation);
83
59
  }
84
60
  }
85
- async busy(entry, ctx, generation) {
61
+ busy(entry, generation) {
86
62
  const delivery = requestedDelivery(entry.message);
87
63
  if (delivery === "steer") {
88
64
  this.owners.delivery.send(entry, "steer", generation);
89
- await this.owners.delivery.detach(entry, entry.message.expectsReply !== true);
90
65
  return;
91
66
  }
92
- if (delivery === "queue" && entry.message.queueMode !== "replace") {
67
+ if (delivery === "queue") {
93
68
  this.owners.delivery.send(entry, "followUp", generation);
94
69
  return;
95
70
  }
96
- if (delivery === "passive") {
97
- this.owners.delivery.queue(entry, "passive");
98
- return;
71
+ this.owners.delivery.queue(entry, "passive");
72
+ }
73
+ /** Counted synchronously so a parking fence observes frames received before its ack. */
74
+ receiving = 0;
75
+ async receive(ctx, from, input) {
76
+ this.receiving++;
77
+ try {
78
+ await this.receiveCounted(ctx, from, input);
99
79
  }
100
- if (!ctx.hasUI) {
101
- await this.busyNonInteractive(entry, ctx, generation);
102
- return;
80
+ finally {
81
+ this.receiving--;
103
82
  }
104
- this.owners.delivery.queue(entry, "auto");
105
83
  }
106
- async receive(ctx, from, input) {
84
+ async receiveCounted(ctx, from, input) {
107
85
  const generation = this.lifecycle.generation;
108
86
  const restore = this.owners.delivery.restoring;
109
87
  if (restore) {
@@ -113,9 +91,17 @@ export class IntercomInbound {
113
91
  if (!live) {
114
92
  return;
115
93
  }
94
+ if (input.guard && !this.lifecycle.accepts(input.guard)) {
95
+ return;
96
+ }
116
97
  // Peer metadata never grants user authority.
117
98
  const message = input.human ? { ...input, human: undefined } : input;
99
+ const entry = this.entry(from, message);
100
+ if (!this.owners.journal.owns(entry)) {
101
+ return;
102
+ }
118
103
  if (this.owners.wait.receive(from, message, () => {
104
+ this.owners.journal.consume(message.id);
119
105
  this.lifecycle.markActivity();
120
106
  this.owners.connection.syncStatus();
121
107
  })) {
@@ -124,7 +110,6 @@ export class IntercomInbound {
124
110
  this.owners.replies.recordIncomingMessage(from, message);
125
111
  this.lifecycle.markActivity();
126
112
  this.owners.connection.syncStatus();
127
- const entry = this.entry(from, message);
128
113
  await this.admit(entry, live, generation);
129
114
  }
130
115
  }
@@ -12,6 +12,18 @@ import { IntercomInspection } from "./inspection.js";
12
12
  import { IntercomTools } from "./tools.js";
13
13
  import { IntercomSessionEvents } from "./session-events.js";
14
14
  import { registerIntercomTools } from "./tool-registration.js";
15
+ import { isParkRequest, PARK, PARKED_HISTORY, WAKE } from "../shared/parking.js";
16
+ /** The agent runtime answers for its exited parked child from the captured native session. */
17
+ function parkedHistory(pi, peer) {
18
+ let history;
19
+ pi.events.emit(PARKED_HISTORY, {
20
+ peer,
21
+ reply: (found) => {
22
+ history = found;
23
+ },
24
+ });
25
+ return history ?? Promise.reject(new Error("No agent runtime owns parked history."));
26
+ }
15
27
  export default function piIntercomExtension(pi) {
16
28
  const config = loadConfig();
17
29
  const lifecycle = new IntercomLifecycle(pi, config, readIntercomLineage());
@@ -21,6 +33,7 @@ export default function piIntercomExtension(pi) {
21
33
  const connection = new IntercomConnection(lifecycle, config, {
22
34
  restoring: () => delivery.restoring,
23
35
  pendingAsks: () => replies.listPending().length,
36
+ receipt: (query) => journal.receipt(query),
24
37
  incoming: (ctx, from, message) => inbound.receive(ctx, from, message),
25
38
  peerLeft: (id) => {
26
39
  wait.peerDisconnected(id);
@@ -29,13 +42,19 @@ export default function piIntercomExtension(pi) {
29
42
  disconnected: (error) => {
30
43
  wait.reject(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }));
31
44
  },
45
+ parkedHistory: (peer) => parkedHistory(pi, peer.id),
46
+ wake: (peer) => {
47
+ pi.events.emit(WAKE, peer);
48
+ },
32
49
  });
33
50
  const delivery = new InboundDeliveryOwner(pi, lifecycle, journal, {
34
51
  replies,
35
52
  syncStatus: () => connection.syncStatus(),
36
53
  });
54
+ lifecycle.registerContextEvents(() => connection.syncStatus());
37
55
  const inbound = new IntercomInbound(lifecycle, config, {
38
56
  delivery,
57
+ journal,
39
58
  replies,
40
59
  wait,
41
60
  connection,
@@ -52,4 +71,26 @@ export default function piIntercomExtension(pi) {
52
71
  wait,
53
72
  }).register();
54
73
  registerIntercomTools(pi, tools);
74
+ pi.events.on(PARK, (request) => {
75
+ if (isParkRequest(request)) {
76
+ request.reply(request.park ? connection.park() : connection.unpark());
77
+ }
78
+ });
79
+ // Native fork sleep readiness; it vetoes only this package's own parking, never other saves.
80
+ Reflect.apply(pi.on.bind(pi), undefined, [
81
+ "working_session_save",
82
+ () => !connection.parking
83
+ ? undefined
84
+ : {
85
+ blockers: [
86
+ inbound.receiving > 0 && "intercom is receiving a message",
87
+ journal.size > 0 && "intercom inbound messages await delivery",
88
+ replies.listPending().length > 0 && "intercom asks await replies",
89
+ wait.waiting && "intercom is waiting for a reply",
90
+ (delivery.restoring !== undefined || delivery.batching) &&
91
+ "intercom delivery is restoring",
92
+ connection.reconnected && "intercom reconnected while parking",
93
+ ].filter((blocker) => typeof blocker === "string"),
94
+ },
95
+ ]);
55
96
  }
@@ -1,17 +1,6 @@
1
- function validPage(offset, limit) {
2
- return (Number.isSafeInteger(offset) &&
3
- offset >= 0 &&
4
- Number.isSafeInteger(limit) &&
5
- limit >= 1 &&
6
- limit <= 16384);
7
- }
1
+ import { isLastMessageQuery, LAST_MESSAGE_PAGE_LIMIT, } from "./types.js";
8
2
  function completedResponse(manager, entryId) {
9
- let id = manager.getLeafId();
10
- while (id !== null) {
11
- const entry = manager.getEntry(id);
12
- if (!entry) {
13
- throw new Error("Native session history is unavailable.");
14
- }
3
+ for (const entry of manager.getBranch().reverse()) {
15
4
  if (entry.type === "message" &&
16
5
  entry.message.role === "assistant" &&
17
6
  (entry.message.stopReason === "stop" || entry.message.stopReason === "length") &&
@@ -24,15 +13,14 @@ function completedResponse(manager, entryId) {
24
13
  .join("\n"),
25
14
  };
26
15
  }
27
- id = entry.parentId;
28
16
  }
29
17
  return;
30
18
  }
31
19
  /** Reads finalized native history only; no model invocation or synthesized response. */
32
20
  export function readLastMessage(manager, session, query) {
33
21
  const offset = query.offset ?? 0;
34
- const limit = query.limit ?? 16384;
35
- if (!validPage(offset, limit)) {
22
+ const limit = query.limit ?? LAST_MESSAGE_PAGE_LIMIT;
23
+ if (!isLastMessageQuery({ offset, limit })) {
36
24
  return {
37
25
  error: "Invalid last response page: offset must be non-negative and limit must be 1–16384.",
38
26
  };