@ashx-j/lunr 0.2.18 → 0.2.20

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 (190) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +2 -2
  3. package/dist/builtin-extensions/ashxj-tui.d.ts +2 -2
  4. package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
  5. package/dist/builtin-extensions/ashxj-tui.js +11 -8
  6. package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
  7. package/dist/builtin-extensions/index.d.ts.map +1 -1
  8. package/dist/builtin-extensions/index.js +0 -2
  9. package/dist/builtin-extensions/index.js.map +1 -1
  10. package/dist/builtin-extensions/pi-intercom/broker/spawn.d.ts +6 -4
  11. package/dist/builtin-extensions/pi-intercom/broker/spawn.d.ts.map +1 -1
  12. package/dist/builtin-extensions/pi-intercom/broker/spawn.js +93 -49
  13. package/dist/builtin-extensions/pi-intercom/broker/spawn.js.map +1 -1
  14. package/dist/builtin-extensions/pi-intercom/index.d.ts.map +1 -1
  15. package/dist/builtin-extensions/pi-intercom/index.js +351 -351
  16. package/dist/builtin-extensions/pi-intercom/index.js.map +1 -1
  17. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts +2 -1
  18. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  19. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +80 -43
  20. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  21. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts +18 -8
  22. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts.map +1 -1
  23. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js +36 -11
  24. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js.map +1 -1
  25. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +4 -3
  26. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
  27. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +21 -12
  28. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
  29. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.d.ts +7 -1
  30. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.d.ts.map +1 -1
  31. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.js +225 -15
  32. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.js.map +1 -1
  33. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-ask.d.ts +29 -0
  34. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-ask.d.ts.map +1 -0
  35. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-ask.js +179 -0
  36. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-ask.js.map +1 -0
  37. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.d.ts +89 -0
  38. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.d.ts.map +1 -0
  39. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.js +541 -0
  40. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.js.map +1 -0
  41. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.d.ts +9 -1
  42. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.d.ts.map +1 -1
  43. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js +34 -9
  44. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js.map +1 -1
  45. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-job-tracker.d.ts.map +1 -1
  46. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-job-tracker.js +1 -2
  47. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-job-tracker.js.map +1 -1
  48. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.d.ts +2 -1
  49. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.d.ts.map +1 -1
  50. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.js +35 -8
  51. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.js.map +1 -1
  52. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.d.ts +1 -1
  53. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.d.ts.map +1 -1
  54. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.js +3 -3
  55. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.js.map +1 -1
  56. package/dist/builtin-extensions/pi-subagents/src/runs/background/result-watcher.d.ts +1 -0
  57. package/dist/builtin-extensions/pi-subagents/src/runs/background/result-watcher.d.ts.map +1 -1
  58. package/dist/builtin-extensions/pi-subagents/src/runs/background/result-watcher.js +54 -38
  59. package/dist/builtin-extensions/pi-subagents/src/runs/background/result-watcher.js.map +1 -1
  60. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.d.ts +3 -6
  61. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.d.ts.map +1 -1
  62. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.js +64 -5
  63. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.js.map +1 -1
  64. package/dist/builtin-extensions/pi-subagents/src/runs/background/wait-tool.d.ts.map +1 -1
  65. package/dist/builtin-extensions/pi-subagents/src/runs/background/wait-tool.js +1 -0
  66. package/dist/builtin-extensions/pi-subagents/src/runs/background/wait-tool.js.map +1 -1
  67. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.d.ts.map +1 -1
  68. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js +21 -8
  69. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js.map +1 -1
  70. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  71. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +62 -16
  72. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
  73. package/dist/builtin-extensions/pi-subagents/src/runs/shared/acceptance.d.ts +2 -0
  74. package/dist/builtin-extensions/pi-subagents/src/runs/shared/acceptance.d.ts.map +1 -1
  75. package/dist/builtin-extensions/pi-subagents/src/runs/shared/acceptance.js +75 -18
  76. package/dist/builtin-extensions/pi-subagents/src/runs/shared/acceptance.js.map +1 -1
  77. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts +13 -1
  78. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts.map +1 -1
  79. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js +67 -11
  80. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js.map +1 -1
  81. package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.d.ts.map +1 -1
  82. package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.js +5 -2
  83. package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.js.map +1 -1
  84. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.d.ts.map +1 -1
  85. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.js +13 -3
  86. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.js.map +1 -1
  87. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.d.ts +3 -0
  88. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.d.ts.map +1 -1
  89. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js +68 -11
  90. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js.map +1 -1
  91. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.d.ts +12 -1
  92. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.d.ts.map +1 -1
  93. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.js +21 -2
  94. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.js.map +1 -1
  95. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +16 -4
  96. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
  97. package/dist/builtin-extensions/pi-subagents/src/shared/types.js +1 -1
  98. package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
  99. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts +2 -0
  100. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts.map +1 -1
  101. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js +24 -6
  102. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js.map +1 -1
  103. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +44 -4
  104. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  105. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +227 -133
  106. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  107. package/dist/builtin-extensions/pi-subagents/src/watchdog/register-main.d.ts.map +1 -1
  108. package/dist/builtin-extensions/pi-subagents/src/watchdog/register-main.js +1 -1
  109. package/dist/builtin-extensions/pi-subagents/src/watchdog/register-main.js.map +1 -1
  110. package/dist/builtin-extensions/pi-subagents/src/watchdog/tool-actions.d.ts +2 -13
  111. package/dist/builtin-extensions/pi-subagents/src/watchdog/tool-actions.d.ts.map +1 -1
  112. package/dist/builtin-extensions/pi-subagents/src/watchdog/tool-actions.js +5 -91
  113. package/dist/builtin-extensions/pi-subagents/src/watchdog/tool-actions.js.map +1 -1
  114. package/dist/core/permissions.d.ts +1 -0
  115. package/dist/core/permissions.d.ts.map +1 -1
  116. package/dist/core/permissions.js +8 -4
  117. package/dist/core/permissions.js.map +1 -1
  118. package/dist/core/plan-mode.d.ts.map +1 -1
  119. package/dist/core/plan-mode.js +0 -5
  120. package/dist/core/plan-mode.js.map +1 -1
  121. package/dist/core/runtime-bridges.d.ts.map +1 -1
  122. package/dist/core/runtime-bridges.js +0 -2
  123. package/dist/core/runtime-bridges.js.map +1 -1
  124. package/dist/core/subagent-cancellation.d.ts +21 -0
  125. package/dist/core/subagent-cancellation.d.ts.map +1 -0
  126. package/dist/core/subagent-cancellation.js +53 -0
  127. package/dist/core/subagent-cancellation.js.map +1 -0
  128. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/assistant-message.js +12 -18
  130. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  131. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/tool-execution.js +12 -0
  133. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  134. package/dist/modes/interactive/interactive-mode.d.ts +1 -2
  135. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  136. package/dist/modes/interactive/interactive-mode.js +29 -22
  137. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  138. package/dist/node-runtime/{agent-session-services-35WNVGMR.js → agent-session-services-75JKASCW.js} +3 -3
  139. package/dist/node-runtime/{builtin-extensions-QATGXENO.js → builtin-extensions-X4JMM4ZS.js} +3 -3
  140. package/dist/node-runtime/{chunk-XATUM2HK.js → chunk-25I2BHAA.js} +1 -1
  141. package/dist/node-runtime/{chunk-MCUNIRD5.js → chunk-46ECNOLF.js} +477 -246
  142. package/dist/node-runtime/{chunk-J26TSGDL.js → chunk-6N22HULO.js} +8 -5
  143. package/dist/node-runtime/{chunk-NI6OTSBH.js → chunk-AL2FMKKM.js} +7 -7
  144. package/dist/node-runtime/chunk-AOFXZQKA.js +24 -0
  145. package/dist/node-runtime/chunk-BBUVIEZB.js +51 -0
  146. package/dist/node-runtime/{chunk-EBF6COVM.js → chunk-ELWEXVO5.js} +7 -7
  147. package/dist/node-runtime/{chunk-R5EDISSV.js → chunk-JHYKXIMM.js} +5 -2
  148. package/dist/node-runtime/{chunk-ATGA2WLC.js → chunk-JYNNB7DU.js} +397 -34
  149. package/dist/node-runtime/{chunk-ZBV7FAHG.js → chunk-K3646JTM.js} +17 -118
  150. package/dist/node-runtime/{chunk-PNJSG3IZ.js → chunk-LTYWTR3V.js} +1 -369
  151. package/dist/node-runtime/{chunk-G4P75AIS.js → chunk-QKFMBRAL.js} +47 -54
  152. package/dist/node-runtime/cli-runtime.js +1 -1
  153. package/dist/node-runtime/{command-AAV3YRR5.js → command-RBTRL55A.js} +1 -1
  154. package/dist/node-runtime/{gateway-OAC4OBBL.js → gateway-L4ANRTVE.js} +2 -2
  155. package/dist/node-runtime/index.js +10 -9
  156. package/dist/node-runtime/{interactive-mode-2SZUO53H.js → interactive-mode-HPUNH7SV.js} +5 -4
  157. package/dist/node-runtime/{interactive-view-S6J3QHEF.js → interactive-view-ILS6TAN5.js} +1 -1
  158. package/dist/node-runtime/{lunr-cron-AUUYP4N4.js → lunr-cron-NNYTWTGF.js} +2 -2
  159. package/dist/node-runtime/main.js +8 -8
  160. package/dist/node-runtime/{pi-intercom-EEMBGQID.js → pi-intercom-DDQMRNPJ.js} +96 -76
  161. package/dist/node-runtime/{pi-subagents-7YJ3I4VE.js → pi-subagents-T5WMMLBB.js} +1040 -318
  162. package/dist/node-runtime/rpc-entry.js +8 -8
  163. package/dist/node-runtime/{runtime-bridges-3RKBDLNA.js → runtime-bridges-NJQERQ72.js} +2 -4
  164. package/dist/node-runtime/{subagent-executor-5Q5A4HJ2.js → subagent-executor-SRFC26SJ.js} +168 -249
  165. package/docs/features.md +17 -3
  166. package/docs/keybindings.md +3 -1
  167. package/docs/settings.md +1 -1
  168. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  169. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  170. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  171. package/examples/extensions/gondolin/package-lock.json +2 -2
  172. package/examples/extensions/gondolin/package.json +1 -1
  173. package/examples/extensions/sandbox/package-lock.json +2 -2
  174. package/examples/extensions/sandbox/package.json +1 -1
  175. package/examples/extensions/with-deps/package-lock.json +2 -2
  176. package/examples/extensions/with-deps/package.json +1 -1
  177. package/package.json +4 -4
  178. package/dist/builtin-extensions/lunr-settings-tools.d.ts +0 -4
  179. package/dist/builtin-extensions/lunr-settings-tools.d.ts.map +0 -1
  180. package/dist/builtin-extensions/lunr-settings-tools.js +0 -120
  181. package/dist/builtin-extensions/lunr-settings-tools.js.map +0 -1
  182. package/dist/core/settings-tools-bridge.d.ts +0 -35
  183. package/dist/core/settings-tools-bridge.d.ts.map +0 -1
  184. package/dist/core/settings-tools-bridge.js +0 -56
  185. package/dist/core/settings-tools-bridge.js.map +0 -1
  186. package/dist/modes/interactive/injected-context.d.ts +0 -2
  187. package/dist/modes/interactive/injected-context.d.ts.map +0 -1
  188. package/dist/modes/interactive/injected-context.js +0 -7
  189. package/dist/modes/interactive/injected-context.js.map +0 -1
  190. package/dist/node-runtime/chunk-3NMWLTPV.js +0 -64
@@ -1,6 +1,8 @@
1
1
  import type { ExtensionAPI } from "@ashx-j/lunr";
2
2
  import { type SubagentState } from "../shared/types.ts";
3
+ import { resolveSupervisorChannelDir as resolveQuestionChannelDir } from "./supervisor-questions.ts";
3
4
  export declare const NATIVE_SUPERVISOR_TOOL_NAME = "subagent_supervisor";
5
+ export declare const resolveSupervisorChannelDir: typeof resolveQuestionChannelDir;
4
6
  type SupervisorReason = "need_decision" | "interview_request" | "progress_update";
5
7
  interface SupervisorRequest {
6
8
  type: "subagent.supervisor.request";
@@ -22,15 +24,19 @@ interface PendingSupervisorRequest extends SupervisorRequest {
22
24
  channelDir: string;
23
25
  requestFile: string;
24
26
  }
25
- export declare function resolveSupervisorChannelDir(runId: string, agent: string, childIndex: number): string;
26
27
  export declare function ensureSupervisorChannelDir(channelDir: string): void;
27
28
  export declare function registerNativeSupervisorClient(pi: ExtensionAPI, options?: {
28
29
  includeIntercomFallback?: boolean;
29
30
  }): void;
31
+ export declare function hasPendingBlockingSupervisorRequest(runId: string, sessionId?: string | null, now?: number): boolean;
30
32
  export declare function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState): {
31
33
  start: () => void;
32
34
  dispose: () => void;
33
35
  pending: Map<string, PendingSupervisorRequest>;
36
+ cancelOwnedQuestions: (reason: string) => void;
37
+ settleRunQuestions: (runId: string, reason: string) => void;
34
38
  };
39
+ export declare function isPendingQuestionForCurrentChild(questionId: string): boolean;
40
+ export declare function markDeliveredSupervisorQuestionFromChild(message: string): void;
35
41
  export {};
