@selesai/code 0.13.38 → 0.13.40

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 (92) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/core/intercom-rpc.d.ts +135 -0
  3. package/dist/core/intercom-rpc.js +64 -0
  4. package/dist/core/release-rpc.d.ts +137 -0
  5. package/dist/core/release-rpc.js +77 -0
  6. package/dist/core/resource-loader.d.ts +3 -0
  7. package/dist/core/resource-loader.js +3 -0
  8. package/dist/core/subagent-rpc.d.ts +110 -0
  9. package/dist/core/subagent-rpc.js +60 -0
  10. package/dist/extensions/agent-browser.ts +9 -0
  11. package/dist/extensions/pi-graft/cli.test.ts +5 -5
  12. package/dist/extensions/pi-graft/cli.ts +10 -5
  13. package/dist/extensions/pi-graft/commands.test.ts +5 -5
  14. package/dist/extensions/pi-graft/graft.integration.test.ts +1 -1
  15. package/dist/extensions/pi-graft/lifecycle.test.ts +26 -26
  16. package/dist/extensions/pi-graft/prompt.test.ts +1 -1
  17. package/dist/extensions/pi-graft/state.test.ts +9 -9
  18. package/dist/extensions/pi-graft/test-support.ts +1 -1
  19. package/dist/extensions/pi-graft/tools.test.ts +1 -1
  20. package/dist/extensions/pi-intercom/README.md +15 -0
  21. package/dist/extensions/pi-intercom/broker/broker.ts +277 -13
  22. package/dist/extensions/pi-intercom/broker/client.ts +6 -2
  23. package/dist/extensions/pi-intercom/broker/mailbox-persistence.test.ts +523 -0
  24. package/dist/extensions/pi-intercom/broker/paths.test.ts +4 -4
  25. package/dist/extensions/pi-intercom/broker/paths.ts +4 -2
  26. package/dist/extensions/pi-intercom/broker/spawn.test.ts +1 -3
  27. package/dist/extensions/pi-intercom/extension-api.ts +10 -0
  28. package/dist/extensions/pi-intercom/index.ts +188 -8
  29. package/dist/extensions/pi-intercom/intercom.integration.test.ts +280 -0
  30. package/dist/extensions/pi-intercom/package.json +1 -1
  31. package/dist/extensions/pi-intercom/release-readiness.test.ts +116 -0
  32. package/dist/extensions/pi-intercom/release-readiness.ts +93 -0
  33. package/dist/extensions/pi-subagents/docs/extension-api.md +8 -0
  34. package/dist/extensions/pi-subagents/docs/observability.md +8 -0
  35. package/dist/extensions/pi-subagents/src/extension/index.ts +51 -1
  36. package/dist/extensions/pi-subagents/src/extension/observability.ts +164 -0
  37. package/dist/extensions/pi-subagents/src/extension/release-readiness.ts +367 -0
  38. package/dist/extensions/pi-subagents/src/extension/rpc.ts +8 -1
  39. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +7 -1
  40. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +1 -44
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +6 -0
  42. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +13 -3
  43. package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +269 -7
  44. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +11 -10
  45. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +15 -3
  46. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +64 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -6
  48. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -1
  49. package/dist/extensions/pi-subagents/src/shared/owner-record.ts +107 -0
  50. package/dist/extensions/pi-subagents/src/shared/process-identity.ts +87 -0
  51. package/dist/extensions/pi-subagents/src/shared/types.ts +9 -0
  52. package/dist/extensions/pi-subagents/test/integration/async-attention-clearing.test.ts +191 -0
  53. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +63 -0
  54. package/dist/extensions/pi-subagents/test/integration/stop-all-background.test.ts +142 -0
  55. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +8 -0
  56. package/dist/extensions/pi-subagents/test/unit/observability.test.ts +198 -0
  57. package/dist/extensions/pi-subagents/test/unit/process-identity.test.ts +75 -0
  58. package/dist/extensions/pi-subagents/test/unit/release-readiness.test.ts +715 -0
  59. package/dist/extensions/pi-subagents/test/unit/result-takeover.test.ts +332 -0
  60. package/dist/extensions/tokenin-onboarding.ts +142 -3
  61. package/dist/index.d.ts +6 -1
  62. package/dist/index.js +7 -0
  63. package/dist/main.d.ts +6 -0
  64. package/dist/main.js +1 -1
  65. package/dist/modes/index.d.ts +1 -1
  66. package/dist/modes/rpc/intercom-bridge.d.ts +14 -0
  67. package/dist/modes/rpc/intercom-bridge.js +75 -0
  68. package/dist/modes/rpc/intercom-bridge.test.d.ts +1 -0
  69. package/dist/modes/rpc/intercom-bridge.test.js +65 -0
  70. package/dist/modes/rpc/release-bridge.d.ts +35 -0
  71. package/dist/modes/rpc/release-bridge.js +159 -0
  72. package/dist/modes/rpc/release-bridge.test.d.ts +1 -0
  73. package/dist/modes/rpc/release-bridge.test.js +347 -0
  74. package/dist/modes/rpc/rpc-client.d.ts +34 -10
  75. package/dist/modes/rpc/rpc-client.js +45 -2
  76. package/dist/modes/rpc/rpc-intercom-transport.test.d.ts +1 -0
  77. package/dist/modes/rpc/rpc-intercom-transport.test.js +85 -0
  78. package/dist/modes/rpc/rpc-mode.d.ts +1 -13
  79. package/dist/modes/rpc/rpc-mode.js +239 -8
  80. package/dist/modes/rpc/rpc-release-transport.test.d.ts +1 -0
  81. package/dist/modes/rpc/rpc-release-transport.test.js +329 -0
  82. package/dist/modes/rpc/rpc-subagent-transport.test.d.ts +1 -0
  83. package/dist/modes/rpc/rpc-subagent-transport.test.js +60 -0
  84. package/dist/modes/rpc/rpc-types.d.ts +12 -2
  85. package/dist/modes/rpc/rpc-types.js +0 -6
  86. package/dist/modes/rpc/subagent-bridge.d.ts +23 -0
  87. package/dist/modes/rpc/subagent-bridge.js +93 -0
  88. package/dist/modes/rpc/test-fixtures/intercom-host.d.ts +1 -0
  89. package/dist/modes/rpc/test-fixtures/intercom-host.js +13 -0
  90. package/docs/rpc-subagent.schema.json +491 -0
  91. package/docs/rpc.md +210 -1
  92. package/package.json +2 -1