36
42
  //# sourceMappingURL=native-supervisor-channel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"native-supervisor-channel.d.ts","sourceRoot":"","sources":["../../../../../src/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,iCAAiC,CAAC;AAUtG,OAAO,EAAyF,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAM/I,eAAO,MAAM,2BAA2B,wBAAwB,CAAC;AAOjE,KAAK,gBAAgB,GAAG,eAAe,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAElF,UAAU,iBAAiB;IAC1B,IAAI,EAAE,6BAA6B,CAAC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,wBAAyB,SAAQ,iBAAiB;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAuCD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpG;AAED,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAGnE;AAkMD,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CAgC1H;AA+QD,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,GAAG;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAA;CAAE,CAmFhL","sourcesContent":["// @ts-nocheck\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { AgentToolResult } from \"@ashx-j/lunr-agent\";\nimport type { ExtensionAPI, ExtensionContext, ToolDefinition } from \"@ashx-j/lunr\";\nimport { Type } from \"typebox\";\nimport {\n\tSUBAGENT_CHILD_AGENT_ENV,\n\tSUBAGENT_CHILD_INDEX_ENV,\n\tSUBAGENT_ORCHESTRATOR_SESSION_ID_ENV,\n\tSUBAGENT_ORCHESTRATOR_TARGET_ENV,\n\tSUBAGENT_RUN_ID_ENV,\n\tSUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,\n} from \"../runs/shared/pi-args.ts\";\nimport { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type IntercomEventBus, type SubagentState } from \"../shared/types.ts\";\nimport { writeAtomicJson } from \"../shared/atomic-json.ts\";\n\nconst SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, \"supervisor-channels\");\nconst REQUESTS_DIR = \"requests\";\nconst REPLIES_DIR = \"replies\";\nexport const NATIVE_SUPERVISOR_TOOL_NAME = \"subagent_supervisor\";\nconst MAX_MESSAGE_BYTES = 64 * 1024;\nconst DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;\nconst CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);\nconst STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;\nconst STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;\n\ntype SupervisorReason = \"need_decision\" | \"interview_request\" | \"progress_update\";\n\ninterface SupervisorRequest {\n\ttype: \"subagent.supervisor.request\";\n\tid: string;\n\tcreatedAt: number;\n\texpiresAt?: number;\n\treason: SupervisorReason;\n\tmessage: string;\n\texpectsReply: boolean;\n\torchestratorTarget?: string;\n\torchestratorSessionId?: string;\n\trunId: string;\n\tagent: string;\n\tchildIndex: number;\n\tchildTarget?: string;\n\tinterview?: unknown;\n}\n\ninterface PendingSupervisorRequest extends SupervisorRequest {\n\tchannelDir: string;\n\trequestFile: string;\n}\n\ninterface SupervisorReply {\n\ttype: \"subagent.supervisor.reply\";\n\trequestId: string;\n\tcreatedAt: number;\n\tmessage: string;\n}\n\ninterface ContactSupervisorParams {\n\treason: SupervisorReason;\n\tmessage?: string;\n\tinterview?: unknown;\n}\n\ninterface IntercomParams {\n\taction: \"list\" | \"send\" | \"ask\" | \"reply\" | \"pending\" | \"status\";\n\tto?: string;\n\tmessage?: string;\n\treplyTo?: string;\n}\n\nconst ContactSupervisorParamsSchema = Type.Object({\n\treason: Type.String({ enum: [\"need_decision\", \"interview_request\", \"progress_update\"] }),\n\tmessage: Type.Optional(Type.String()),\n\tinterview: Type.Optional(Type.Unsafe({ type: \"object\", additionalProperties: true })),\n}, { additionalProperties: false });\n\nconst IntercomParamsSchema = Type.Object({\n\taction: Type.String({ enum: [\"list\", \"send\", \"ask\", \"reply\", \"pending\", \"status\"] }),\n\tto: Type.Optional(Type.String()),\n\tmessage: Type.Optional(Type.String()),\n\treplyTo: Type.Optional(Type.String()),\n}, { additionalProperties: false });\n\nfunction safeSegment(value: string): string {\n\treturn value.trim().replace(/[^A-Za-z0-9._-]+/g, \"-\").replace(/^-+|-+$/g, \"\") || \"unknown\";\n}\n\nexport function resolveSupervisorChannelDir(runId: string, agent: string, childIndex: number): string {\n\treturn path.join(SUPERVISOR_CHANNEL_ROOT, `${safeSegment(runId)}-${safeSegment(agent)}-${childIndex}`);\n}\n\nexport function ensureSupervisorChannelDir(channelDir: string): void {\n\tfs.mkdirSync(path.join(channelDir, REQUESTS_DIR), { recursive: true, mode: 0o700 });\n\tfs.mkdirSync(path.join(channelDir, REPLIES_DIR), { recursive: true, mode: 0o700 });\n}\n\nfunction requestPath(channelDir: string, requestId: string): string {\n\treturn path.join(channelDir, REQUESTS_DIR, `${safeSegment(requestId)}.json`);\n}\n\nfunction replyPath(channelDir: string, requestId: string): string {\n\treturn path.join(channelDir, REPLIES_DIR, `${safeSegment(requestId)}.json`);\n}\n\nfunction readTextEnv(name: string): string | undefined {\n\tconst value = process.env[name]?.trim();\n\treturn value ? value : undefined;\n}\n\nfunction readChildMetadata(): {\n\tchannelDir: string;\n\trunId: string;\n\tagent: string;\n\tchildIndex: number;\n\torchestratorTarget?: string;\n\torchestratorSessionId?: string;\n\tchildTarget?: string;\n} | undefined {\n\tconst channelDir = readTextEnv(SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV);\n\tconst runId = readTextEnv(SUBAGENT_RUN_ID_ENV);\n\tconst agent = readTextEnv(SUBAGENT_CHILD_AGENT_ENV);\n\tconst rawIndex = readTextEnv(SUBAGENT_CHILD_INDEX_ENV);\n\tconst orchestratorSessionId = readTextEnv(SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV);\n\tif (!channelDir || !runId || !agent || !orchestratorSessionId || rawIndex === undefined || !/^\\d+$/.test(rawIndex)) return undefined;\n\treturn {\n\t\tchannelDir,\n\t\trunId,\n\t\tagent,\n\t\tchildIndex: Number(rawIndex),\n\t\torchestratorTarget: readTextEnv(SUBAGENT_ORCHESTRATOR_TARGET_ENV),\n\t\torchestratorSessionId,\n\t\tchildTarget: readTextEnv(\"PI_SUBAGENT_INTERCOM_SESSION_NAME\"),\n\t};\n}\n\nfunction reasonHeading(reason: SupervisorReason): string {\n\tif (reason === \"interview_request\") return \"Subagent requests a structured supervisor interview.\";\n\tif (reason === \"progress_update\") return \"Subagent progress update.\";\n\treturn \"Subagent needs a supervisor decision.\";\n}\n\nfunction formatChildMessage(input: {\n\treason: SupervisorReason;\n\tmessage?: string;\n\tinterview?: unknown;\n\trunId: string;\n\tagent: string;\n\tchildIndex: number;\n\tchildTarget?: string;\n}): string {\n\tconst lines = [\n\t\treasonHeading(input.reason),\n\t\t`Run: ${input.runId}`,\n\t\t`Agent: ${input.agent}`,\n\t\t`Child index: ${input.childIndex}`,\n\t];\n\tif (input.childTarget) lines.push(`Child intercom target: ${input.childTarget}`);\n\tlines.push(\"\");\n\tif (input.message?.trim()) lines.push(input.message.trim());\n\tif (input.reason === \"interview_request\") {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t\"Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.\",\n\t\t);\n\t\tif (input.interview !== undefined) lines.push(JSON.stringify(input.interview, null, \"\\t\"));\n\t}\n\treturn lines.join(\"\\n\").trimEnd();\n}\n\nfunction parseStructuredReply(message: string): { value?: unknown; error?: string } {\n\tconst trimmed = message.trim();\n\tconst fenced = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i)?.[1]?.trim();\n\ttry {\n\t\treturn { value: JSON.parse(fenced ?? trimmed) };\n\t} catch (error) {\n\t\treturn { error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) };\n\t}\n}\n\nfunction askTimeoutMs(): number {\n\tconst parsed = Number(process.env.PI_INTERCOM_ASK_TIMEOUT_MS);\n\treturn Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ASK_TIMEOUT_MS;\n}\n\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Supervisor request cancelled.\"));\n\t\t\treturn;\n\t\t}\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\tconst cleanup = () => {\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tcleanup();\n\t\t\treject(new Error(\"Supervisor request cancelled.\"));\n\t\t};\n\t\ttimer = setTimeout(() => {\n\t\t\tcleanup();\n\t\t\tresolve();\n\t\t}, ms);\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nasync function waitForReply(channelDir: string, requestId: string, deadline: number, signal?: AbortSignal): Promise<SupervisorReply> {\n\tconst file = replyPath(channelDir, requestId);\n\twhile (Date.now() <= deadline) {\n\t\tif (signal?.aborted) throw new Error(\"Supervisor request cancelled.\");\n\t\tif (fs.existsSync(file)) {\n\t\t\tconst parsed = JSON.parse(fs.readFileSync(file, \"utf-8\")) as Partial<SupervisorReply>;\n\t\t\tif (parsed.type === \"subagent.supervisor.reply\" && parsed.requestId === requestId && typeof parsed.message === \"string\") {\n\t\t\t\treturn parsed as SupervisorReply;\n\t\t\t}\n\t\t}\n\t\tawait delay(250, signal);\n\t}\n\tthrow new Error(\"Timed out waiting for supervisor reply.\");\n}\n\nasync function sendSupervisorRequest(params: ContactSupervisorParams, signal?: AbortSignal): Promise<AgentToolResult<Record<string, unknown>>> {\n\tconst metadata = readChildMetadata();\n\tif (!metadata) throw new Error(\"Native supervisor channel is not available for this subagent.\");\n\tif (params.reason !== \"progress_update\" && !params.message?.trim() && params.reason !== \"interview_request\") {\n\t\tthrow new Error(\"message is required for supervisor decisions.\");\n\t}\n\tensureSupervisorChannelDir(metadata.channelDir);\n\tconst requestId = randomUUID();\n\tconst expectsReply = params.reason !== \"progress_update\";\n\tconst createdAt = Date.now();\n\tconst replyDeadline = createdAt + askTimeoutMs();\n\tconst expiresAt = expectsReply ? replyDeadline : undefined;\n\tconst message = formatChildMessage({ ...metadata, reason: params.reason, message: params.message, interview: params.interview });\n\tconst request: SupervisorRequest = {\n\t\ttype: \"subagent.supervisor.request\",\n\t\tid: requestId,\n\t\tcreatedAt,\n\t\t...(expiresAt !== undefined ? { expiresAt } : {}),\n\t\treason: params.reason,\n\t\tmessage,\n\t\texpectsReply,\n\t\t...(metadata.orchestratorTarget ? { orchestratorTarget: metadata.orchestratorTarget } : {}),\n\t\t...(metadata.orchestratorSessionId ? { orchestratorSessionId: metadata.orchestratorSessionId } : {}),\n\t\trunId: metadata.runId,\n\t\tagent: metadata.agent,\n\t\tchildIndex: metadata.childIndex,\n\t\t...(metadata.childTarget ? { childTarget: metadata.childTarget } : {}),\n\t\t...(params.interview !== undefined ? { interview: params.interview } : {}),\n\t};\n\tconst serialized = JSON.stringify(request, null, \"\\t\");\n\tif (Buffer.byteLength(serialized, \"utf-8\") > MAX_MESSAGE_BYTES) throw new Error(\"Supervisor request is too large.\");\n\twriteAtomicJson(requestPath(metadata.channelDir, requestId), request);\n\n\tif (!expectsReply) {\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: \"Supervisor progress update queued.\" }],\n\t\t\tdetails: { delivered: true, requestId, reason: params.reason },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst reply = await waitForReply(metadata.channelDir, requestId, replyDeadline, signal);\n\t\tconst details: Record<string, unknown> = { requestId, reason: params.reason };\n\t\tif (params.reason === \"interview_request\") {\n\t\t\tconst structured = parseStructuredReply(reply.message);\n\t\t\tif (structured.error) details.structuredReplyParseError = structured.error;\n\t\t\telse details.structuredReply = structured.value;\n\t\t}\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `**Reply from supervisor:**\\n${reply.message}` }],\n\t\t\tdetails,\n\t\t};\n\t} catch (error) {\n\t\tremoveRequestFile(requestPath(metadata.channelDir, requestId));\n\t\tthrow error;\n\t}\n}\n\nfunction hasTool(pi: ExtensionAPI, name: string): boolean {\n\ttry {\n\t\treturn pi.getAllTools?.().some((tool: { name?: unknown }) => tool.name === name) === true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function registerNativeSupervisorClient(pi: ExtensionAPI, options: { includeIntercomFallback?: boolean } = {}): void {\n\tif (!readChildMetadata()) return;\n\tconst includeIntercomFallback = options.includeIntercomFallback !== false;\n\tif (!hasTool(pi, \"contact_supervisor\")) {\n\t\tconst tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {\n\t\t\tname: \"contact_supervisor\",\n\t\t\tlabel: \"Contact Supervisor\",\n\t\t\tdescription: \"Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.\",\n\t\t\tparameters: ContactSupervisorParamsSchema,\n\t\t\texecute(_id, params, signal) {\n\t\t\t\treturn sendSupervisorRequest(params as ContactSupervisorParams, signal);\n\t\t\t},\n\t\t};\n\t\tpi.registerTool(tool);\n\t}\n\tif (includeIntercomFallback && !hasTool(pi, \"intercom\")) {\n\t\tconst tool: ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> = {\n\t\t\tname: \"intercom\",\n\t\t\tlabel: \"Intercom\",\n\t\t\tdescription: \"Native supervisor-channel intercom fallback for subagents. Prefer contact_supervisor when available.\",\n\t\t\tparameters: IntercomParamsSchema,\n\t\t\tasync execute(_id, params, signal) {\n\t\t\t\tconst action = (params as IntercomParams).action;\n\t\t\t\tif (action === \"status\") return { content: [{ type: \"text\", text: \"Native supervisor channel is active.\" }], details: { active: true } };\n\t\t\t\tif (action === \"list\") return { content: [{ type: \"text\", text: \"Supervisor session available through contact_supervisor.\" }], details: { sessions: [] } };\n\t\t\t\tif (action === \"send\") return sendSupervisorRequest({ reason: \"progress_update\", message: (params as IntercomParams).message ?? \"\" }, signal);\n\t\t\t\tif (action === \"ask\") return sendSupervisorRequest({ reason: \"need_decision\", message: (params as IntercomParams).message ?? \"\" }, signal);\n\t\t\t\tthrow new Error(\"Native child intercom supports status, list, send, and ask. Use parent intercom reply from the supervisor session.\");\n\t\t\t},\n\t\t};\n\t\tpi.registerTool(tool);\n\t}\n}\n\nfunction parseRequestFile(file: string, channelDir: string): PendingSupervisorRequest | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(fs.readFileSync(file, \"utf-8\")) as Partial<SupervisorRequest>;\n\t\tif (parsed.type !== \"subagent.supervisor.request\") return undefined;\n\t\tif (typeof parsed.id !== \"string\" || !parsed.id) return undefined;\n\t\tif (parsed.reason !== \"need_decision\" && parsed.reason !== \"interview_request\" && parsed.reason !== \"progress_update\") return undefined;\n\t\tif (typeof parsed.message !== \"string\" || !parsed.message) return undefined;\n\t\tif (typeof parsed.runId !== \"string\" || typeof parsed.agent !== \"string\" || typeof parsed.childIndex !== \"number\") return undefined;\n\t\treturn { ...parsed as SupervisorRequest, channelDir, requestFile: file };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction listRequestFiles(): Array<{ channelDir: string; file: string }> {\n\tlet channelEntries: fs.Dirent[];\n\ttry {\n\t\tchannelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\tthrow error;\n\t}\n\tconst files: Array<{ channelDir: string; file: string }> = [];\n\tfor (const entry of channelEntries) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name);\n\t\tconst requestsDir = path.join(channelDir, REQUESTS_DIR);\n\t\tlet requestEntries: fs.Dirent[];\n\t\ttry {\n\t\t\trequestEntries = fs.readdirSync(requestsDir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const requestEntry of requestEntries) {\n\t\t\tif (requestEntry.isFile() && requestEntry.name.endsWith(\".json\")) files.push({ channelDir, file: path.join(requestsDir, requestEntry.name) });\n\t\t}\n\t}\n\treturn files;\n}\n\nfunction readDirectoryEntries(dir: string): fs.Dirent[] | undefined {\n\ttry {\n\t\treturn fs.readdirSync(dir, { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\treturn undefined;\n\t}\n}\n\nfunction directoryMtimeMs(dir: string): number {\n\ttry {\n\t\treturn fs.statSync(dir).mtimeMs;\n\t} catch {\n\t\treturn 0;\n\t}\n}\n\nfunction removeEmptyDirectory(dir: string): boolean {\n\ttry {\n\t\tfs.rmdirSync(dir);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\tif (code === \"ENOENT\") return true;\n\t\tif (code === \"ENOTEMPTY\" || code === \"EEXIST\" || code === \"EPERM\" || code === \"EBUSY\") return false;\n\t\tthrow error;\n\t}\n}\n\nfunction removeStaleEmptySupervisorChannel(channelDir: string, nowMs: number): boolean {\n\tconst requestsDir = path.join(channelDir, REQUESTS_DIR);\n\tconst repliesDir = path.join(channelDir, REPLIES_DIR);\n\tconst newestKnownMtimeMs = Math.max(\n\t\tdirectoryMtimeMs(channelDir),\n\t\tdirectoryMtimeMs(requestsDir),\n\t\tdirectoryMtimeMs(repliesDir),\n\t);\n\tif (nowMs - newestKnownMtimeMs < STALE_EMPTY_CHANNEL_AGE_MS) return false;\n\n\tconst requestEntries = readDirectoryEntries(requestsDir);\n\tif (!requestEntries || requestEntries.length > 0) return false;\n\tconst replyEntries = readDirectoryEntries(repliesDir);\n\tif (!replyEntries || replyEntries.length > 0) return false;\n\n\tif (!removeEmptyDirectory(requestsDir)) return false;\n\tif (!removeEmptyDirectory(repliesDir)) return false;\n\tif (!removeEmptyDirectory(channelDir)) return false;\n\treturn true;\n}\n\nfunction cleanupStaleEmptySupervisorChannels(nowMs = Date.now()): number {\n\tlet channelEntries: fs.Dirent[];\n\ttry {\n\t\tchannelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return 0;\n\t\tthrow error;\n\t}\n\n\tlet removed = 0;\n\tfor (const entry of channelEntries) {\n\t\tif (!entry.isDirectory()) continue;\n\t\ttry {\n\t\t\tif (removeStaleEmptySupervisorChannel(path.join(SUPERVISOR_CHANNEL_ROOT, entry.name), nowMs)) removed++;\n\t\t} catch {\n\t\t\t// Cleanup is opportunistic; active writers can race with us and will be picked up by a later pass.\n\t\t}\n\t}\n\treturn removed;\n}\n\nfunction currentContextSessionId(state: Pick<SubagentState, \"currentSessionId\">, ctx: ExtensionContext): string | undefined {\n\ttry {\n\t\tconst sessionId = ctx.sessionManager.getSessionId();\n\t\tif (sessionId) return sessionId;\n\t} catch {\n\t\t// Fall through to the last known identity.\n\t}\n\treturn state.currentSessionId ?? undefined;\n}\n\nfunction requestMatchesContext(request: SupervisorRequest, state: Pick<SubagentState, \"currentSessionId\">, ctx: ExtensionContext): boolean {\n\tconst currentSessionId = currentContextSessionId(state, ctx);\n\treturn Boolean(currentSessionId && request.orchestratorSessionId === currentSessionId);\n}\n\nfunction removeRequestFile(file: string): void {\n\ttry {\n\t\tfs.rmSync(file, { force: true });\n\t} catch {\n\t\t// Request cleanup is best-effort; reply files and timeout errors remain authoritative.\n\t}\n}\n\ntype SupervisorRequestLifecycle = \"pending\" | \"resolved\" | \"expired\" | \"inactive\" | \"missing\" | \"wrong-session\";\n\nfunction requestExpiresAt(request: SupervisorRequest, now: number): number {\n\tconst expiresAt = (request as { expiresAt?: unknown }).expiresAt;\n\tif (typeof expiresAt === \"number\" && Number.isFinite(expiresAt)) return expiresAt;\n\treturn Number.isFinite(request.createdAt) ? request.createdAt + askTimeoutMs() : now;\n}\n\nfunction requestRunInactive(request: SupervisorRequest, state: SubagentState): boolean {\n\tif (state.foregroundControls.has(request.runId)) return false;\n\tconst foregroundRun = state.foregroundRuns?.get(request.runId);\n\tconst foregroundChild = foregroundRun?.children.find((child) => child.index === request.childIndex && child.agent === request.agent)\n\t\t?? foregroundRun?.children[request.childIndex];\n\tif (foregroundChild) return foregroundChild.status !== \"detached\";\n\n\tconst asyncJob = state.asyncJobs.get(request.runId);\n\tif (!asyncJob) return false;\n\tif (asyncJob.status === \"complete\" || asyncJob.status === \"failed\" || asyncJob.status === \"paused\") return true;\n\tconst stepStatus = asyncJob.steps?.[request.childIndex]?.status;\n\treturn stepStatus === \"complete\" || stepStatus === \"completed\" || stepStatus === \"failed\" || stepStatus === \"paused\";\n}\n\nfunction requestLifecycle(request: PendingSupervisorRequest, state: SubagentState, ctx: ExtensionContext | undefined, now: number): SupervisorRequestLifecycle {\n\tif (ctx && !requestMatchesContext(request, state, ctx)) return \"wrong-session\";\n\tif (!fs.existsSync(request.requestFile)) return \"missing\";\n\tif (request.expectsReply && fs.existsSync(replyPath(request.channelDir, request.id))) return \"resolved\";\n\tif (request.expectsReply && now > requestExpiresAt(request, now)) return \"expired\";\n\tif (request.expectsReply && requestRunInactive(request, state)) return \"inactive\";\n\treturn \"pending\";\n}\n\nfunction cleanupRequestLifecycle(request: PendingSupervisorRequest, lifecycle: SupervisorRequestLifecycle): void {\n\tif (lifecycle === \"resolved\" || lifecycle === \"expired\" || lifecycle === \"inactive\") removeRequestFile(request.requestFile);\n}\n\nfunction refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined): void {\n\tconst now = Date.now();\n\tfor (const request of pending.values()) {\n\t\tconst lifecycle = requestLifecycle(request, state, ctx, now);\n\t\tif (lifecycle === \"pending\") continue;\n\t\tpending.delete(request.id);\n\t\tcleanupRequestLifecycle(request, lifecycle);\n\t}\n}\n\nfunction formatPendingLine(request: PendingSupervisorRequest): string {\n\tconst replyHint = request.expectsReply ? ` Reply: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: \"reply\", replyTo: \"${request.id}\", message: \"...\" })` : \"\";\n\treturn `- ${request.id}: ${request.agent} [${request.runId}#${request.childIndex}] ${request.reason}.${replyHint}`;\n}\n\nfunction requestVisibleText(request: PendingSupervisorRequest): string {\n\tconst lines = [request.message];\n\tif (request.expectsReply) {\n\t\tlines.push(\"\", `Reply with: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: \"reply\", replyTo: \"${request.id}\", message: \"...\" })`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nfunction writeReply(request: PendingSupervisorRequest, message: string): void {\n\tif (!message.trim()) throw new Error(\"message is required for supervisor replies.\");\n\tconst reply: SupervisorReply = {\n\t\ttype: \"subagent.supervisor.reply\",\n\t\trequestId: request.id,\n\t\tcreatedAt: Date.now(),\n\t\tmessage: message.trim(),\n\t};\n\twriteAtomicJson(replyPath(request.channelDir, request.id), reply);\n\tremoveRequestFile(request.requestFile);\n}\n\nfunction resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, params: IntercomParams): PendingSupervisorRequest {\n\tif (params.replyTo) {\n\t\tconst request = pending.get(params.replyTo);\n\t\tif (!request) throw new Error(`No pending supervisor request found for replyTo '${params.replyTo}'.`);\n\t\treturn request;\n\t}\n\tconst requests = [...pending.values()].filter((request) => request.expectsReply);\n\tif (params.to) {\n\t\tconst normalizedTo = params.to.toLowerCase();\n\t\tconst matches = requests.filter((request) =>\n\t\t\trequest.id.toLowerCase().startsWith(normalizedTo)\n\t\t\t|| request.agent.toLowerCase() === normalizedTo\n\t\t\t|| request.childTarget?.toLowerCase() === normalizedTo,\n\t\t);\n\t\tif (matches.length === 1) return matches[0]!;\n\t\tif (matches.length > 1) throw new Error(`Multiple pending supervisor requests match '${params.to}'. Use replyTo.`);\n\t}\n\tif (requests.length === 1) return requests[0]!;\n\tif (requests.length === 0) throw new Error(\"No pending supervisor requests need a reply.\");\n\tthrow new Error(\"Multiple pending supervisor requests need replies. Use replyTo.\");\n}\n\nfunction publicPendingRequests(pending: Map<string, PendingSupervisorRequest>): Array<Record<string, unknown>> {\n\treturn [...pending.values()].map((request) => ({\n\t\tid: request.id,\n\t\trunId: request.runId,\n\t\tagent: request.agent,\n\t\tchildIndex: request.childIndex,\n\t\treason: request.reason,\n\t\texpectsReply: request.expectsReply,\n\t}));\n}\n\nfunction buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, name = \"intercom\"): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {\n\treturn {\n\t\tname,\n\t\tlabel: name === \"intercom\" ? \"Intercom\" : \"Subagent Supervisor\",\n\t\tdescription: name === \"intercom\"\n\t\t\t? \"Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests.\"\n\t\t\t: \"Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.\",\n\t\tparameters: IntercomParamsSchema,\n\t\tasync execute(_id, params) {\n\t\t\trefreshPendingRequests(pending, state, state.lastUiContext ?? undefined);\n\t\t\tconst input = params as IntercomParams;\n\t\t\tif (input.action === \"status\") {\n\t\t\t\treturn { content: [{ type: \"text\", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };\n\t\t\t}\n\t\t\tif (input.action === \"pending\" || input.action === \"list\") {\n\t\t\t\tconst lines = [...pending.values()].filter((request) => request.expectsReply).map(formatPendingLine);\n\t\t\t\treturn { content: [{ type: \"text\", text: lines.length ? lines.join(\"\\n\") : \"No pending supervisor requests.\" }], details: { pending: publicPendingRequests(pending) } };\n\t\t\t}\n\t\t\tif (input.action === \"reply\") {\n\t\t\t\tconst request = resolvePendingRequest(pending, input);\n\t\t\t\twriteReply(request, input.message ?? \"\");\n\t\t\t\tpending.delete(request.id);\n\t\t\t\treturn { content: [{ type: \"text\", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };\n\t\t\t}\n\t\t\tif (input.action === \"send\" || input.action === \"ask\") {\n\t\t\t\tthrow new Error(\"Native pi-subagents intercom currently handles supervisor replies. Child agents initiate asks with contact_supervisor.\");\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported intercom action: ${input.action}`);\n\t\t},\n\t};\n}\n\nexport function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {\n\tconst pending = new Map<string, PendingSupervisorRequest>();\n\tconst seenFiles = new Set<string>();\n\tlet poller: ReturnType<typeof setInterval> | undefined;\n\tlet lastStaleCleanupAt = 0;\n\n\tconst registerParentTools = (): void => {\n\t\tif (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentIntercomTool(pending, state, NATIVE_SUPERVISOR_TOOL_NAME));\n\t\tif (!hasTool(pi, \"intercom\")) pi.registerTool(buildParentIntercomTool(pending, state));\n\t};\n\n\tconst cleanupStaleChannelsIfDue = (): void => {\n\t\tconst nowMs = Date.now();\n\t\tif (nowMs - lastStaleCleanupAt < STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS) return;\n\t\tlastStaleCleanupAt = nowMs;\n\t\ttry {\n\t\t\tcleanupStaleEmptySupervisorChannels(nowMs);\n\t\t} catch {\n\t\t\t// Supervisor delivery must not fail because best-effort temp cleanup failed.\n\t\t}\n\t};\n\n\tconst poll = (): void => {\n\t\tcleanupStaleChannelsIfDue();\n\t\tconst ctx = state.lastUiContext;\n\t\tif (!ctx) return;\n\t\trefreshPendingRequests(pending, state, ctx);\n\t\tconst now = Date.now();\n\t\tfor (const { channelDir, file } of listRequestFiles()) {\n\t\t\tif (seenFiles.has(file)) continue;\n\t\t\tconst request = parseRequestFile(file, channelDir);\n\t\t\tif (!request || !requestMatchesContext(request, state, ctx)) continue;\n\t\t\tconst lifecycle = requestLifecycle(request, state, undefined, now);\n\t\t\tif (lifecycle !== \"pending\") {\n\t\t\t\tseenFiles.add(file);\n\t\t\t\tcleanupRequestLifecycle(request, lifecycle);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tseenFiles.add(file);\n\t\t\tif (request.expectsReply) pending.set(request.id, request);\n\t\t\telse {\n\t\t\t\tremoveRequestFile(request.requestFile);\n\t\t\t}\n\t\t\tpi.sendMessage({\n\t\t\t\tcustomType: \"subagent_supervisor_request\",\n\t\t\t\tcontent: requestVisibleText(request),\n\t\t\t\tdisplay: true,\n\t\t\t\tdetails: {\n\t\t\t\t\tid: request.id,\n\t\t\t\t\treason: request.reason,\n\t\t\t\t\texpectsReply: request.expectsReply,\n\t\t\t\t\trunId: request.runId,\n\t\t\t\t\tagent: request.agent,\n\t\t\t\t\tchildIndex: request.childIndex,\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (request.expectsReply) {\n\t\t\t\t(pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {\n\t\t\t\t\trequestId: request.id,\n\t\t\t\t\trunId: request.runId,\n\t\t\t\t\tagent: request.agent,\n\t\t\t\t\tchildIndex: request.childIndex,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tstart: () => {\n\t\t\tif (poller) return;\n\t\t\tregisterParentTools();\n\t\t\tpoll();\n\t\t\tpoller = setInterval(poll, CHANNEL_POLL_MS);\n\t\t\tpoller.unref?.();\n\t\t},\n\t\tdispose: () => {\n\t\t\tif (poller) clearInterval(poller);\n\t\t\tpoller = undefined;\n\t\t\tpending.clear();\n\t\t\tseenFiles.clear();\n\t\t},\n\t\tpending,\n\t};\n}\n"]}
1
+ {"version":3,"file":"native-supervisor-channel.d.ts","sourceRoot":"","sources":["../../../../../src/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,iCAAiC,CAAC;AAUtG,OAAO,EAAkG,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxJ,OAAO,EAcN,2BAA2B,IAAI,yBAAyB,EAQxD,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,2BAA2B,wBAAwB,CAAC;AACjE,eAAO,MAAM,2BAA2B,kCAA4B,CAAC;AAOrE,KAAK,gBAAgB,GAAG,eAAe,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAElF,UAAU,iBAAiB;IAC1B,IAAI,EAAE,6BAA6B,CAAC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,wBAAyB,SAAQ,iBAAiB;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAwED,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAInE;AA8OD,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CAgC1H;AAgBD,wBAAgB,mCAAmC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,SAAa,GAAG,OAAO,CAYvH;AA6TD,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,GAAG;IACtF,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5D,CAmHA;AAED,wBAAgB,gCAAgC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAO5E;AAED,wBAAgB,wCAAwC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAW9E","sourcesContent":["// @ts-nocheck\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { AgentToolResult } from \"@ashx-j/lunr-agent\";\nimport type { ExtensionAPI, ExtensionContext, ToolDefinition } from \"@ashx-j/lunr\";\nimport { Type } from \"typebox\";\nimport {\n\tSUBAGENT_CHILD_AGENT_ENV,\n\tSUBAGENT_CHILD_INDEX_ENV,\n\tSUBAGENT_ORCHESTRATOR_SESSION_ID_ENV,\n\tSUBAGENT_ORCHESTRATOR_TARGET_ENV,\n\tSUBAGENT_RUN_ID_ENV,\n\tSUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,\n} from \"../runs/shared/pi-args.ts\";\nimport { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, SUBAGENT_CONTROL_EVENT, type IntercomEventBus, type SubagentState } from \"../shared/types.ts\";\nimport { writeAtomicJson } from \"../shared/atomic-json.ts\";\nimport { askRunningAsyncChild, reconcileAsyncQuestion } from \"./supervisor-ask.ts\";\nimport {\n\tanswerSupervisorQuestion,\n\tcancelPendingSupervisorQuestionsForOwner,\n\tcancelSupervisorQuestion,\n\tclaimQuestionNotification,\n\tformatSupervisorQuestionAnswer,\n\thasOutstandingQuestionForChild,\n\tisQuestionWaitActive,\n\tlistSupervisorChannelDirs,\n\tlistSupervisorQuestions,\n\tmarkSupervisorQuestionDelivered,\n\toutstandingQuestionsForChild,\n\tpendingSupervisorQuestionsForDelivery,\n\tQUESTIONS_DIR,\n\tresolveSupervisorChannelDir as resolveQuestionChannelDir,\n\tSUBAGENT_QUESTION_EVENT,\n\tSUBAGENT_SUPERVISOR_ANSWER_TYPE,\n\tSUPERVISOR_CHANNEL_ROOT,\n\tsupervisorQuestionIsTerminal,\n\tsupervisorQuestionPublicState,\n\ttype SupervisorQuestion,\n\ttype SupervisorQuestionOwner,\n} from \"./supervisor-questions.ts\";\n\nconst REQUESTS_DIR = \"requests\";\nconst REPLIES_DIR = \"replies\";\nexport const NATIVE_SUPERVISOR_TOOL_NAME = \"subagent_supervisor\";\nexport const resolveSupervisorChannelDir = resolveQuestionChannelDir;\nconst MAX_MESSAGE_BYTES = 64 * 1024;\nconst DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;\nconst CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);\nconst STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;\nconst STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;\n\ntype SupervisorReason = \"need_decision\" | \"interview_request\" | \"progress_update\";\n\ninterface SupervisorRequest {\n\ttype: \"subagent.supervisor.request\";\n\tid: string;\n\tcreatedAt: number;\n\texpiresAt?: number;\n\treason: SupervisorReason;\n\tmessage: string;\n\texpectsReply: boolean;\n\torchestratorTarget?: string;\n\torchestratorSessionId?: string;\n\trunId: string;\n\tagent: string;\n\tchildIndex: number;\n\tchildTarget?: string;\n\tinterview?: unknown;\n}\n\ninterface PendingSupervisorRequest extends SupervisorRequest {\n\tchannelDir: string;\n\trequestFile: string;\n}\n\ninterface SupervisorReply {\n\ttype: \"subagent.supervisor.reply\";\n\trequestId: string;\n\tcreatedAt: number;\n\tmessage: string;\n}\n\ninterface ContactSupervisorParams {\n\treason?: SupervisorReason;\n\tmessage?: string;\n\tinterview?: unknown;\n\taction?: \"reply\";\n\treplyTo?: string;\n}\n\ninterface IntercomParams {\n\taction: \"list\" | \"send\" | \"ask\" | \"reply\" | \"pending\" | \"status\";\n\tto?: string;\n\tmessage?: string;\n\treplyTo?: string;\n\tid?: string;\n\tindex?: number;\n\tchildId?: string;\n\treason?: string;\n}\n\nconst PARENT_ASK_DESCRIPTION = [\n\t\"Ask a specific running async child, or reply to a child supervisor request.\",\n\t\"Ask only when the child owns missing context, the answer changes a concrete next decision, and waiting risks a block or rework.\",\n\t\"Use available results first, batch related questions, and keep working while awaiting the answer. Do not use ask for status checks, polling, duplicate queries, or step-by-step supervision. Follow up only when the answer leaves the original decision unresolved.\",\n\t\"Returns a question id immediately. The child's explicit answer is delivered separately from the final run result.\",\n\t'Ask: { action: \"ask\", id: \"<runId>\", index?: 0, childId?: \"...\", reason: \"<concrete decision>\", message: \"...\" }',\n\t\"Use pending or list to find child requests awaiting your reply; status reports channel availability. Free-form send is unsupported. Asking never revives a child or changes its assignment.\",\n\t'Reply: { action: \"reply\", replyTo: \"<requestId>\", message: \"...\" }',\n].join(\" \");\n\nconst CHILD_CONTACT_DESCRIPTION = [\n\t\"Contact the parent/supervisor session for a blocking decision, structured interview, progress update, or reply to a parent question.\",\n\t\"For parent questions use action='reply' with replyTo set to the question id. Answer from current findings and uncertainty; do not start extra investigation just to reply. Continue the assigned task after replying.\",\n\t\"Do not open a blocking need_decision while a parent question is already outstanding for this child.\",\n].join(\" \");\n\nconst ContactSupervisorParamsSchema = Type.Object({\n\treason: Type.Optional(Type.String({ enum: [\"need_decision\", \"interview_request\", \"progress_update\"] })),\n\tmessage: Type.Optional(Type.String()),\n\tinterview: Type.Optional(Type.Unsafe({ type: \"object\", additionalProperties: true })),\n\taction: Type.Optional(Type.String({ enum: [\"reply\"] })),\n\treplyTo: Type.Optional(Type.String()),\n}, { additionalProperties: false });\n\nconst IntercomParamsSchema = Type.Object({\n\taction: Type.String({ enum: [\"list\", \"send\", \"ask\", \"reply\", \"pending\", \"status\"] }),\n\tto: Type.Optional(Type.String()),\n\tmessage: Type.Optional(Type.String()),\n\treplyTo: Type.Optional(Type.String()),\n\tid: Type.Optional(Type.String({ description: \"Async run id for action='ask'.\" })),\n\tindex: Type.Optional(Type.Integer({ minimum: 0, description: \"Zero-based child index for action='ask'. Select index or childId when more than one child is running.\" })),\n\tchildId: Type.Optional(Type.String({ description: \"Private child identity for action='ask'.\" })),\n\treason: Type.Optional(Type.String({ description: \"Concrete decision the child's answer will change. Required for action='ask'.\" })),\n}, { additionalProperties: false });\n\nconst ParentSupervisorParamsSchema = Type.Object({\n\t...IntercomParamsSchema.properties,\n\taction: Type.String({ enum: [\"list\", \"ask\", \"reply\", \"pending\", \"status\"] }),\n}, { additionalProperties: false });\n\nfunction safeSegment(value: string): string {\n\treturn value.trim().replace(/[^A-Za-z0-9._-]+/g, \"-\").replace(/^-+|-+$/g, \"\") || \"unknown\";\n}\n\nexport function ensureSupervisorChannelDir(channelDir: string): void {\n\tfs.mkdirSync(path.join(channelDir, REQUESTS_DIR), { recursive: true, mode: 0o700 });\n\tfs.mkdirSync(path.join(channelDir, REPLIES_DIR), { recursive: true, mode: 0o700 });\n\tfs.mkdirSync(path.join(channelDir, QUESTIONS_DIR), { recursive: true, mode: 0o700 });\n}\n\nfunction requestPath(channelDir: string, requestId: string): string {\n\treturn path.join(channelDir, REQUESTS_DIR, `${safeSegment(requestId)}.json`);\n}\n\nfunction replyPath(channelDir: string, requestId: string): string {\n\treturn path.join(channelDir, REPLIES_DIR, `${safeSegment(requestId)}.json`);\n}\n\nfunction readTextEnv(name: string): string | undefined {\n\tconst value = process.env[name]?.trim();\n\treturn value ? value : undefined;\n}\n\nfunction readChildMetadata(): {\n\tchannelDir: string;\n\trunId: string;\n\tagent: string;\n\tchildIndex: number;\n\torchestratorTarget?: string;\n\torchestratorSessionId?: string;\n\tchildTarget?: string;\n} | undefined {\n\tconst channelDir = readTextEnv(SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV);\n\tconst runId = readTextEnv(SUBAGENT_RUN_ID_ENV);\n\tconst agent = readTextEnv(SUBAGENT_CHILD_AGENT_ENV);\n\tconst rawIndex = readTextEnv(SUBAGENT_CHILD_INDEX_ENV);\n\tconst orchestratorSessionId = readTextEnv(SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV);\n\tif (!channelDir || !runId || !agent || !orchestratorSessionId || rawIndex === undefined || !/^\\d+$/.test(rawIndex)) return undefined;\n\treturn {\n\t\tchannelDir,\n\t\trunId,\n\t\tagent,\n\t\tchildIndex: Number(rawIndex),\n\t\torchestratorTarget: readTextEnv(SUBAGENT_ORCHESTRATOR_TARGET_ENV),\n\t\torchestratorSessionId,\n\t\tchildTarget: readTextEnv(\"PI_SUBAGENT_INTERCOM_SESSION_NAME\"),\n\t};\n}\n\nfunction readChildQuestionOwner(metadata = readChildMetadata()): SupervisorQuestionOwner | undefined {\n\tif (!metadata) return undefined;\n\tconst readyAt = Number(process.env.PI_SUBAGENT_STEER_READY_AT);\n\tif (!Number.isFinite(readyAt) || readyAt <= 0) return undefined;\n\treturn {\n\t\trunId: metadata.runId,\n\t\tchildIndex: metadata.childIndex,\n\t\tchildId: metadata.agent,\n\t\tpid: process.pid,\n\t\treadyAt,\n\t};\n}\n\nfunction answerParentQuestion(params: ContactSupervisorParams): AgentToolResult<Record<string, unknown>> {\n\tconst metadata = readChildMetadata();\n\tconst owner = readChildQuestionOwner(metadata);\n\tif (!metadata || !owner) throw new Error(\"Native supervisor channel is not available for this subagent.\");\n\tconst replyTo = params.replyTo?.trim();\n\tif (!replyTo) throw new Error(\"action='reply' requires replyTo with the parent question id.\");\n\tconst message = params.message?.trim();\n\tif (!message) throw new Error(\"message is required for supervisor question replies.\");\n\tconst answered = answerSupervisorQuestion(metadata.channelDir, replyTo, message, owner);\n\treturn {\n\t\tcontent: [{ type: \"text\", text: `Replied to parent question ${answered.id}. Continue the assigned task.` }],\n\t\tdetails: {\n\t\t\treplyTo: answered.id,\n\t\t\tstate: supervisorQuestionPublicState(answered),\n\t\t\tanswered: true,\n\t\t},\n\t};\n}\n\nfunction reasonHeading(reason: SupervisorReason): string {\n\tif (reason === \"interview_request\") return \"Subagent requests a structured supervisor interview.\";\n\tif (reason === \"progress_update\") return \"Subagent progress update.\";\n\treturn \"Subagent needs a supervisor decision.\";\n}\n\nfunction formatChildMessage(input: {\n\treason: SupervisorReason;\n\tmessage?: string;\n\tinterview?: unknown;\n\trunId: string;\n\tagent: string;\n\tchildIndex: number;\n\tchildTarget?: string;\n}): string {\n\tconst lines = [\n\t\treasonHeading(input.reason),\n\t\t`Run: ${input.runId}`,\n\t\t`Agent: ${input.agent}`,\n\t\t`Child index: ${input.childIndex}`,\n\t];\n\tif (input.childTarget) lines.push(`Child intercom target: ${input.childTarget}`);\n\tlines.push(\"\");\n\tif (input.message?.trim()) lines.push(input.message.trim());\n\tif (input.reason === \"interview_request\") {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t\"Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.\",\n\t\t);\n\t\tif (input.interview !== undefined) lines.push(JSON.stringify(input.interview, null, \"\\t\"));\n\t}\n\treturn lines.join(\"\\n\").trimEnd();\n}\n\nfunction parseStructuredReply(message: string): { value?: unknown; error?: string } {\n\tconst trimmed = message.trim();\n\tconst fenced = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i)?.[1]?.trim();\n\ttry {\n\t\treturn { value: JSON.parse(fenced ?? trimmed) };\n\t} catch (error) {\n\t\treturn { error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) };\n\t}\n}\n\nfunction askTimeoutMs(): number {\n\tconst parsed = Number(process.env.PI_INTERCOM_ASK_TIMEOUT_MS);\n\treturn Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ASK_TIMEOUT_MS;\n}\n\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Supervisor request cancelled.\"));\n\t\t\treturn;\n\t\t}\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\tconst cleanup = () => {\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tcleanup();\n\t\t\treject(new Error(\"Supervisor request cancelled.\"));\n\t\t};\n\t\ttimer = setTimeout(() => {\n\t\t\tcleanup();\n\t\t\tresolve();\n\t\t}, ms);\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nasync function waitForReply(channelDir: string, requestId: string, deadline: number, signal?: AbortSignal): Promise<SupervisorReply> {\n\tconst file = replyPath(channelDir, requestId);\n\twhile (Date.now() <= deadline) {\n\t\tif (signal?.aborted) throw new Error(\"Supervisor request cancelled.\");\n\t\tif (fs.existsSync(file)) {\n\t\t\tconst parsed = JSON.parse(fs.readFileSync(file, \"utf-8\")) as Partial<SupervisorReply>;\n\t\t\tif (parsed.type === \"subagent.supervisor.reply\" && parsed.requestId === requestId && typeof parsed.message === \"string\") {\n\t\t\t\treturn parsed as SupervisorReply;\n\t\t\t}\n\t\t}\n\t\tawait delay(250, signal);\n\t}\n\tthrow new Error(\"Timed out waiting for supervisor reply.\");\n}\n\nasync function sendSupervisorRequest(params: ContactSupervisorParams, signal?: AbortSignal): Promise<AgentToolResult<Record<string, unknown>>> {\n\tif (params.action === \"reply\" || params.replyTo?.trim()) {\n\t\treturn answerParentQuestion(params);\n\t}\n\tconst metadata = readChildMetadata();\n\tif (!metadata) throw new Error(\"Native supervisor channel is not available for this subagent.\");\n\tif (!params.reason) throw new Error(\"reason is required for supervisor contact.\");\n\tif (params.reason !== \"progress_update\" && hasOutstandingQuestionForChild(metadata.channelDir, metadata.childIndex, metadata.agent)) {\n\t\tthrow new Error(\"A parent question is already outstanding for this child. Answer it with action='reply' before opening a blocking supervisor request.\");\n\t}\n\tif (params.reason !== \"progress_update\" && !params.message?.trim() && params.reason !== \"interview_request\") {\n\t\tthrow new Error(\"message is required for supervisor decisions.\");\n\t}\n\tensureSupervisorChannelDir(metadata.channelDir);\n\tconst requestId = randomUUID();\n\tconst expectsReply = params.reason !== \"progress_update\";\n\tconst createdAt = Date.now();\n\tconst replyDeadline = createdAt + askTimeoutMs();\n\tconst expiresAt = expectsReply ? replyDeadline : undefined;\n\tconst message = formatChildMessage({ ...metadata, reason: params.reason, message: params.message, interview: params.interview });\n\tconst request: SupervisorRequest = {\n\t\ttype: \"subagent.supervisor.request\",\n\t\tid: requestId,\n\t\tcreatedAt,\n\t\t...(expiresAt !== undefined ? { expiresAt } : {}),\n\t\treason: params.reason,\n\t\tmessage,\n\t\texpectsReply,\n\t\t...(metadata.orchestratorTarget ? { orchestratorTarget: metadata.orchestratorTarget } : {}),\n\t\t...(metadata.orchestratorSessionId ? { orchestratorSessionId: metadata.orchestratorSessionId } : {}),\n\t\trunId: metadata.runId,\n\t\tagent: metadata.agent,\n\t\tchildIndex: metadata.childIndex,\n\t\t...(metadata.childTarget ? { childTarget: metadata.childTarget } : {}),\n\t\t...(params.interview !== undefined ? { interview: params.interview } : {}),\n\t};\n\tconst serialized = JSON.stringify(request, null, \"\\t\");\n\tif (Buffer.byteLength(serialized, \"utf-8\") > MAX_MESSAGE_BYTES) throw new Error(\"Supervisor request is too large.\");\n\twriteAtomicJson(requestPath(metadata.channelDir, requestId), request);\n\tif (expectsReply) {\n\t\tfor (const question of outstandingQuestionsForChild(metadata.channelDir, metadata.childIndex, metadata.agent)) {\n\t\t\tcancelSupervisorQuestion(metadata.channelDir, question.id, \"child needs a supervisor decision; reply to its request first\");\n\t\t}\n\t}\n\n\tif (!expectsReply) {\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: \"Supervisor progress update queued.\" }],\n\t\t\tdetails: { delivered: true, requestId, reason: params.reason },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst reply = await waitForReply(metadata.channelDir, requestId, replyDeadline, signal);\n\t\tconst details: Record<string, unknown> = { requestId, reason: params.reason };\n\t\tif (params.reason === \"interview_request\") {\n\t\t\tconst structured = parseStructuredReply(reply.message);\n\t\t\tif (structured.error) details.structuredReplyParseError = structured.error;\n\t\t\telse details.structuredReply = structured.value;\n\t\t}\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `**Reply from supervisor:**\\n${reply.message}` }],\n\t\t\tdetails,\n\t\t};\n\t} catch (error) {\n\t\tremoveRequestFile(requestPath(metadata.channelDir, requestId));\n\t\tthrow error;\n\t}\n}\n\nfunction hasTool(pi: ExtensionAPI, name: string): boolean {\n\ttry {\n\t\treturn pi.getAllTools?.().some((tool: { name?: unknown }) => tool.name === name) === true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function registerNativeSupervisorClient(pi: ExtensionAPI, options: { includeIntercomFallback?: boolean } = {}): void {\n\tif (!readChildMetadata()) return;\n\tconst includeIntercomFallback = options.includeIntercomFallback !== false;\n\tif (!hasTool(pi, \"contact_supervisor\")) {\n\t\tconst tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {\n\t\t\tname: \"contact_supervisor\",\n\t\t\tlabel: \"Contact Supervisor\",\n\t\t\tdescription: CHILD_CONTACT_DESCRIPTION,\n\t\t\tparameters: ContactSupervisorParamsSchema,\n\t\t\texecute(_id, params, signal) {\n\t\t\t\treturn sendSupervisorRequest(params as ContactSupervisorParams, signal);\n\t\t\t},\n\t\t};\n\t\tpi.registerTool(tool);\n\t}\n\tif (includeIntercomFallback && !hasTool(pi, \"intercom\")) {\n\t\tconst tool: ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> = {\n\t\t\tname: \"intercom\",\n\t\t\tlabel: \"Intercom\",\n\t\t\tdescription: \"Native supervisor-channel intercom fallback for subagents. Prefer contact_supervisor when available.\",\n\t\t\tparameters: IntercomParamsSchema,\n\t\t\tasync execute(_id, params, signal) {\n\t\t\t\tconst action = (params as IntercomParams).action;\n\t\t\t\tif (action === \"status\") return { content: [{ type: \"text\", text: \"Native supervisor channel is active.\" }], details: { active: true } };\n\t\t\t\tif (action === \"list\") return { content: [{ type: \"text\", text: \"Supervisor session available through contact_supervisor.\" }], details: { sessions: [] } };\n\t\t\t\tif (action === \"send\") return sendSupervisorRequest({ reason: \"progress_update\", message: (params as IntercomParams).message ?? \"\" }, signal);\n\t\t\t\tif (action === \"ask\") return sendSupervisorRequest({ reason: \"need_decision\", message: (params as IntercomParams).message ?? \"\" }, signal);\n\t\t\t\tthrow new Error(\"Native child intercom supports status, list, send, and ask. Use parent intercom reply from the supervisor session.\");\n\t\t\t},\n\t\t};\n\t\tpi.registerTool(tool);\n\t}\n}\n\nfunction parseRequestFile(file: string, channelDir: string): PendingSupervisorRequest | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(fs.readFileSync(file, \"utf-8\")) as Partial<SupervisorRequest>;\n\t\tif (parsed.type !== \"subagent.supervisor.request\") return undefined;\n\t\tif (typeof parsed.id !== \"string\" || !parsed.id) return undefined;\n\t\tif (parsed.reason !== \"need_decision\" && parsed.reason !== \"interview_request\" && parsed.reason !== \"progress_update\") return undefined;\n\t\tif (typeof parsed.message !== \"string\" || !parsed.message) return undefined;\n\t\tif (typeof parsed.runId !== \"string\" || typeof parsed.agent !== \"string\" || typeof parsed.childIndex !== \"number\") return undefined;\n\t\treturn { ...parsed as SupervisorRequest, channelDir, requestFile: file };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function hasPendingBlockingSupervisorRequest(runId: string, sessionId?: string | null, now = Date.now()): boolean {\n\tif (!runId) return false;\n\tfor (const { channelDir, file } of listRequestFiles()) {\n\t\tconst request = parseRequestFile(file, channelDir);\n\t\tif (!request?.expectsReply) continue;\n\t\tif (request.runId !== runId) continue;\n\t\tif (sessionId && request.orchestratorSessionId && request.orchestratorSessionId !== sessionId) continue;\n\t\tif (fs.existsSync(replyPath(request.channelDir, request.id))) continue;\n\t\tif (now > requestExpiresAt(request, now)) continue;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nfunction listRequestFiles(): Array<{ channelDir: string; file: string }> {\n\tlet channelEntries: fs.Dirent[];\n\ttry {\n\t\tchannelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\tthrow error;\n\t}\n\tconst files: Array<{ channelDir: string; file: string }> = [];\n\tfor (const entry of channelEntries) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name);\n\t\tconst requestsDir = path.join(channelDir, REQUESTS_DIR);\n\t\tlet requestEntries: fs.Dirent[];\n\t\ttry {\n\t\t\trequestEntries = fs.readdirSync(requestsDir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const requestEntry of requestEntries) {\n\t\t\tif (requestEntry.isFile() && requestEntry.name.endsWith(\".json\")) files.push({ channelDir, file: path.join(requestsDir, requestEntry.name) });\n\t\t}\n\t}\n\treturn files;\n}\n\nfunction readDirectoryEntries(dir: string): fs.Dirent[] | undefined {\n\ttry {\n\t\treturn fs.readdirSync(dir, { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\treturn undefined;\n\t}\n}\n\nfunction directoryMtimeMs(dir: string): number {\n\ttry {\n\t\treturn fs.statSync(dir).mtimeMs;\n\t} catch {\n\t\treturn 0;\n\t}\n}\n\nfunction removeEmptyDirectory(dir: string): boolean {\n\ttry {\n\t\tfs.rmdirSync(dir);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\tif (code === \"ENOENT\") return true;\n\t\tif (code === \"ENOTEMPTY\" || code === \"EEXIST\" || code === \"EPERM\" || code === \"EBUSY\") return false;\n\t\tthrow error;\n\t}\n}\n\nfunction removeStaleEmptySupervisorChannel(channelDir: string, nowMs: number): boolean {\n\tconst requestsDir = path.join(channelDir, REQUESTS_DIR);\n\tconst repliesDir = path.join(channelDir, REPLIES_DIR);\n\tconst questionsPath = path.join(channelDir, QUESTIONS_DIR);\n\tconst newestKnownMtimeMs = Math.max(\n\t\tdirectoryMtimeMs(channelDir),\n\t\tdirectoryMtimeMs(requestsDir),\n\t\tdirectoryMtimeMs(repliesDir),\n\t\tdirectoryMtimeMs(questionsPath),\n\t);\n\tif (nowMs - newestKnownMtimeMs < STALE_EMPTY_CHANNEL_AGE_MS) return false;\n\n\tconst requestEntries = readDirectoryEntries(requestsDir);\n\tif (!requestEntries || requestEntries.length > 0) return false;\n\tconst replyEntries = readDirectoryEntries(repliesDir);\n\tif (!replyEntries || replyEntries.length > 0) return false;\n\tconst questionEntries = readDirectoryEntries(questionsPath);\n\tif (!questionEntries || questionEntries.length > 0) return false;\n\n\tif (!removeEmptyDirectory(requestsDir)) return false;\n\tif (!removeEmptyDirectory(repliesDir)) return false;\n\tif (!removeEmptyDirectory(questionsPath)) return false;\n\tif (!removeEmptyDirectory(channelDir)) return false;\n\treturn true;\n}\n\nfunction cleanupStaleEmptySupervisorChannels(nowMs = Date.now()): number {\n\tlet channelEntries: fs.Dirent[];\n\ttry {\n\t\tchannelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return 0;\n\t\tthrow error;\n\t}\n\n\tlet removed = 0;\n\tfor (const entry of channelEntries) {\n\t\tif (!entry.isDirectory()) continue;\n\t\ttry {\n\t\t\tif (removeStaleEmptySupervisorChannel(path.join(SUPERVISOR_CHANNEL_ROOT, entry.name), nowMs)) removed++;\n\t\t} catch {\n\t\t\t// Cleanup is opportunistic; active writers can race with us and will be picked up by a later pass.\n\t\t}\n\t}\n\treturn removed;\n}\n\nfunction currentContextSessionId(state: Pick<SubagentState, \"currentSessionId\">, ctx: ExtensionContext): string | undefined {\n\ttry {\n\t\tconst sessionId = ctx.sessionManager.getSessionId();\n\t\tif (sessionId) return sessionId;\n\t} catch {\n\t\t// Fall through to the last known identity.\n\t}\n\treturn state.currentSessionId ?? undefined;\n}\n\nfunction requestMatchesContext(request: SupervisorRequest, state: Pick<SubagentState, \"currentSessionId\">, ctx: ExtensionContext): boolean {\n\tconst currentSessionId = currentContextSessionId(state, ctx);\n\treturn Boolean(currentSessionId && request.orchestratorSessionId === currentSessionId);\n}\n\nfunction removeRequestFile(file: string): void {\n\ttry {\n\t\tfs.rmSync(file, { force: true });\n\t} catch {\n\t\t// Request cleanup is best-effort; reply files and timeout errors remain authoritative.\n\t}\n}\n\ntype SupervisorRequestLifecycle = \"pending\" | \"resolved\" | \"expired\" | \"inactive\" | \"missing\" | \"wrong-session\";\n\nfunction requestExpiresAt(request: SupervisorRequest, now: number): number {\n\tconst expiresAt = (request as { expiresAt?: unknown }).expiresAt;\n\tif (typeof expiresAt === \"number\" && Number.isFinite(expiresAt)) return expiresAt;\n\treturn Number.isFinite(request.createdAt) ? request.createdAt + askTimeoutMs() : now;\n}\n\nfunction requestRunInactive(request: SupervisorRequest, state: SubagentState): boolean {\n\tif (state.foregroundControls.has(request.runId)) return false;\n\tconst foregroundRun = state.foregroundRuns?.get(request.runId);\n\tconst foregroundChild = foregroundRun?.children.find((child) => child.index === request.childIndex && child.agent === request.agent)\n\t\t?? foregroundRun?.children[request.childIndex];\n\tif (foregroundChild) return foregroundChild.status !== \"detached\";\n\n\tconst asyncJob = state.asyncJobs.get(request.runId);\n\tif (!asyncJob) return false;\n\tif (asyncJob.status === \"complete\" || asyncJob.status === \"failed\" || asyncJob.status === \"paused\") return true;\n\tconst stepStatus = asyncJob.steps?.[request.childIndex]?.status;\n\treturn stepStatus === \"complete\" || stepStatus === \"completed\" || stepStatus === \"failed\" || stepStatus === \"paused\";\n}\n\nfunction requestLifecycle(request: PendingSupervisorRequest, state: SubagentState, ctx: ExtensionContext | undefined, now: number): SupervisorRequestLifecycle {\n\tif (ctx && !requestMatchesContext(request, state, ctx)) return \"wrong-session\";\n\tif (!fs.existsSync(request.requestFile)) return \"missing\";\n\tif (request.expectsReply && fs.existsSync(replyPath(request.channelDir, request.id))) return \"resolved\";\n\tif (request.expectsReply && now > requestExpiresAt(request, now)) return \"expired\";\n\tif (request.expectsReply && requestRunInactive(request, state)) return \"inactive\";\n\treturn \"pending\";\n}\n\nfunction cleanupRequestLifecycle(request: PendingSupervisorRequest, lifecycle: SupervisorRequestLifecycle): void {\n\tif (lifecycle === \"resolved\" || lifecycle === \"expired\" || lifecycle === \"inactive\") removeRequestFile(request.requestFile);\n}\n\nfunction refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined): void {\n\tconst now = Date.now();\n\tfor (const request of pending.values()) {\n\t\tconst lifecycle = requestLifecycle(request, state, ctx, now);\n\t\tif (lifecycle === \"pending\") continue;\n\t\tpending.delete(request.id);\n\t\tcleanupRequestLifecycle(request, lifecycle);\n\t}\n}\n\nfunction formatPendingLine(request: PendingSupervisorRequest): string {\n\tconst replyHint = request.expectsReply ? ` Reply: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: \"reply\", replyTo: \"${request.id}\", message: \"...\" })` : \"\";\n\treturn `- ${request.id}: ${request.agent} [${request.runId}#${request.childIndex}] ${request.reason}.${replyHint}`;\n}\n\nfunction requestVisibleText(request: PendingSupervisorRequest): string {\n\tconst lines = [request.message];\n\tif (request.expectsReply) {\n\t\tlines.push(\"\", `Reply with: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: \"reply\", replyTo: \"${request.id}\", message: \"...\" })`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nfunction writeReply(request: PendingSupervisorRequest, message: string): void {\n\tif (!message.trim()) throw new Error(\"message is required for supervisor replies.\");\n\tconst reply: SupervisorReply = {\n\t\ttype: \"subagent.supervisor.reply\",\n\t\trequestId: request.id,\n\t\tcreatedAt: Date.now(),\n\t\tmessage: message.trim(),\n\t};\n\twriteAtomicJson(replyPath(request.channelDir, request.id), reply);\n\tremoveRequestFile(request.requestFile);\n}\n\nfunction resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, params: IntercomParams): PendingSupervisorRequest {\n\tif (params.replyTo) {\n\t\tconst request = pending.get(params.replyTo);\n\t\tif (!request) throw new Error(`No pending supervisor request found for replyTo '${params.replyTo}'.`);\n\t\treturn request;\n\t}\n\tconst requests = [...pending.values()].filter((request) => request.expectsReply);\n\tif (params.to) {\n\t\tconst normalizedTo = params.to.toLowerCase();\n\t\tconst matches = requests.filter((request) =>\n\t\t\trequest.id.toLowerCase().startsWith(normalizedTo)\n\t\t\t|| request.agent.toLowerCase() === normalizedTo\n\t\t\t|| request.childTarget?.toLowerCase() === normalizedTo,\n\t\t);\n\t\tif (matches.length === 1) return matches[0]!;\n\t\tif (matches.length > 1) throw new Error(`Multiple pending supervisor requests match '${params.to}'. Use replyTo.`);\n\t}\n\tif (requests.length === 1) return requests[0]!;\n\tif (requests.length === 0) throw new Error(\"No pending supervisor requests need a reply.\");\n\tthrow new Error(\"Multiple pending supervisor requests need replies. Use replyTo.\");\n}\n\nfunction publicPendingRequests(pending: Map<string, PendingSupervisorRequest>): Array<Record<string, unknown>> {\n\treturn [...pending.values()].map((request) => ({\n\t\tid: request.id,\n\t\trunId: request.runId,\n\t\tagent: request.agent,\n\t\tchildIndex: request.childIndex,\n\t\treason: request.reason,\n\t\texpectsReply: request.expectsReply,\n\t}));\n}\n\nfunction buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, name = \"intercom\"): ToolDefinition<typeof ParentSupervisorParamsSchema, Record<string, unknown>> {\n\treturn {\n\t\tname,\n\t\tlabel: name === \"intercom\" ? \"Intercom\" : \"Subagent Supervisor\",\n\t\tdescription: PARENT_ASK_DESCRIPTION,\n\t\tparameters: ParentSupervisorParamsSchema,\n\t\tasync execute(_id, params) {\n\t\t\trefreshPendingRequests(pending, state, state.lastUiContext ?? undefined);\n\t\t\tconst input = params as IntercomParams;\n\t\t\tif (input.action === \"status\") {\n\t\t\t\treturn { content: [{ type: \"text\", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };\n\t\t\t}\n\t\t\tif (input.action === \"pending\" || input.action === \"list\") {\n\t\t\t\tconst lines = [...pending.values()].filter((request) => request.expectsReply).map(formatPendingLine);\n\t\t\t\treturn { content: [{ type: \"text\", text: lines.length ? lines.join(\"\\n\") : \"No pending supervisor requests.\" }], details: { pending: publicPendingRequests(pending) } };\n\t\t\t}\n\t\t\tif (input.action === \"reply\") {\n\t\t\t\tconst request = resolvePendingRequest(pending, input);\n\t\t\t\twriteReply(request, input.message ?? \"\");\n\t\t\t\tpending.delete(request.id);\n\t\t\t\treturn { content: [{ type: \"text\", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };\n\t\t\t}\n\t\t\tif (input.action === \"ask\") {\n\t\t\t\treturn askRunningAsyncChild({\n\t\t\t\t\tparams: {\n\t\t\t\t\t\tid: input.id,\n\t\t\t\t\t\tindex: input.index,\n\t\t\t\t\t\tchildId: input.childId,\n\t\t\t\t\t\treason: input.reason,\n\t\t\t\t\t\tmessage: input.message,\n\t\t\t\t\t},\n\t\t\t\t\tstate,\n\t\t\t\t\tchildRequests: pending.values(),\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (input.action === \"send\") {\n\t\t\t\tthrow new Error(\"Native pi-subagents intercom does not support free-form send. Use action='ask' for a running async child or action='reply' for a pending child request.\");\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported intercom action: ${input.action}`);\n\t\t},\n\t};\n}\n\nfunction questionMatchesParentContext(question: SupervisorQuestion, state: SubagentState, ctx: ExtensionContext): boolean {\n\tconst sessionId = currentContextSessionId(state, ctx);\n\tif (!sessionId || question.parentSessionId !== sessionId) return false;\n\tif ((state.sessionGeneration ?? 0) !== question.parentGeneration) return false;\n\treturn true;\n}\n\nfunction notifyQuestionTerminal(pi: ExtensionAPI, question: SupervisorQuestion, channelDir: string): void {\n\tif (!supervisorQuestionIsTerminal(question) || question.notifiedAt) return;\n\t(pi as { events?: IntercomEventBus }).events?.emit(SUBAGENT_QUESTION_EVENT, {\n\t\tquestionId: question.id,\n\t\tstate: supervisorQuestionPublicState(question),\n\t\trunId: question.runId,\n\t});\n\tif (isQuestionWaitActive(question.id)) return;\n\tif (!claimQuestionNotification(channelDir, question.id)) return;\n\tconst state = supervisorQuestionPublicState(question);\n\tpi.sendMessage({\n\t\tcustomType: SUBAGENT_SUPERVISOR_ANSWER_TYPE,\n\t\tcontent: formatSupervisorQuestionAnswer(question),\n\t\tdisplay: state === \"answered\",\n\t\tdetails: {\n\t\t\tquestionId: question.id,\n\t\t\tstate,\n\t\t\trunId: question.runId,\n\t\t\tchildIndex: question.childIndex,\n\t\t\tchildId: question.childId,\n\t\t\treason: question.reason,\n\t\t\tanswered: state === \"answered\",\n\t\t\tdelivered: Boolean(question.deliveredAt),\n\t\t},\n\t}, { triggerTurn: state === \"answered\" });\n}\n\nfunction pollSupervisorQuestions(pi: ExtensionAPI, state: SubagentState, ctx: ExtensionContext): void {\n\tfor (const channelDir of listSupervisorChannelDirs()) {\n\t\tfor (const question of listSupervisorQuestions(channelDir)) {\n\t\t\tif (!questionMatchesParentContext(question, state, ctx)) continue;\n\t\t\tconst current = reconcileAsyncQuestion(channelDir, question, state);\n\t\t\tnotifyQuestionTerminal(pi, current, channelDir);\n\t\t}\n\t}\n}\n\nexport function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState): {\n\tstart: () => void;\n\tdispose: () => void;\n\tpending: Map<string, PendingSupervisorRequest>;\n\tcancelOwnedQuestions: (reason: string) => void;\n\tsettleRunQuestions: (runId: string, reason: string) => void;\n} {\n\tconst pending = new Map<string, PendingSupervisorRequest>();\n\tconst seenFiles = new Set<string>();\n\tlet poller: ReturnType<typeof setInterval> | undefined;\n\tlet lastStaleCleanupAt = 0;\n\n\tconst registerParentTools = (): void => {\n\t\tif (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentIntercomTool(pending, state, NATIVE_SUPERVISOR_TOOL_NAME));\n\t\tif (!hasTool(pi, \"intercom\")) pi.registerTool(buildParentIntercomTool(pending, state));\n\t};\n\n\tconst cleanupStaleChannelsIfDue = (): void => {\n\t\tconst nowMs = Date.now();\n\t\tif (nowMs - lastStaleCleanupAt < STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS) return;\n\t\tlastStaleCleanupAt = nowMs;\n\t\ttry {\n\t\t\tcleanupStaleEmptySupervisorChannels(nowMs);\n\t\t} catch {\n\t\t\t// Supervisor delivery must not fail because best-effort temp cleanup failed.\n\t\t}\n\t};\n\n\tconst cancelOwnedQuestions = (reason: string): void => {\n\t\tif (!state.currentSessionId) return;\n\t\tcancelPendingSupervisorQuestionsForOwner({\n\t\t\tparentSessionId: state.currentSessionId,\n\t\t\tparentGeneration: state.sessionGeneration ?? 0,\n\t\t\treason,\n\t\t});\n\t};\n\n\tconst settleRunQuestions = (runId: string, reason: string): void => {\n\t\tif (!state.currentSessionId) return;\n\t\tcancelPendingSupervisorQuestionsForOwner({\n\t\t\tparentSessionId: state.currentSessionId,\n\t\t\trunId,\n\t\t\treason,\n\t\t});\n\t};\n\n\tconst poll = (): void => {\n\t\tcleanupStaleChannelsIfDue();\n\t\tconst ctx = state.lastUiContext;\n\t\tif (!ctx) return;\n\t\trefreshPendingRequests(pending, state, ctx);\n\t\tpollSupervisorQuestions(pi, state, ctx);\n\t\tconst now = Date.now();\n\t\tfor (const { channelDir, file } of listRequestFiles()) {\n\t\t\tif (seenFiles.has(file)) continue;\n\t\t\tconst request = parseRequestFile(file, channelDir);\n\t\t\tif (!request || !requestMatchesContext(request, state, ctx)) continue;\n\t\t\tconst lifecycle = requestLifecycle(request, state, undefined, now);\n\t\t\tif (lifecycle !== \"pending\") {\n\t\t\t\tseenFiles.add(file);\n\t\t\t\tcleanupRequestLifecycle(request, lifecycle);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tseenFiles.add(file);\n\t\t\tif (request.expectsReply) pending.set(request.id, request);\n\t\t\telse {\n\t\t\t\tremoveRequestFile(request.requestFile);\n\t\t\t}\n\t\t\tpi.sendMessage({\n\t\t\t\tcustomType: \"subagent_supervisor_request\",\n\t\t\t\tcontent: requestVisibleText(request),\n\t\t\t\tdisplay: true,\n\t\t\t\tdetails: {\n\t\t\t\t\tid: request.id,\n\t\t\t\t\treason: request.reason,\n\t\t\t\t\texpectsReply: request.expectsReply,\n\t\t\t\t\trunId: request.runId,\n\t\t\t\t\tagent: request.agent,\n\t\t\t\t\tchildIndex: request.childIndex,\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (request.expectsReply) {\n\t\t\t\tconst job = state.asyncJobs.get(request.runId);\n\t\t\t\tif (job) job.activityState = \"needs_attention\";\n\t\t\t\tconst events = (pi as { events?: IntercomEventBus }).events;\n\t\t\t\tevents?.emit(INTERCOM_DETACH_REQUEST_EVENT, {\n\t\t\t\t\trequestId: request.id,\n\t\t\t\t\trunId: request.runId,\n\t\t\t\t\tagent: request.agent,\n\t\t\t\t\tchildIndex: request.childIndex,\n\t\t\t\t});\n\t\t\t\tevents?.emit(SUBAGENT_CONTROL_EVENT, {\n\t\t\t\t\ttype: \"needs_attention\",\n\t\t\t\t\tto: \"needs_attention\",\n\t\t\t\t\trunId: request.runId,\n\t\t\t\t\tagent: request.agent,\n\t\t\t\t\tindex: request.childIndex,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tstart: () => {\n\t\t\tif (poller) return;\n\t\t\tregisterParentTools();\n\t\t\tpoll();\n\t\t\tpoller = setInterval(poll, CHANNEL_POLL_MS);\n\t\t\tpoller.unref?.();\n\t\t},\n\t\tdispose: () => {\n\t\t\tcancelOwnedQuestions(\"parent supervisor channel disposed\");\n\t\t\tif (poller) clearInterval(poller);\n\t\t\tpoller = undefined;\n\t\t\tpending.clear();\n\t\t\tseenFiles.clear();\n\t\t},\n\t\tpending,\n\t\tcancelOwnedQuestions,\n\t\tsettleRunQuestions,\n\t};\n}\n\nexport function isPendingQuestionForCurrentChild(questionId: string): boolean {\n\tconst metadata = readChildMetadata();\n\tconst owner = readChildQuestionOwner(metadata);\n\tif (!metadata || !owner) return false;\n\treturn pendingSupervisorQuestionsForDelivery(metadata.channelDir, owner).some((question) =>\n\t\tquestion.id === questionId && question.parentSessionId === metadata.orchestratorSessionId,\n\t);\n}\n\nexport function markDeliveredSupervisorQuestionFromChild(message: string): void {\n\tconst match = message.match(/Parent question \\(([^)\\s]+)\\)/);\n\tif (!match?.[1]) return;\n\tconst metadata = readChildMetadata();\n\tconst owner = readChildQuestionOwner(metadata);\n\tif (!metadata || !owner) return;\n\ttry {\n\t\tmarkSupervisorQuestionDelivered(metadata.channelDir, match[1], owner);\n\t} catch {\n\t\t// Delivery markers are best-effort; answer ownership still enforces identity.\n\t}\n}\n"]}
@@ -4,37 +4,61 @@ import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { Type } from "typebox";
6
6
  import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV, SUBAGENT_ORCHESTRATOR_TARGET_ENV, SUBAGENT_RUN_ID_ENV, SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV, } from "../runs/shared/pi-args.js";