@@ -25,7 +25,14 @@ import {
25
25
  type IntercomOutboxResultCode,
26
26
  type IntercomOutboxResultStatus,
27
27
  type IntercomOutboxResultV1,
28
+ INTERCOM_RPC_REQUEST_EVENT, INTERCOM_RPC_RESULT_EVENT, INTERCOM_RPC_EVENT,
29
+ intercomRpcTimeout, type IntercomRpcRequest, type IntercomRpcResult,
30
+ type IntercomRpcEventData as IntercomRpcEvent, type IntercomRpcDelivery,
31
+ RELEASE_READINESS_EVENT, answerReleaseReadiness,
32
+ STOP_ALL_BACKGROUND_EVENT, answerStopAllBackground,
28
33
  } from "./extension-api.ts";
34
+ import { intercomReleaseBlockers, stopIntercom } from "./release-readiness.ts";
35
+ import { isMessage } from "./broker/protocol.ts";
29
36
  import { ReplyTracker } from "./reply-tracker.ts";
30
37
  import { resolve as resolvePath } from "node:path";
31
38
  import { sameCwd } from "./cwd.ts";
@@ -664,10 +671,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
664
671
  let replyWaiter: {
665
672
  from: string;
666
673
  replyTo: string;
674
+ startedAt: number;
667
675
  resolve: (message: Message) => void;
668
676
  reject: (error: Error) => void;
669
677
  } | null = null;
670
- function waitForReply(from: string, replyTo: string, signal?: AbortSignal, cancelOnAbort?: () => void, getDeliveryState: () => string = () => "unknown"): Promise<Message> {
678
+ function waitForReply(from: string, replyTo: string, signal?: AbortSignal, cancelOnAbort?: () => void, getDeliveryState: () => string = () => "unknown", timeoutMs = askTimeoutMs): Promise<Message> {
671
679
  if (replyWaiter) {
672
680
  return Promise.reject(new Error("Already waiting for a reply"));
673
681
  }
@@ -676,9 +684,9 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
676
684
  }
677
685
  return new Promise((resolve, reject) => {
678
686
  const timeout = setTimeout(() => {
679
- const timeoutDescription = askTimeoutMs % 60000 === 0 ? `${askTimeoutMs / 60000} minutes` : `${askTimeoutMs}ms`;
687
+ const timeoutDescription = timeoutMs % 60000 === 0 ? `${timeoutMs / 60000} minutes` : `${timeoutMs}ms`;
680
688
  rejectReplyWaiter(new Error(`No reply from "${from}" for message ${replyTo} within ${timeoutDescription}. Last known delivery state: ${getDeliveryState()}. This waiter timeout is not cancellation; the delivered message may still be queued or actionable in the recipient session.`));
681
- }, askTimeoutMs);
689
+ }, timeoutMs);
682
690
  const cleanup = () => {
683
691
  clearTimeout(timeout);
684
692
  signal?.removeEventListener("abort", onAbort);
@@ -695,6 +703,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
695
703
  replyWaiter = {
696
704
  from,
697
705
  replyTo,
706
+ startedAt: Date.now(),
698
707
  resolve: (message) => {
699
708
  cleanup();
700
709
  resolve(message);
@@ -1162,6 +1171,126 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1162
1171
  }
1163
1172
  })();
1164
1173
  }