7
- import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR } from "../shared/types.js";
7
+ import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, SUBAGENT_CONTROL_EVENT } from "../shared/types.js";
8
8
  import { writeAtomicJson } from "../shared/atomic-json.js";
9
- const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
9
+ import { askRunningAsyncChild, reconcileAsyncQuestion } from "./supervisor-ask.js";
10
+ import { answerSupervisorQuestion, cancelPendingSupervisorQuestionsForOwner, cancelSupervisorQuestion, claimQuestionNotification, formatSupervisorQuestionAnswer, hasOutstandingQuestionForChild, isQuestionWaitActive, listSupervisorChannelDirs, listSupervisorQuestions, markSupervisorQuestionDelivered, outstandingQuestionsForChild, pendingSupervisorQuestionsForDelivery, QUESTIONS_DIR, resolveSupervisorChannelDir as resolveQuestionChannelDir, SUBAGENT_QUESTION_EVENT, SUBAGENT_SUPERVISOR_ANSWER_TYPE, SUPERVISOR_CHANNEL_ROOT, supervisorQuestionIsTerminal, supervisorQuestionPublicState, } from "./supervisor-questions.js";
10
11
  const REQUESTS_DIR = "requests";
11
12
  const REPLIES_DIR = "replies";
12
13
  export const NATIVE_SUPERVISOR_TOOL_NAME = "subagent_supervisor";
14
+ export const resolveSupervisorChannelDir = resolveQuestionChannelDir;
13
15
  const MAX_MESSAGE_BYTES = 64 * 1024;
14
16
  const DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;
15
17
  const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);
16
18
  const STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;
17
19
  const STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;
20
+ const PARENT_ASK_DESCRIPTION = [
21
+ "Ask a specific running async child, or reply to a child supervisor request.",
22
+ "Ask only when the child owns missing context, the answer changes a concrete next decision, and waiting risks a block or rework.",
23
+ "Use available results first, batch related questions, and keep working while awaiting the answer. Do not use ask for status checks, polling, duplicate queries, or step-by-step supervision. Follow up only when the answer leaves the original decision unresolved.",
24
+ "Returns a question id immediately. The child's explicit answer is delivered separately from the final run result.",
25
+ 'Ask: { action: "ask", id: "<runId>", index?: 0, childId?: "...", reason: "<concrete decision>", message: "..." }',
26
+ "Use pending or list to find child requests awaiting your reply; status reports channel availability. Free-form send is unsupported. Asking never revives a child or changes its assignment.",
27
+ 'Reply: { action: "reply", replyTo: "<requestId>", message: "..." }',
28
+ ].join(" ");
29
+ const CHILD_CONTACT_DESCRIPTION = [
30
+ "Contact the parent/supervisor session for a blocking decision, structured interview, progress update, or reply to a parent question.",
31
+ "For parent questions use action='reply' with replyTo set to the question id. Answer from current findings and uncertainty; do not start extra investigation just to reply. Continue the assigned task after replying.",
32
+ "Do not open a blocking need_decision while a parent question is already outstanding for this child.",
33
+ ].join(" ");
18
34
  const ContactSupervisorParamsSchema = Type.Object({
19
- reason: Type.String({ enum: ["need_decision", "interview_request", "progress_update"] }),
35
+ reason: Type.Optional(Type.String({ enum: ["need_decision", "interview_request", "progress_update"] })),
20
36
  message: Type.Optional(Type.String()),
21
37
  interview: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true })),