1174
+ function emitRpcEvent(event: IntercomRpcEvent): void {
1175
+ pi.events.emit(INTERCOM_RPC_EVENT, { sessionId: currentSessionId, event });
1176
+ }
1177
+ function emitRpcConnection(connected: boolean, reason?: string): void {
1178
+ emitRpcEvent({ type: "intercom_connection", enabled: config.enabled, connected,
1179
+ sessionId: currentIntercomSessionId, ...(reason ? { reason } : {}) });
1180
+ }
1181
+ const rpcRequests = new Map<string, { controller: AbortController; settle(result: IntercomRpcResult): void }>();
1182
+ function endRpcRequests(): void {
1183
+ for (const [requestId, request] of [...rpcRequests]) {
1184
+ request.settle({ requestId, success: false, error: "Intercom session ended" });
1185
+ request.controller.abort();
1186
+ }
1187
+ }
1188
+ function handleRpcRequest(payload: unknown): void {
1189
+ const request = payload as IntercomRpcRequest;
1190
+ if (!request || typeof request.accept !== "function" || !request.command || !request.signal) return;
1191
+ request.accept();
1192
+ const { requestId, command } = request;
1193
+ const controller = new AbortController();
1194
+ const signal = AbortSignal.any([request.signal, controller.signal]);
1195
+ const generation = runtimeGeneration;
1196
+ const ctx = getLiveContext();
1197
+ const settle = (result: IntercomRpcResult) => {
1198
+ signal.removeEventListener("abort", onAbort);
1199
+ if (!rpcRequests.delete(requestId)) return;
1200
+ pi.events.emit(INTERCOM_RPC_RESULT_EVENT, result);
1201
+ };
1202
+ const onAbort = () => settle({ requestId, success: false, error: "Intercom session ended or request timed out" });
1203
+ rpcRequests.set(requestId, { controller, settle });
1204
+ signal.addEventListener("abort", onAbort, { once: true });
1205
+ const checkLive = () => {
1206
+ if (signal.aborted || !getLiveContext(ctx, generation)) throw new Error("Intercom session ended or request timed out");
1207
+ };
1208
+ void (async () => {
1209
+ if (!["intercom_status", "intercom_list", "intercom_send", "intercom_ask", "intercom_reply", "intercom_pending"].includes(command.type)) throw new Error("Unknown Intercom RPC command");
1210
+ if (signal.aborted) throw new Error("Intercom request ended");
1211
+ const timeoutMs = intercomRpcTimeout(command);
1212
+ if (command.type === "intercom_status") {
1213
+ return { enabled: config.enabled, connected: Boolean(client?.isConnected()), sessionId: currentIntercomSessionId,
1214
+ ...(currentSessionId ? { name: buildPresenceIdentity(pi, currentIntercomSessionId ?? currentSessionId).name } : {}) };
1215
+ }
1216
+ if (!config.enabled) throw new Error("Intercom disabled");
1217
+ checkLive();
1218
+ if (command.type === "intercom_pending") return { asks: replyTracker.listPending() };
1219
+ if (command.type !== "intercom_list") {
1220
+ if (typeof command.message !== "string" || !command.message.trim()) throw new Error("message must be a non-empty string");
1221
+ if (command.to !== undefined && (typeof command.to !== "string" || !command.to.trim())) throw new Error("to must be a non-empty string");
1222
+ if (command.type !== "intercom_reply" && !command.to) throw new Error("to is required");
1223
+ if (command.replyTo !== undefined && (typeof command.replyTo !== "string" || !command.replyTo.trim())) throw new Error("replyTo must be a non-empty string");
1224
+ if (!isMessage({ id: "validation", timestamp: Date.now(), replyTo: command.replyTo,
1225
+ content: { text: command.message, attachments: command.attachments } })) throw new Error("Invalid message or attachments");
1226
+ }
1227
+ const activeClient = await ensureConnected("tool");
1228
+ checkLive();
1229
+ if (command.type === "intercom_list") {
1230
+ const sessions = await activeClient.listSessions();
1231
+ checkLive();
1232
+ return { sessionId: activeClient.sessionId!, sessions };
1233
+ }
1234
+ let replyTo = command.replyTo;
1235
+ let to: string;
1236
+ if (command.type === "intercom_reply") {
1237
+ const target = replyTracker.resolveReplyTarget({ to: command.to, replyTo });
1238
+ to = target.from.id;
1239
+ replyTo = target.message.id;
1240
+ } else {
1241
+ to = await resolveSessionTarget(activeClient, command.to) ?? command.to;
1242
+ if (command.type === "intercom_ask" && !(await activeClient.listSessions()).some(peer => peer.id === to)) {
1243
+ throw new Error("Recipient is not currently connected; blocking asks are not queued");
1244
+ }
1245
+ }
1246
+ checkLive();
1247
+ if (currentSessionTargetMatches(to, to, activeClient)) throw new Error("Cannot message the current session");
1248
+ if (command.type === "intercom_send" && !replyTo) {
1249
+ const mismatch = replyTracker.findActiveReplyTargetMismatch(to);
1250
+ if (mismatch) throw new Error(`Use reply for active ask ${mismatch.message.id}; refusing misdirected send`);
1251
+ replyTo = replyTracker.findUniquePendingAskFrom(to)?.message.id;
1252
+ }
1253
+ if (command.type === "intercom_send" && !command.replyTo && config.confirmSend) {
1254
+ if (!ctx!.hasUI) throw new Error("confirmation_unavailable: confirmSend requires UI");
1255
+ const confirmed = await ctx!.ui.confirm("Send message", `Send to "${command.to}":\n\n${command.message}${command.attachments?.length ? formatAttachments(command.attachments) : ""}`,
1256
+ { signal, timeout: 30_000 });
1257
+ checkLive();
1258
+ if (!confirmed) throw new Error("Message cancelled by user");
1259
+ }
1260
+ const questionId = randomUUID();
1261
+ let replyPromise: Promise<Message> | undefined;
1262
+ let deliveryState = "created";
1263
+ // Reserve the shared waiter before sending, so a fast reply cannot be lost.
1264
+ if (command.type === "intercom_ask") {
1265
+ if (replyWaiter) throw new Error("Already waiting for a reply");
1266
+ replyPromise = waitForReply(to, questionId, signal, undefined,
1267
+ () => latestDeliveryState(questionId, deliveryState), timeoutMs);
1268
+ replyPromise.catch(() => undefined);
1269
+ }
1270
+ try {
1271
+ checkLive();
1272
+ const result = await activeClient.send(to, { text: command.message, attachments: command.attachments, replyTo,
1273
+ ...(replyPromise ? { messageId: questionId, expectsReply: true } : {}) });
1274
+ deliveryState = result.delivery;
1275
+ checkLive();
1276
+ const data: IntercomRpcDelivery = { messageId: result.id, delivered: result.delivered, to, replyTo,
1277
+ delivery: result.delivery, retryable: result.retryable, outcomeKnown: result.outcomeKnown,
1278
+ ...(result.code ? { code: result.code } : {}), ...(result.reason ? { reason: result.reason } : {}) };
1279
+ if (!result.delivered) throw new Error(`Message ${result.id} not delivered: ${result.reason ?? result.delivery}`);
1280
+ pi.appendEntry("intercom_sent", { to, message: { text: command.message, attachments: command.attachments, replyTo }, messageId: result.id, timestamp: Date.now() });
1281
+ if (replyTo) dismissIncomingAsk(replyTo);
1282
+ if (!replyPromise) return data;
1283
+ const reply = await replyPromise;
1284
+ checkLive();
1285
+ pi.appendEntry("intercom_received", { from: to, message: reply.content, messageId: reply.id, timestamp: reply.timestamp });
1286
+ return { ...data, reply };
1287
+ } catch (error) {
1288
+ // Only the owner of this waiter may reject it; concurrent asks fail without replacing it.
1289
+ if (replyPromise && replyWaiter?.replyTo === questionId) rejectReplyWaiter(toError(error));
1290
+ throw error;
1291
+ }
1292
+ })().then(data => settle({ requestId, success: true, data }), error => settle({ requestId, success: false, error: getErrorMessage(error) }));
1293
+ }
1165
1294
  function shouldTriggerInboundMessage(entry: InboundMessageEntry, forceTrigger = false): boolean {
1166
1295
  if (forceTrigger) {
1167
1296
  return true;
@@ -1219,6 +1348,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1219
1348
  return;
1220
1349
  }
1221
1350
  const receivedMessage = { ...message, receiverReceivedAt };
1351
+ emitRpcEvent({ type: "intercom_message", from, message: receivedMessage });
1222
1352
  emitMessageReceipt(receivedMessage.id, "receiver_received");
1223
1353
  if (replyWaiter) {
1224
1354
  const senderTarget = from.name || from.id;
@@ -1277,6 +1407,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1277
1407
  if (client !== nextClient) return;
1278
1408
  switch (message.type) {
1279
1409
  case "registered": {
1410
+ emitRpcConnection(true);
1280
1411
  const supported = message.features?.includes(EXTENSION_BUS_FEATURE) ?? false;
1281
1412
  if (supported && localExtensions.size > 0) {
1282
1413
  nextClient.updateExtensionCapabilities(currentExtensionCapabilities());
@@ -1323,7 +1454,12 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1323
1454
  ...(message.reason ? { reason: message.reason } : {}),
1324
1455
  });
1325
1456
  break;
1457
+ case "delivered":
1458
+ case "delivery_failed":
1459
+ emitRpcEvent({ type: "intercom_delivery", result: message });
1460
+ break;
1326
1461
  case "message_receipt":
1462
+ emitRpcEvent({ type: "intercom_receipt", from: message.from, receipt: message.receipt });
1327
1463
  latestOutboundReceipts.set(message.receipt.messageId, {
1328
1464
  status: message.receipt.status,
1329
1465
  timestamp: message.receipt.timestamp,
@@ -1334,16 +1470,19 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1334
1470
  handleMessageControl(message.control);
1335
1471
  break;
1336
1472
  case "session_joined":
1473
+ emitRpcEvent({ type: "intercom_session_joined", session: message.session });
1337
1474
  for (const namespace of localExtensions.keys()) {
1338
1475
  emitLocalExtensionEvent(namespace, { type: "session_joined", session: message.session });
1339
1476
  }
1340
1477
  break;
1341
1478
  case "session_left":
1479
+ emitRpcEvent({ type: "intercom_session_left", sessionId: message.sessionId });
1342
1480
  for (const namespace of localExtensions.keys()) {
1343
1481
  emitLocalExtensionEvent(namespace, { type: "session_left", sessionId: message.sessionId });
1344
1482
  }
1345
1483
  break;
1346
1484
  case "presence_update":
1485
+ emitRpcEvent({ type: "intercom_presence", session: message.session });
1347
1486
  for (const namespace of localExtensions.keys()) {
1348
1487
  emitLocalExtensionEvent(namespace, { type: "presence_update", session: message.session });
1349
1488
  }
@@ -1361,6 +1500,8 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1361
1500
  if (client !== nextClient) {
1362
1501
  return;
1363
1502
  }
1503
+ emitRpcConnection(false, error.message);
1504
+ endRpcRequests();
1364
1505
  rejectReplyWaiter(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }));
1365
1506
  for (const [namespace, extension] of localExtensions) {
1366
1507
  extension.owner = undefined;
@@ -1433,6 +1574,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1433
1574
  if (reason === "background" && getLiveContext(contextAtStart, generationAtStart)) {
1434
1575
  scheduleReconnect();
1435
1576
  }
1577
+ if (getLiveContext(contextAtStart, generationAtStart)) emitRpcConnection(false, getErrorMessage(error));
1436
1578
  throw toError(error);
1437
1579
  } finally {
1438
1580
  if (reconnectPromise === nextReconnectPromise) {
@@ -1556,6 +1698,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1556
1698
  }
1557
1699
  function startSessionRuntime(ctx: ExtensionContext): void {
1558
1700
  const previousClient = client;
1701
+ endRpcRequests();
1559
1702
  failPendingOutboxRequests(runtimeGeneration, "session_ended", "Session replaced");
1560
1703
  shuttingDown = false;
1561
1704
  disposed = false;
@@ -1576,6 +1719,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1576
1719
  currentSessionId = ctx.sessionManager.getSessionId();
1577
1720
  currentIntercomSessionId = resolveConfiguredIntercomSessionId(currentSessionId, config);
1578
1721
  publishIntercomSessionId(currentIntercomSessionId);
1722
+ emitRpcConnection(false, "Session started");
1579
1723
  currentModel = ctx.model?.id ?? "unknown";
1580
1724
  sessionStartedAt = Date.now();
1581
1725
  const initialPresenceIdentity = buildPresenceIdentity(pi, currentIntercomSessionId);
@@ -1695,9 +1839,40 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1695
1839
  acknowledge: true,
1696
1840
  });
1697
1841
  });
1842
+ const unsubscribeRpcRequest = pi.events.on(INTERCOM_RPC_REQUEST_EVENT, handleRpcRequest);
1843
+ // Release readiness (release:readiness:v1): answer only for the session this extension is bound to.
1844
+ const unsubscribeReleaseReadiness = pi.events.on(RELEASE_READINESS_EVENT, (payload) => {
1845
+ answerReleaseReadiness(payload, "intercom", currentSessionId, () => intercomReleaseBlockers({
1846
+ pendingAsks: replyTracker.listPending(),
1847
+ outboundAsk: replyWaiter,
1848
+ pendingOutboxRequests: pendingOutboxRequests.size,
1849
+ }));
1850
+ });
1851
+ // Kill switch (stop_all_background): cancel this session's blocking outbound ask; inbound asks cannot be cancelled by the receiver.
1852
+ const unsubscribeStopAll = pi.events.on(STOP_ALL_BACKGROUND_EVENT, (payload) => {
1853
+ answerStopAllBackground(payload, "intercom", currentSessionId, (request) => stopIntercom({
1854
+ pendingAsks: replyTracker.listPending(),
1855
+ outboundAsk: replyWaiter,
1856
+ cancelOutboundAsk: (ask) => {
1857
+ const activeClient = client;
1858
+ // Broker first (drops the ask edge + pending-ask record), then free the blocked tool call.
1859
+ activeClient?.cancelAsk(ask.replyTo);
1860
+ rejectReplyWaiter(new Error("Cancelled: session background work was stopped"));
1861
+ // Also ask the recipient to drop the ask. Best-effort: it may already be injected or processed.
1862
+ if (!activeClient?.isConnected()) return undefined;
1863
+ return activeClient.cancelMessage(ask.replyTo).then(
1864
+ (result) => result.delivered ? "recipient asked to drop it" : "recipient could not be asked to drop it (it may already be handled)",
1865
+ () => "recipient could not be asked to drop it (it may already be handled)",
1866
+ );
1867
+ },
1868
+ }, request));
1869
+ });
1698
1870
  const unsubscribeOutboxRequest = pi.events.on(INTERCOM_OUTBOX_REQUEST_EVENT, handleOutboxRequest);
1699
1871
  pi.on("session_start", (_event, ctx) => {
1700
1872
  if (!config.enabled) {
1873
+ runtimeContext = ctx;
1874
+ currentSessionId = ctx.sessionManager.getSessionId();
1875
+ emitRpcConnection(false, "Intercom disabled");
1701
1876
  return;
1702
1877
  }
1703
1878
  startSessionRuntime(ctx);
@@ -1708,6 +1883,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1708
1883
  unsubscribeSubagentControlIntercom();
1709
1884
  unsubscribeSubagentResultIntercom();
1710
1885
  unsubscribeOutboxRequest();
1886
+ unsubscribeRpcRequest();
1887
+ unsubscribeReleaseReadiness();
1888
+ unsubscribeStopAll();
1889
+ endRpcRequests();
1890
+ emitRpcConnection(false, "Session shutting down");
1711
1891
  shuttingDown = true;
1712
1892
  disposed = true;
1713
1893
  failPendingOutboxRequests(runtimeGeneration, "session_ended", "Session shutting down");
@@ -1976,7 +2156,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1976
2156
  replyPromise = waitForReply(sendTo, questionId, signal, () => connectedClient.cancelAsk(questionId!), () => latestDeliveryState(questionId, deliveryState));
1977
2157
  replyPromise.catch(() => undefined);
1978
2158
  if (signal?.aborted) {
1979
- rejectReplyWaiter(new Error("Cancelled"));
2159
+ if (replyWaiter?.replyTo === questionId) rejectReplyWaiter(new Error("Cancelled"));
1980
2160
  try {
1981
2161
  await replyPromise;
1982
2162
  } catch {
@@ -1998,7 +2178,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1998
2178
  deliveryState = sendResult.delivered ? "socket_delivered" : "delivery_failed";
1999
2179
  if (!sendResult.delivered) {
2000
2180
  const errorText = sendResult.reason ?? "Session may not exist or has disconnected.";
2001
- rejectReplyWaiter(new Error(`Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}`));
2181
+ if (replyWaiter?.replyTo === questionId) rejectReplyWaiter(new Error(`Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}`));
2002
2182
  if (replyPromise) {
2003
2183
  try {
2004
2184
  await replyPromise;
@@ -2044,7 +2224,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
2044
2224
  : {},
2045
2225
  };
2046
2226
  } catch (error) {
2047
- rejectReplyWaiter(toError(error));
2227
+ if (questionId && replyWaiter?.replyTo === questionId) rejectReplyWaiter(toError(error));
2048
2228
  if (replyPromise) {
2049
2229
  try {
2050
2230
  await replyPromise;
@@ -2478,7 +2658,7 @@ Usage:
2478
2658
  deliveryState = sendResult.delivery;
2479
2659
  if (!sendResult.delivered) {
2480
2660
  const errorText = sendResult.reason ?? "Session may not exist or has disconnected.";
2481
- rejectReplyWaiter(new Error(`Message to "${targetDisplay}" was not delivered: ${errorText}`));
2661
+ if (replyWaiter?.replyTo === questionId) rejectReplyWaiter(new Error(`Message to "${targetDisplay}" was not delivered: ${errorText}`));
2482
2662
  if (replyPromise) {
2483
2663
  try {
2484
2664
  await replyPromise;
@@ -2513,7 +2693,7 @@ Usage:
2513
2693
  details: target.projectPane ? { openedProjectPane: true, paneId: target.projectPane.paneId, projectRoot: target.projectPane.projectRoot } : {},
2514
2694
  };
2515
2695
  } catch (error) {
2516
- rejectReplyWaiter(toError(error));
2696
+ if (questionId && replyWaiter?.replyTo === questionId) rejectReplyWaiter(toError(error));
2517
2697
  if (replyPromise) {
2518
2698
  try {
2519
2699
  await replyPromise;
@@ -236,6 +236,7 @@ function createExtensionHarness(sessionName: string | (() => string) = "child-wo
236
236
  entries,
237
237
  sentMessages,
238
238
  getActiveTools: () => pi.getActiveTools(),
239
+ clearLifecycleHandlers: () => lifecycleHandlers.clear(),
239
240
  async emitLifecycle(event: string, payload: unknown = {}, eventContext: unknown = ctx) {
240
241
  for (const handler of lifecycleHandlers.get(event) ?? []) {
241
242
  await handler(payload, eventContext);
@@ -4349,3 +4350,282 @@ test("failed delivery from an inferred reply preserves the pending ask", { concu
4349
4350
  await cleanup();
4350
4351
  }
4351
4352
  });
4353
+
4354
+ test("structured RPC uses the shared runtime for commands, fast replies, receipts, presence and idle/busy incoming messages", { concurrency: false }, async () => {
4355
+ const { RpcIntercomBridge } = await import("../../modes/rpc/intercom-bridge.ts");
4356
+ const { INTERCOM_RPC_EVENT } = await import("./extension-api.ts");
4357
+ const { default: piIntercomExtension } = await import("./index.ts");
4358
+ const { planner, orchestrator, cleanup } = await setupClients();
4359
+ let idle = true;
4360
+ const harness = createExtensionHarness("rpc-worker", { mode: "rpc", hasUI: true, isIdle: () => idle });
4361
+ const events: Array<{ type: string; [key: string]: unknown }> = [];
4362
+ const bridge = new RpcIntercomBridge(harness.pi.events, event => events.push(event as never), harness.ctx.sessionManager.getSessionId());
4363
+ try {
4364
+ await withIntercomConfig({ inboundTrigger: "always" }, async () => {
4365
+ piIntercomExtension(harness.pi as never);
4366
+ await harness.emitLifecycle("session_start");
4367
+ const self = await waitForSessionByName(planner, "rpc-worker");
4368
+ assert.equal((await bridge.request({ type: "intercom_status" })).success, true);
4369
+ const unknown = await bridge.request({ type: "intercom_arbitrary", to: "planner", message: "must not send" } as never);
4370
+ assert.ok(!unknown.success);
4371
+ assert.match(unknown.error, /Unknown/);
4372
+ const list = await bridge.request({ type: "intercom_list" });
4373
+ assert.ok(list.success && "data" in list && "sessions" in list.data);
4374
+ assert.equal(list.data.sessions.filter(peer => peer.name === "rpc-worker").length, 1);
4375
+ assert.ok(events.some(event => event.type === "intercom_connection" && event.connected === true));
4376
+ const sent = await bridge.request({ type: "intercom_send", to: planner.sessionId!, message: "Desktop update" });
4377
+ assert.ok(sent.success && "data" in sent && "messageId" in sent.data);
4378
+ planner.sendMessageReceipt({ messageId: sent.data.messageId, status: "receiver_received", timestamp: Date.now() });
4379
+ assert.equal((await bridge.request({ type: "intercom_send", to: self.id, message: "self" })).success, false);
4380
+ assert.equal((await bridge.request({ type: "intercom_ask", to: "missing-peer", message: "?", timeoutMs: 100 })).success, false);
4381
+ assert.equal((await bridge.request({ type: "intercom_send", to: planner.sessionId!, message: "", attachments: [] })).success, false);
4382
+ assert.equal((await bridge.request({ type: "intercom_send", to: planner.sessionId!, message: "bad", attachments: [{ type: "invalid", name: "x", content: "x" }] as never })).success, false);
4383
+ const answer = (from: SessionInfo, message: Message) => {
4384
+ if (message.expectsReply && message.content.text === "Fast question") void planner.send(from.id, { text: "Fast answer", replyTo: message.id });
4385
+ };
4386
+ planner.on("message", answer);
4387
+ const asked = await bridge.request({ id: "ask-correlation", type: "intercom_ask", to: planner.sessionId!, message: "Fast question", timeoutMs: 2000 });
4388
+ planner.off("message", answer);
4389
+ assert.ok(asked.success && "data" in asked && "reply" in asked.data);
4390
+ assert.equal(asked.id, "ask-correlation");
4391
+ assert.equal(asked.data.reply.replyTo, asked.data.messageId);
4392
+ assert.equal(asked.data.reply.content.text, "Fast answer");
4393
+ const askId = "rpc-inbound-ask";
4394
+ await planner.send(self.id, { messageId: askId, text: "Inbound", expectsReply: true });
4395
+ await new Promise(resolve => setTimeout(resolve, 30));
4396
+ assert.ok(harness.sentMessages.some(item => item.options?.triggerTurn));
4397
+ const pending = await bridge.request({ type: "intercom_pending" });
4398
+ assert.ok(pending.success && "data" in pending && "asks" in pending.data);
4399
+ assert.equal(pending.data.asks[0]?.message.id, askId);
4400
+ const replyReceived = waitForReply(planner, askId);
4401
+ assert.equal((await bridge.request({ type: "intercom_reply", message: "Desktop answer", replyTo: askId })).success, true);
4402
+ assert.equal((await replyReceived).message.content.text, "Desktop answer");
4403
+ idle = false;
4404
+ const messagesBefore = harness.sentMessages.length;
4405
+ await orchestrator.send(self.id, { text: "Busy message" });
4406
+ await new Promise(resolve => setTimeout(resolve, 30));
4407
+ assert.equal(harness.sentMessages.length, messagesBefore + 1);
4408
+ assert.equal(harness.sentMessages.at(-1)?.options?.deliverAs, "steer");
4409
+ orchestrator.updatePresence({ status: "thinking" });
4410
+ await new Promise(resolve => setTimeout(resolve, 30));
4411
+ assert.ok(events.some(event => event.type === "intercom_message"));
4412
+ assert.ok(events.some(event => event.type === "intercom_receipt"));
4413
+ assert.ok(events.some(event => event.type === "intercom_delivery"));
4414
+ assert.ok(events.some(event => event.type === "intercom_presence"));
4415
+ await orchestrator.disconnect();
4416
+ await new Promise(resolve => setTimeout(resolve, 30));
4417
+ assert.ok(events.some(event => event.type === "intercom_session_left"));
4418
+ // Duplicate broker messages must not create a second structured incoming event.
4419
+ const incomingCount = events.filter(event => event.type === "intercom_message").length;
4420
+ await planner.send(self.id, { messageId: askId, text: "Inbound", expectsReply: true });
4421
+ await new Promise(resolve => setTimeout(resolve, 30));
4422
+ assert.equal(events.filter(event => event.type === "intercom_message").length, incomingCount);
4423
+ const timedOut = await bridge.request({ type: "intercom_ask", to: planner.sessionId!, message: "Unanswered", timeoutMs: 30 });
4424
+ assert.ok(!timedOut.success);
4425
+ assert.match(timedOut.error, /not cancellation/);
4426
+ const blocked = bridge.request({ type: "intercom_ask", to: planner.sessionId!, message: "Pending at shutdown", timeoutMs: 1000 });
4427
+ await new Promise(resolve => setTimeout(resolve, 30));
4428
+ const competing = await bridge.request({ type: "intercom_ask", to: planner.sessionId!, message: "Competing ask", timeoutMs: 1000 });
4429
+ assert.ok(!competing.success);
4430
+ assert.match(competing.error, /Already waiting/);
4431
+ await harness.emitLifecycle("session_shutdown");
4432
+ assert.equal((await blocked).success, false);
4433
+ assert.equal((await bridge.request({ type: "intercom_list" })).success, false);
4434
+ assert.ok(events.some(event => event.type === "intercom_connection" && event.reason === "Session shutting down"));
4435
+ // Reload installs exactly one listener on the same bus and stale results are ignored.
4436
+ harness.clearLifecycleHandlers();
4437
+ piIntercomExtension(harness.pi as never);
4438
+ await harness.emitLifecycle("session_start");
4439
+ assert.equal((await bridge.request({ type: "intercom_list" })).success, true);
4440
+ await harness.emitLifecycle("session_shutdown");
4441
+ harness.pi.events.emit(INTERCOM_RPC_EVENT, { type: "intercom_connection", enabled: true, connected: false, sessionId: null });
4442
+ });
4443
+ } finally {
4444
+ bridge.dispose();
4445
+ await harness.emitLifecycle("session_shutdown");
4446
+ await cleanup();
4447
+ }
4448
+ });
4449
+
4450
+ test("stop_all_background cancels the blocking outbound ask and reports inbound asks it cannot cancel", { concurrency: false }, async () => {
4451
+ const { RpcIntercomBridge } = await import("../../modes/rpc/intercom-bridge.ts");
4452
+ const { RpcReleaseBridge } = await import("../../modes/rpc/release-bridge.ts");
4453
+ const { default: piIntercomExtension } = await import("./index.ts");
4454
+ const { planner, orchestrator, cleanup } = await setupClients();
4455
+ const harness = createExtensionHarness("stop-all-worker", { mode: "rpc", hasUI: true, isIdle: () => true });
4456
+ const sessionId = harness.ctx.sessionManager.getSessionId();
4457
+ const intercomBridge = new RpcIntercomBridge(harness.pi.events, () => undefined, sessionId);
4458
+ const releaseBridge = new RpcReleaseBridge(harness.pi.events, { version: 1, sessionId, epoch: "e1" });
4459
+ const stopCommand = { type: "stop_all_background" as const, version: 1 as const };
4460
+ try {
4461
+ await withIntercomConfig({ inboundTrigger: "always" }, async () => {
4462
+ piIntercomExtension(harness.pi as never);
4463
+ await harness.emitLifecycle("session_start");
4464
+ const self = await waitForSessionByName(planner, "stop-all-worker");
4465
+ // Idle: nothing to stop, and the intercom contributor answered.
4466
+ const idle = await releaseBridge.stopAll(stopCommand, () => [], () => []);
4467
+ assert.ok(idle.success && idle.command === "stop_all_background");
4468
+ assert.deepEqual(idle.data.stopped, []);
4469
+
4470
+ await planner.send(self.id, { messageId: "stop-all-inbound", text: "Please answer", expectsReply: true });
4471
+ // A different peer than the inbound asker: the broker refuses mutual asks.
4472
+ const blocked = intercomBridge.request({ type: "intercom_ask", to: orchestrator.sessionId!, message: "Pending ask", timeoutMs: 20_000 });
4473
+ await new Promise(resolve => setTimeout(resolve, 100));
4474
+ const reply = await releaseBridge.stopAll(stopCommand, () => [], () => []);
4475
+ assert.ok(reply.success && reply.command === "stop_all_background");
4476
+ const intercomItems = reply.data.stopped.filter(item => item.source === "intercom");
4477
+ const outbound = intercomItems.find(item => item.kind === "outbound_ask");
4478
+ assert.ok(outbound, JSON.stringify(reply.data.stopped));
4479
+ assert.equal(outbound.ok, true);
4480
+ assert.match(outbound.detail, /^Cancelled blocking ask to /);
4481
+ assert.deepEqual(intercomItems.filter(item => item.kind === "inbound_ask").map(item => [item.id, item.ok, item.error]), [["stop-all-inbound", false, "cannot cancel inbound"]]);
4482
+ // The blocked tool call returned with the cancellation instead of waiting for its 20s timeout.
4483
+ const settled = await blocked;
4484
+ assert.equal(settled.success, false);
4485
+ assert.match((settled as { error: string }).error, /Cancelled: session background work was stopped/);
4486
+ // Readiness afterwards has no outbound ask; the inbound ask is still ours to answer.
4487
+ assert.deepEqual(reply.data.remaining.blockers.filter(b => b.source === "intercom").map(b => b.kind), ["inbound_ask"]);
4488
+ // Idempotent: only the inbound ask is reported the second time.
4489
+ const again = await releaseBridge.stopAll(stopCommand, () => [], () => []);
4490
+ assert.ok(again.success && again.command === "stop_all_background");
4491
+ assert.deepEqual(again.data.stopped.map(item => [item.kind, item.ok]), [["inbound_ask", false]]);
4492
+ });
4493
+ } finally {
4494
+ intercomBridge.dispose();
4495
+ releaseBridge.dispose();
4496
+ await harness.emitLifecycle("session_shutdown");
4497
+ await cleanup();
4498
+ }
4499
+ });
4500
+
4501
+ test("structured RPC fails closed for disabled runtime, missing backend, and rejected/unavailable confirmation", { concurrency: false }, async () => {
4502
+ const { RpcIntercomBridge } = await import("../../modes/rpc/intercom-bridge.ts");
4503
+ const { default: piIntercomExtension } = await import("./index.ts");
4504
+ const { planner, cleanup } = await setupClients();
4505
+ try {
4506
+ for (const options of [
4507
+ { config: { enabled: false }, hasUI: true },
4508
+ { config: { confirmSend: true }, hasUI: false },
4509
+ { config: { confirmSend: true }, hasUI: true },
4510
+ ]) {
4511
+ const harness = createExtensionHarness("rpc-consent", { mode: "rpc", hasUI: options.hasUI, ui: { confirm: async () => false } });
4512
+ const bridge = new RpcIntercomBridge(harness.pi.events, () => {}, harness.ctx.sessionManager.getSessionId());
4513
+ await withIntercomConfig(options.config, async () => {
4514
+ piIntercomExtension(harness.pi as never);
4515
+ await harness.emitLifecycle("session_start");
4516
+ const sent = await bridge.request({ type: "intercom_send", to: planner.sessionId!, message: "Must not send" });
4517
+ assert.ok(!sent.success);
4518
+ assert.match(sent.error, /disabled|confirmation_unavailable|cancelled/);
4519
+ if (options.config.enabled === false) {
4520
+ const status = await bridge.request({ type: "intercom_status" });
4521
+ assert.ok(status.success && "data" in status && "enabled" in status.data);
4522
+ assert.equal(status.data.enabled, false);
4523
+ }
4524
+ await harness.emitLifecycle("session_shutdown");
4525
+ const missing = await bridge.request({ type: "intercom_list" });
4526
+ assert.ok(!missing.success);
4527
+ assert.match(missing.error, /unavailable/);
4528
+ });
4529
+ bridge.dispose();
4530
+ }
4531
+ } finally { await cleanup(); }
4532
+ });
4533
+
4534
+ test("structured RPC aborts a pending confirmation on disconnect and can reconnect", { concurrency: false }, async () => {
4535
+ const { RpcIntercomBridge } = await import("../../modes/rpc/intercom-bridge.ts");
4536
+ const { default: piIntercomExtension } = await import("./index.ts");
4537
+ const { planner, cleanup } = await setupClients();
4538
+ let confirmSignal: AbortSignal | undefined;
4539
+ let signalAborted = false;
4540
+ let enteredConfirm!: () => void;
4541
+ const confirming = new Promise<void>(resolve => { enteredConfirm = resolve; });
4542
+ const harness = createExtensionHarness("rpc-disconnect-confirm", { mode: "rpc", hasUI: true,
4543
+ ui: { confirm: (_title: string, _message: string, options: { signal: AbortSignal }) => new Promise<boolean>(resolve => {
4544
+ confirmSignal = options.signal;
4545
+ options.signal.addEventListener("abort", () => { signalAborted = true; resolve(false); }, { once: true });
4546
+ enteredConfirm();
4547
+ }) } });
4548
+ const bridge = new RpcIntercomBridge(harness.pi.events, () => {}, harness.ctx.sessionManager.getSessionId());
4549
+ const originalList = IntercomClient.prototype.listSessions;
4550
+ let extensionClient: InstanceType<typeof IntercomClient> | undefined;
4551
+ try {
4552
+ await withIntercomConfig({ confirmSend: true }, async () => {
4553
+ piIntercomExtension(harness.pi as never);
4554
+ await harness.emitLifecycle("session_start");
4555
+ IntercomClient.prototype.listSessions = function (options) {
4556
+ extensionClient = this;
4557
+ return originalList.call(this, options);
4558
+ };
4559
+ assert.equal((await bridge.request({ type: "intercom_list" })).success, true);
4560
+ IntercomClient.prototype.listSessions = originalList;
4561
+ const sent = bridge.request({ type: "intercom_send", to: planner.sessionId!, message: "Do not send after disconnect" });
4562
+ await confirming;
4563
+ assert.ok(confirmSignal && !confirmSignal.aborted);
4564
+ // Destroy the live transport without graceful unregister so the real disconnect handler runs.
4565
+ (extensionClient as unknown as { socket: { destroy(): void } }).socket.destroy();
4566
+ const response = await Promise.race([sent, new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Disconnect did not settle confirmation promptly")), 1000).unref())]);
4567
+ assert.ok(!response.success);
4568
+ assert.match(response.error, /session ended/);
4569
+ assert.equal(signalAborted, true);
4570
+ assert.equal((await bridge.request({ type: "intercom_list" })).success, true);
4571
+ assert.equal(harness.entries.filter(entry => entry.type === "intercom_sent").length, 0);
4572
+ });
4573
+ } finally {
4574
+ IntercomClient.prototype.listSessions = originalList;
4575
+ bridge.dispose();
4576
+ await harness.emitLifecycle("session_shutdown");
4577
+ await cleanup();
4578
+ }
4579
+ });
4580
+
4581
+ test("structured RPC ask survives a concurrent model ask target-resolution failure", { concurrency: false }, async () => {
4582
+ const { RpcIntercomBridge } = await import("../../modes/rpc/intercom-bridge.ts");
4583
+ const { default: piIntercomExtension } = await import("./index.ts");
4584
+ const { planner, cleanup } = await setupClients();
4585
+ const harness = createExtensionHarness("rpc-model-waiter-owner", { mode: "rpc", hasUI: true });
4586
+ const bridge = new RpcIntercomBridge(harness.pi.events, () => {}, harness.ctx.sessionManager.getSessionId());
4587
+ const originalList = IntercomClient.prototype.listSessions;
4588
+ let releaseResolution!: () => void;
4589
+ let startedResolution!: () => void;
4590
+ const resolving = new Promise<void>(resolve => { startedResolution = resolve; });
4591
+ const resolutionGate = new Promise<void>(resolve => { releaseResolution = resolve; });
4592
+ try {
4593
+ piIntercomExtension(harness.pi as never);
4594
+ await harness.emitLifecycle("session_start");
4595
+ assert.equal((await bridge.request({ type: "intercom_list" })).success, true);
4596
+ IntercomClient.prototype.listSessions = async function () {
4597
+ startedResolution();
4598
+ await resolutionGate;
4599
+ throw new Error("Injected model target-resolution failure");
4600
+ };
4601
+ const tool = harness.tools.find(tool => tool.name === "intercom")!;
4602
+ const modelAsk = tool.execute("model-resolution-race", { action: "ask", to: "planner", message: "Model question" }, new AbortController().signal, undefined, harness.ctx);
4603
+ await resolving;
4604
+ IntercomClient.prototype.listSessions = originalList;
4605
+ const rpcQuestion = new Promise<{ from: SessionInfo; message: Message }>(resolve => {
4606
+ const handler = (from: SessionInfo, message: Message) => {
4607
+ if (message.content.text !== "RPC question owns waiter") return;
4608
+ planner.off("message", handler);
4609
+ resolve({ from, message });
4610
+ };
4611
+ planner.on("message", handler);
4612
+ });
4613
+ const rpcAsk = bridge.request({ type: "intercom_ask", to: planner.sessionId!, message: "RPC question owns waiter", timeoutMs: 3000 });
4614
+ const question = await rpcQuestion;
4615
+ releaseResolution();
4616
+ const modelResult = await modelAsk;
4617
+ assert.equal(modelResult.details?.error, true);
4618
+ assert.match(modelResult.content[0].text, /target-resolution failure/);
4619
+ await planner.send(question.from.id, { text: "RPC waiter was preserved", replyTo: question.message.id });
4620
+ const rpcResult = await rpcAsk;
4621
+ assert.ok(rpcResult.success && "data" in rpcResult && "reply" in rpcResult.data);
4622
+ assert.equal(rpcResult.data.reply.content.text, "RPC waiter was preserved");
4623
+ assert.equal(rpcResult.data.reply.replyTo, rpcResult.data.messageId);
4624
+ } finally {
4625
+ releaseResolution?.();
4626
+ IntercomClient.prototype.listSessions = originalList;
4627
+ bridge.dispose();
4628
+ await harness.emitLifecycle("session_shutdown");
4629
+ await cleanup();
4630
+ }
4631
+ });
@@ -18,7 +18,7 @@
18
18
  "skills/**/*"
19
19
  ],
20
20
  "scripts": {
21
- "test": "tsx --test broker/client.test.ts broker/client-liveness.test.ts broker/extension.test.ts broker/framing.test.ts broker/paths.test.ts broker/runtime-claim.test.ts broker/spawn.test.ts config.test.ts cwd.test.ts format-context.test.ts project-agent.test.ts reply-tracker.test.ts intercom.integration.test.ts test/inline-message.test.ts test/overlay-width.test.ts"
21
+ "test": "tsx --test broker/client.test.ts broker/client-liveness.test.ts broker/extension.test.ts broker/framing.test.ts broker/mailbox-persistence.test.ts broker/paths.test.ts broker/runtime-claim.test.ts broker/spawn.test.ts config.test.ts cwd.test.ts format-context.test.ts project-agent.test.ts reply-tracker.test.ts intercom.integration.test.ts test/inline-message.test.ts test/overlay-width.test.ts"
22
22
  },
23
23
  "keywords": [
24
24
  "pi-package"