38
+ action: Type.Optional(Type.String({ enum: ["reply"] })),
39
+ replyTo: Type.Optional(Type.String()),
22
40
  }, { additionalProperties: false });
23
41
  const IntercomParamsSchema = Type.Object({
24
42
  action: Type.String({ enum: ["list", "send", "ask", "reply", "pending", "status"] }),
25
43
  to: Type.Optional(Type.String()),
26
44
  message: Type.Optional(Type.String()),
27
45
  replyTo: Type.Optional(Type.String()),
46
+ id: Type.Optional(Type.String({ description: "Async run id for action='ask'." })),
47
+ index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for action='ask'. Select index or childId when more than one child is running." })),
48
+ childId: Type.Optional(Type.String({ description: "Private child identity for action='ask'." })),
49
+ reason: Type.Optional(Type.String({ description: "Concrete decision the child's answer will change. Required for action='ask'." })),
50
+ }, { additionalProperties: false });
51
+ const ParentSupervisorParamsSchema = Type.Object({
52
+ ...IntercomParamsSchema.properties,
53
+ action: Type.String({ enum: ["list", "ask", "reply", "pending", "status"] }),
28
54
  }, { additionalProperties: false });
29
55
  function safeSegment(value) {
30
56
  return value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
31
57
  }
32
- export function resolveSupervisorChannelDir(runId, agent, childIndex) {
33
- return path.join(SUPERVISOR_CHANNEL_ROOT, `${safeSegment(runId)}-${safeSegment(agent)}-${childIndex}`);
34
- }
35
58
  export function ensureSupervisorChannelDir(channelDir) {
36
59
  fs.mkdirSync(path.join(channelDir, REQUESTS_DIR), { recursive: true, mode: 0o700 });
37
60
  fs.mkdirSync(path.join(channelDir, REPLIES_DIR), { recursive: true, mode: 0o700 });
61
+ fs.mkdirSync(path.join(channelDir, QUESTIONS_DIR), { recursive: true, mode: 0o700 });
38
62
  }
39
63
  function requestPath(channelDir, requestId) {
40
64
  return path.join(channelDir, REQUESTS_DIR, `${safeSegment(requestId)}.json`);
@@ -64,6 +88,41 @@ function readChildMetadata() {
64
88
  childTarget: readTextEnv("PI_SUBAGENT_INTERCOM_SESSION_NAME"),
65
89
  };
66
90
  }
91
+ function readChildQuestionOwner(metadata = readChildMetadata()) {
92
+ if (!metadata)
93
+ return undefined;
94
+ const readyAt = Number(process.env.PI_SUBAGENT_STEER_READY_AT);
95
+ if (!Number.isFinite(readyAt) || readyAt <= 0)
96
+ return undefined;
97
+ return {
98
+ runId: metadata.runId,
99
+ childIndex: metadata.childIndex,
100
+ childId: metadata.agent,
101
+ pid: process.pid,
102
+ readyAt,
103
+ };
104
+ }
105
+ function answerParentQuestion(params) {
106
+ const metadata = readChildMetadata();
107
+ const owner = readChildQuestionOwner(metadata);
108
+ if (!metadata || !owner)
109
+ throw new Error("Native supervisor channel is not available for this subagent.");
110
+ const replyTo = params.replyTo?.trim();
111
+ if (!replyTo)
112
+ throw new Error("action='reply' requires replyTo with the parent question id.");
113
+ const message = params.message?.trim();
114
+ if (!message)
115
+ throw new Error("message is required for supervisor question replies.");
116
+ const answered = answerSupervisorQuestion(metadata.channelDir, replyTo, message, owner);
117
+ return {
118
+ content: [{ type: "text", text: `Replied to parent question ${answered.id}. Continue the assigned task.` }],
119
+ details: {
120
+ replyTo: answered.id,
121
+ state: supervisorQuestionPublicState(answered),
122
+ answered: true,
123
+ },
124
+ };
125
+ }
67
126
  function reasonHeading(reason) {
68
127
  if (reason === "interview_request")
69
128
  return "Subagent requests a structured supervisor interview.";
@@ -143,9 +202,17 @@ async function waitForReply(channelDir, requestId, deadline, signal) {
143
202
  throw new Error("Timed out waiting for supervisor reply.");
144
203
  }
145
204
  async function sendSupervisorRequest(params, signal) {
205
+ if (params.action === "reply" || params.replyTo?.trim()) {
206
+ return answerParentQuestion(params);
207
+ }
146
208
  const metadata = readChildMetadata();
147
209
  if (!metadata)
148
210
  throw new Error("Native supervisor channel is not available for this subagent.");
211
+ if (!params.reason)
212
+ throw new Error("reason is required for supervisor contact.");
213
+ if (params.reason !== "progress_update" && hasOutstandingQuestionForChild(metadata.channelDir, metadata.childIndex, metadata.agent)) {
214
+ throw new Error("A parent question is already outstanding for this child. Answer it with action='reply' before opening a blocking supervisor request.");
215
+ }
149
216
  if (params.reason !== "progress_update" && !params.message?.trim() && params.reason !== "interview_request") {
150
217
  throw new Error("message is required for supervisor decisions.");
151
218
  }
@@ -176,6 +243,11 @@ async function sendSupervisorRequest(params, signal) {
176
243
  if (Buffer.byteLength(serialized, "utf-8") > MAX_MESSAGE_BYTES)
177
244
  throw new Error("Supervisor request is too large.");
178
245
  writeAtomicJson(requestPath(metadata.channelDir, requestId), request);
246
+ if (expectsReply) {
247
+ for (const question of outstandingQuestionsForChild(metadata.channelDir, metadata.childIndex, metadata.agent)) {
248
+ cancelSupervisorQuestion(metadata.channelDir, question.id, "child needs a supervisor decision; reply to its request first");
249
+ }
250
+ }
179
251
  if (!expectsReply) {
180
252
  return {
181
253
  content: [{ type: "text", text: "Supervisor progress update queued." }],
@@ -218,7 +290,7 @@ export function registerNativeSupervisorClient(pi, options = {}) {
218
290
  const tool = {
219
291
  name: "contact_supervisor",
220
292
  label: "Contact Supervisor",
221
- description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
293
+ description: CHILD_CONTACT_DESCRIPTION,
222
294
  parameters: ContactSupervisorParamsSchema,
223
295
  execute(_id, params, signal) {
224
296
  return sendSupervisorRequest(params, signal);
@@ -267,6 +339,25 @@ function parseRequestFile(file, channelDir) {
267
339
  return undefined;
268
340
  }
269
341
  }
342
+ export function hasPendingBlockingSupervisorRequest(runId, sessionId, now = Date.now()) {
343
+ if (!runId)
344
+ return false;
345
+ for (const { channelDir, file } of listRequestFiles()) {
346
+ const request = parseRequestFile(file, channelDir);
347
+ if (!request?.expectsReply)
348
+ continue;
349
+ if (request.runId !== runId)
350
+ continue;
351
+ if (sessionId && request.orchestratorSessionId && request.orchestratorSessionId !== sessionId)
352
+ continue;
353
+ if (fs.existsSync(replyPath(request.channelDir, request.id)))
354
+ continue;
355
+ if (now > requestExpiresAt(request, now))
356
+ continue;
357
+ return true;
358
+ }
359
+ return false;
360
+ }
270
361
  function listRequestFiles() {
271
362
  let channelEntries;
272
363
  try {
@@ -332,7 +423,8 @@ function removeEmptyDirectory(dir) {
332
423
  function removeStaleEmptySupervisorChannel(channelDir, nowMs) {
333
424
  const requestsDir = path.join(channelDir, REQUESTS_DIR);
334
425
  const repliesDir = path.join(channelDir, REPLIES_DIR);
335
- const newestKnownMtimeMs = Math.max(directoryMtimeMs(channelDir), directoryMtimeMs(requestsDir), directoryMtimeMs(repliesDir));
426
+ const questionsPath = path.join(channelDir, QUESTIONS_DIR);
427
+ const newestKnownMtimeMs = Math.max(directoryMtimeMs(channelDir), directoryMtimeMs(requestsDir), directoryMtimeMs(repliesDir), directoryMtimeMs(questionsPath));
336
428
  if (nowMs - newestKnownMtimeMs < STALE_EMPTY_CHANNEL_AGE_MS)
337
429
  return false;
338
430
  const requestEntries = readDirectoryEntries(requestsDir);
@@ -341,10 +433,15 @@ function removeStaleEmptySupervisorChannel(channelDir, nowMs) {
341
433
  const replyEntries = readDirectoryEntries(repliesDir);
342
434
  if (!replyEntries || replyEntries.length > 0)
343
435
  return false;
436
+ const questionEntries = readDirectoryEntries(questionsPath);
437
+ if (!questionEntries || questionEntries.length > 0)
438
+ return false;
344
439
  if (!removeEmptyDirectory(requestsDir))
345
440
  return false;
346
441
  if (!removeEmptyDirectory(repliesDir))
347
442
  return false;
443
+ if (!removeEmptyDirectory(questionsPath))
444
+ return false;
348
445
  if (!removeEmptyDirectory(channelDir))
349
446
  return false;
350
447
  return true;
@@ -506,10 +603,8 @@ function buildParentIntercomTool(pending, state, name = "intercom") {
506
603
  return {
507
604
  name,
508
605
  label: name === "intercom" ? "Intercom" : "Subagent Supervisor",
509
- description: name === "intercom"
510
- ? "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests."
511
- : "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
512
- parameters: IntercomParamsSchema,
606
+ description: PARENT_ASK_DESCRIPTION,
607
+ parameters: ParentSupervisorParamsSchema,
513
608
  async execute(_id, params) {
514
609
  refreshPendingRequests(pending, state, state.lastUiContext ?? undefined);
515
610
  const input = params;
@@ -526,13 +621,73 @@ function buildParentIntercomTool(pending, state, name = "intercom") {
526
621
  pending.delete(request.id);
527
622
  return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
528
623
  }
529
- if (input.action === "send" || input.action === "ask") {
530
- throw new Error("Native pi-subagents intercom currently handles supervisor replies. Child agents initiate asks with contact_supervisor.");
624
+ if (input.action === "ask") {
625
+ return askRunningAsyncChild({
626
+ params: {
627
+ id: input.id,
628
+ index: input.index,
629
+ childId: input.childId,
630
+ reason: input.reason,
631
+ message: input.message,
632
+ },
633
+ state,
634
+ childRequests: pending.values(),
635
+ });
636
+ }
637
+ if (input.action === "send") {
638
+ throw new Error("Native pi-subagents intercom does not support free-form send. Use action='ask' for a running async child or action='reply' for a pending child request.");
531
639
  }
532
640
  throw new Error(`Unsupported intercom action: ${input.action}`);
533
641
  },
534
642
  };
535
643
  }
644
+ function questionMatchesParentContext(question, state, ctx) {
645
+ const sessionId = currentContextSessionId(state, ctx);
646
+ if (!sessionId || question.parentSessionId !== sessionId)
647
+ return false;
648
+ if ((state.sessionGeneration ?? 0) !== question.parentGeneration)
649
+ return false;
650
+ return true;
651
+ }
652
+ function notifyQuestionTerminal(pi, question, channelDir) {
653
+ if (!supervisorQuestionIsTerminal(question) || question.notifiedAt)
654
+ return;
655
+ pi.events?.emit(SUBAGENT_QUESTION_EVENT, {
656
+ questionId: question.id,
657
+ state: supervisorQuestionPublicState(question),
658
+ runId: question.runId,
659
+ });
660
+ if (isQuestionWaitActive(question.id))
661
+ return;
662
+ if (!claimQuestionNotification(channelDir, question.id))
663
+ return;
664
+ const state = supervisorQuestionPublicState(question);
665
+ pi.sendMessage({
666
+ customType: SUBAGENT_SUPERVISOR_ANSWER_TYPE,
667
+ content: formatSupervisorQuestionAnswer(question),
668
+ display: state === "answered",
669
+ details: {
670
+ questionId: question.id,
671
+ state,
672
+ runId: question.runId,
673
+ childIndex: question.childIndex,
674
+ childId: question.childId,
675
+ reason: question.reason,
676
+ answered: state === "answered",
677
+ delivered: Boolean(question.deliveredAt),
678
+ },
679
+ }, { triggerTurn: state === "answered" });
680
+ }
681
+ function pollSupervisorQuestions(pi, state, ctx) {
682
+ for (const channelDir of listSupervisorChannelDirs()) {
683
+ for (const question of listSupervisorQuestions(channelDir)) {
684
+ if (!questionMatchesParentContext(question, state, ctx))
685
+ continue;
686
+ const current = reconcileAsyncQuestion(channelDir, question, state);
687
+ notifyQuestionTerminal(pi, current, channelDir);
688
+ }
689
+ }
690
+ }
536
691
  export function createNativeSupervisorChannel(pi, state) {
537
692
  const pending = new Map();
538
693
  const seenFiles = new Set();
@@ -556,12 +711,31 @@ export function createNativeSupervisorChannel(pi, state) {
556
711
  // Supervisor delivery must not fail because best-effort temp cleanup failed.
557
712
  }
558
713
  };
714
+ const cancelOwnedQuestions = (reason) => {
715
+ if (!state.currentSessionId)
716
+ return;
717
+ cancelPendingSupervisorQuestionsForOwner({
718
+ parentSessionId: state.currentSessionId,
719
+ parentGeneration: state.sessionGeneration ?? 0,
720
+ reason,
721
+ });
722
+ };
723
+ const settleRunQuestions = (runId, reason) => {
724
+ if (!state.currentSessionId)
725
+ return;
726
+ cancelPendingSupervisorQuestionsForOwner({
727
+ parentSessionId: state.currentSessionId,
728
+ runId,
729
+ reason,
730
+ });
731
+ };
559
732
  const poll = () => {
560
733
  cleanupStaleChannelsIfDue();
561
734
  const ctx = state.lastUiContext;
562
735
  if (!ctx)
563
736
  return;
564
737
  refreshPendingRequests(pending, state, ctx);
738
+ pollSupervisorQuestions(pi, state, ctx);
565
739
  const now = Date.now();
566
740
  for (const { channelDir, file } of listRequestFiles()) {
567
741
  if (seenFiles.has(file))
@@ -595,12 +769,23 @@ export function createNativeSupervisorChannel(pi, state) {
595
769
  },
596
770
  });
597
771
  if (request.expectsReply) {
598
- pi.events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
772
+ const job = state.asyncJobs.get(request.runId);
773
+ if (job)
774
+ job.activityState = "needs_attention";
775
+ const events = pi.events;
776
+ events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
599
777
  requestId: request.id,
600
778
  runId: request.runId,
601
779
  agent: request.agent,
602
780
  childIndex: request.childIndex,
603
781
  });
782
+ events?.emit(SUBAGENT_CONTROL_EVENT, {
783
+ type: "needs_attention",
784
+ to: "needs_attention",
785
+ runId: request.runId,
786
+ agent: request.agent,
787
+ index: request.childIndex,
788
+ });
604
789
  }
605
790
  }
606
791
  };
@@ -614,6 +799,7 @@ export function createNativeSupervisorChannel(pi, state) {
614
799
  poller.unref?.();
615
800
  },
616
801
  dispose: () => {
802
+ cancelOwnedQuestions("parent supervisor channel disposed");
617
803
  if (poller)
618
804
  clearInterval(poller);
619
805
  poller = undefined;
@@ -621,6 +807,30 @@ export function createNativeSupervisorChannel(pi, state) {
621
807
  seenFiles.clear();
622
808
  },
623
809
  pending,
810
+ cancelOwnedQuestions,
811
+ settleRunQuestions,
624
812
  };
625
813
  }
814
+ export function isPendingQuestionForCurrentChild(questionId) {
815
+ const metadata = readChildMetadata();
816
+ const owner = readChildQuestionOwner(metadata);
817
+ if (!metadata || !owner)
818
+ return false;
819
+ return pendingSupervisorQuestionsForDelivery(metadata.channelDir, owner).some((question) => question.id === questionId && question.parentSessionId === metadata.orchestratorSessionId);
820
+ }
821
+ export function markDeliveredSupervisorQuestionFromChild(message) {
822
+ const match = message.match(/Parent question \(([^)\s]+)\)/);
823
+ if (!match?.[1])
824
+ return;
825
+ const metadata = readChildMetadata();
826
+ const owner = readChildQuestionOwner(metadata);
827
+ if (!metadata || !owner)
828
+ return;
829
+ try {
830
+ markSupervisorQuestionDelivered(metadata.channelDir, match[1], owner);
831
+ }
832
+ catch {
833
+ // Delivery markers are best-effort; answer ownership still enforces identity.
834
+ }
835
+ }
626
836
  //# sourceMappingURL=native-supervisor-channel.js.map