@code-yeongyu/senpi 2026.11.6-2 → 2026.11.9-2

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 (244) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-3TZQXZKH.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-45MVD7BU.js} +1 -1
  6. package/dist/bundle/chunks/chunk-4JIBEZZE.js +106 -0
  7. package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-4UR6UFBH.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-527Q2UYM.js} +9 -8
  10. package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-622VYTRI.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-7CFORZPE.js} +221 -229
  12. package/dist/bundle/chunks/{chunk-GKAYPJ4E.js → chunk-7PROV6BB.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-4E7GDG4H.js → chunk-7VMEE62L.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-K3UHGDUQ.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-LHSWAJJL.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
  17. package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
  18. package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-M7GF73FO.js} +2 -2
  19. package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-NDSWTAYJ.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-Q6NR6644.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-SGAFIMAU.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-STGOYIXC.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
  24. package/dist/bundle/chunks/chunk-XZDCMDUH.js +2 -0
  25. package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-Y5L4UFMI.js} +2 -2
  26. package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-XBEFQW5A.js} +1 -1
  27. package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
  28. package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-QFEY3JMQ.js} +1 -1
  29. package/dist/bundle/chunks/{headless-host-ES74ZB6M.js → headless-host-YXUGONR3.js} +1 -1
  30. package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-VHX2ILLM.js} +1 -1
  31. package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-P3MRW63Z.js} +1 -1
  32. package/dist/bundle/chunks/interactive-mode-YGL3QHU6.js +2 -0
  33. package/dist/bundle/chunks/main-5SD4DHMC.js +2 -0
  34. package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-PGRHY3JP.js} +20 -17
  35. package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-SADLU7QX.js} +1 -1
  36. package/dist/bundle/chunks/{role-RRFFHZF5.js → role-4BUU46VW.js} +3 -3
  37. package/dist/bundle/chunks/{rotation-stream-BN2YK2MC.js → rotation-stream-TODBIHXJ.js} +1 -1
  38. package/dist/bundle/chunks/rpc-mode-27TC7W6K.js +2 -0
  39. package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-MXCC5IZR.js} +1 -1
  40. package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
  41. package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-MX56D6UE.js} +1 -1
  42. package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-IL6ERD4U.js} +1 -1
  43. package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-6VRC25MJ.js} +1 -1
  44. package/dist/bundle/chunks/session-worker.js +114 -110
  45. package/dist/bundle/cli.js +2 -2
  46. package/dist/bundle/index.js +1 -1
  47. package/dist/bundle/rpc-entry.js +1 -1
  48. package/dist/bundle/runtime-manifest.json +1 -1
  49. package/dist/core/agent-session-runtime.d.ts +2 -0
  50. package/dist/core/agent-session-runtime.js +9 -2
  51. package/dist/core/agent-session-services.d.ts +2 -0
  52. package/dist/core/agent-session-services.js +1 -0
  53. package/dist/core/agent-session.d.ts +32 -3
  54. package/dist/core/agent-session.js +142 -29
  55. package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
  56. package/dist/core/credential-pool/state-store.d.ts +1 -1
  57. package/dist/core/egress/classify.d.ts +8 -3
  58. package/dist/core/egress/classify.js +177 -158
  59. package/dist/core/egress/console-redactor.d.ts +7 -0
  60. package/dist/core/egress/console-redactor.js +74 -0
  61. package/dist/core/egress/credentials.d.ts +73 -0
  62. package/dist/core/egress/credentials.js +20 -0
  63. package/dist/core/egress/device-custody.d.ts +1 -1
  64. package/dist/core/egress/device-custody.js +18 -15
  65. package/dist/core/egress/device-proxy-call.d.ts +59 -0
  66. package/dist/core/egress/device-proxy-call.js +148 -0
  67. package/dist/core/egress/device-proxy-lost-calls.d.ts +27 -0
  68. package/dist/core/egress/device-proxy-lost-calls.js +28 -0
  69. package/dist/core/egress/device-proxy-ports.d.ts +49 -0
  70. package/dist/core/egress/device-proxy-ports.js +2 -0
  71. package/dist/core/egress/device-proxy-request.d.ts +34 -0
  72. package/dist/core/egress/device-proxy-request.js +69 -0
  73. package/dist/core/egress/device-proxy-waits.d.ts +40 -0
  74. package/dist/core/egress/device-proxy-waits.js +67 -0
  75. package/dist/core/egress/device-proxy.d.ts +3 -0
  76. package/dist/core/egress/device-proxy.js +214 -0
  77. package/dist/core/egress/endpoint.js +2 -1
  78. package/dist/core/egress/entry-verify.d.ts +13 -0
  79. package/dist/core/egress/entry-verify.js +33 -0
  80. package/dist/core/egress/hpke.d.ts +63 -0
  81. package/dist/core/egress/hpke.js +192 -0
  82. package/dist/core/egress/key-policy-template.d.ts +7 -1
  83. package/dist/core/egress/ledger-schema.d.ts +80 -0
  84. package/dist/core/egress/ledger-schema.js +117 -0
  85. package/dist/core/egress/ledger.d.ts +55 -0
  86. package/dist/core/egress/ledger.js +159 -0
  87. package/dist/core/egress/pinned-infra.d.ts +23 -2
  88. package/dist/core/egress/pinned-infra.js +9 -2
  89. package/dist/core/egress/placement-claim.d.ts +69 -0
  90. package/dist/core/egress/placement-claim.js +115 -0
  91. package/dist/core/egress/pool-state.d.ts +18 -0
  92. package/dist/core/egress/pool-state.js +5 -0
  93. package/dist/core/egress/redact.d.ts +10 -0
  94. package/dist/core/egress/redact.js +134 -0
  95. package/dist/core/egress/sealed-format.d.ts +101 -0
  96. package/dist/core/egress/sealed-format.js +174 -0
  97. package/dist/core/egress/sealed-source.d.ts +35 -0
  98. package/dist/core/egress/sealed-source.js +140 -0
  99. package/dist/core/egress/verified-keys.js +1 -1
  100. package/dist/core/egress/wire.d.ts +228 -0
  101. package/dist/core/egress/wire.js +49 -0
  102. package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
  103. package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
  104. package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
  105. package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
  106. package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
  107. package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
  108. package/dist/core/extensions/builtin/goal/index.js +46 -0
  109. package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
  110. package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
  111. package/dist/core/extensions/builtin/monitor-state-event.d.ts +26 -0
  112. package/dist/core/extensions/builtin/monitor-state-event.js +2 -0
  113. package/dist/core/extensions/builtin/permission-system/index.js +10 -4
  114. package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
  115. package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
  116. package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
  117. package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
  118. package/dist/core/extensions/builtin/terminal/background-age.d.ts +25 -0
  119. package/dist/core/extensions/builtin/terminal/background-age.js +91 -0
  120. package/dist/core/extensions/builtin/terminal/extension-state.d.ts +1 -1
  121. package/dist/core/extensions/builtin/terminal/extension-state.js +21 -3
  122. package/dist/core/extensions/builtin/terminal/notify.d.ts +6 -0
  123. package/dist/core/extensions/builtin/terminal/notify.js +39 -5
  124. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +9 -2
  125. package/dist/core/extensions/builtin/terminal/session-bundle.js +36 -1
  126. package/dist/core/extensions/builtin/terminal/settings.d.ts +2 -0
  127. package/dist/core/extensions/builtin/terminal/settings.js +8 -0
  128. package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.d.ts +15 -0
  129. package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.js +87 -0
  130. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +3 -1
  131. package/dist/core/extensions/builtin/terminal/tools/bash.js +44 -15
  132. package/dist/core/extensions/builtin/terminal/tools/child-placement.d.ts +8 -0
  133. package/dist/core/extensions/builtin/terminal/tools/child-placement.js +43 -6
  134. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +14 -2
  135. package/dist/core/extensions/bun-extension-importer.js +4 -3
  136. package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
  137. package/dist/core/extensions/bun-extension-load-turns.js +45 -0
  138. package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
  139. package/dist/core/extensions/bun-extension-registry.js +19 -1
  140. package/dist/core/extensions/loader.js +5 -0
  141. package/dist/core/extensions/session-control-types.d.ts +15 -0
  142. package/dist/core/extensions/session-control-types.js +11 -0
  143. package/dist/core/extensions/session-dialog-api.d.ts +5 -0
  144. package/dist/core/extensions/session-dialog-api.js +13 -0
  145. package/dist/core/extensions/types.d.ts +14 -1
  146. package/dist/core/external-admission.d.ts +16 -2
  147. package/dist/core/external-admission.js +22 -4
  148. package/dist/core/permission-decision.d.ts +22 -0
  149. package/dist/core/permission-decision.js +53 -0
  150. package/dist/core/permission-mode.d.ts +56 -0
  151. package/dist/core/permission-mode.js +133 -0
  152. package/dist/core/retry-fallback/controller-types.d.ts +6 -0
  153. package/dist/core/retry-fallback/controller.d.ts +9 -0
  154. package/dist/core/retry-fallback/controller.js +34 -3
  155. package/dist/core/retry-fallback/unusable.d.ts +25 -1
  156. package/dist/core/retry-fallback/unusable.js +87 -2
  157. package/dist/core/retry-fallback/usage-limit.d.ts +6 -1
  158. package/dist/core/retry-fallback/usage-limit.js +12 -4
  159. package/dist/core/sdk.d.ts +6 -1
  160. package/dist/core/sdk.js +3 -0
  161. package/dist/core/session-log.js +1 -1
  162. package/dist/core/stall-warning.d.ts +41 -0
  163. package/dist/core/stall-warning.js +72 -0
  164. package/dist/core/terminal-settings.d.ts +1 -0
  165. package/dist/core/turn-origin.d.ts +31 -0
  166. package/dist/core/turn-origin.js +64 -0
  167. package/dist/modes/interactive/components/remote-delivery-message.d.ts +2 -1
  168. package/dist/modes/interactive/components/remote-delivery-message.js +13 -5
  169. package/dist/modes/rpc/admit-external-message-command.d.ts +37 -0
  170. package/dist/modes/rpc/admit-external-message-command.js +61 -0
  171. package/dist/modes/rpc/admit-external-message-wire.d.ts +42 -0
  172. package/dist/modes/rpc/admit-external-message-wire.js +55 -0
  173. package/dist/modes/rpc/client-input-handler.d.ts +2 -0
  174. package/dist/modes/rpc/client-input-handler.js +2 -0
  175. package/dist/modes/rpc/connection-handler.js +30 -5
  176. package/dist/modes/rpc/connection-roles.d.ts +6 -0
  177. package/dist/modes/rpc/connection-roles.js +13 -0
  178. package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
  179. package/dist/modes/rpc/delegated-drive-origin.js +16 -0
  180. package/dist/modes/rpc/effect-commands.d.ts +7 -1
  181. package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
  182. package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
  183. package/dist/modes/rpc/host-capabilities.d.ts +2 -0
  184. package/dist/modes/rpc/host-capabilities.js +3 -0
  185. package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
  186. package/dist/modes/rpc/host-handover-parked.js +17 -0
  187. package/dist/modes/rpc/host-handover-reopen.js +18 -6
  188. package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
  189. package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
  190. package/dist/modes/rpc/host-idle-handover.js +1 -0
  191. package/dist/modes/rpc/host-warm.d.ts +16 -1
  192. package/dist/modes/rpc/host-warm.js +33 -6
  193. package/dist/modes/rpc/method-classes-schema-delivery.d.ts +32 -0
  194. package/dist/modes/rpc/method-classes-schema-delivery.js +22 -0
  195. package/dist/modes/rpc/method-classes-schema-session.d.ts +36 -0
  196. package/dist/modes/rpc/method-classes-schema-session.js +3 -0
  197. package/dist/modes/rpc/method-classes-schema.d.ts +36 -0
  198. package/dist/modes/rpc/method-classes.d.ts +2 -0
  199. package/dist/modes/rpc/method-classes.js +4 -0
  200. package/dist/modes/rpc/multi-session-host.js +6 -1
  201. package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
  202. package/dist/modes/rpc/permission-preset-command.js +27 -0
  203. package/dist/modes/rpc/rpc-input-validation.js +4 -3
  204. package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
  205. package/dist/modes/rpc/rpc-response-types.d.ts +2 -1
  206. package/dist/modes/rpc/rpc-session-command-types.d.ts +6 -1
  207. package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
  208. package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
  209. package/dist/modes/rpc/rpc-session-state.js +2 -0
  210. package/dist/modes/rpc/session-attachment-commands.js +3 -3
  211. package/dist/modes/rpc/session-event-writer-connections.js +1 -0
  212. package/dist/modes/rpc/session-event-writer.d.ts +1 -1
  213. package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
  214. package/dist/modes/rpc/session-registry-attach.js +16 -3
  215. package/dist/modes/rpc/session-registry-types.d.ts +2 -0
  216. package/dist/modes/rpc/session-registry.d.ts +2 -0
  217. package/dist/modes/rpc/session-registry.js +16 -3
  218. package/dist/modes/rpc/session-router-close.js +43 -6
  219. package/dist/modes/rpc/session-router-context.js +2 -0
  220. package/dist/modes/rpc/session-router-occupancy.js +2 -11
  221. package/dist/modes/rpc/session-router-open-profile.js +4 -0
  222. package/dist/modes/rpc/session-router-open.d.ts +1 -1
  223. package/dist/modes/rpc/session-router-open.js +6 -17
  224. package/dist/modes/rpc/session-router-residency.d.ts +24 -0
  225. package/dist/modes/rpc/session-router-residency.js +90 -0
  226. package/dist/modes/rpc/session-router-routing.js +7 -1
  227. package/dist/modes/rpc/session-status.js +4 -1
  228. package/dist/modes/rpc/session-worker.js +4 -0
  229. package/dist/modes/rpc/tool-child/terminal-jobs.d.ts +1 -0
  230. package/dist/modes/rpc/tool-child/terminal-jobs.js +17 -1
  231. package/dist/modes/rpc/worker-session-registry.js +5 -2
  232. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  233. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +29 -2
  234. package/docs/extensions.md +12 -0
  235. package/docs/rpc.md +129 -12
  236. package/docs/terminal-tools.md +25 -1
  237. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  238. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  239. package/package.json +6 -6
  240. package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
  241. package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
  242. package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
  243. package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
  244. package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
@@ -6,6 +6,7 @@ export declare const ptyBashSchema: Type.TObject<{
6
6
  timeout: Type.TOptional<Type.TNumber>;
7
7
  description: Type.TOptional<Type.TString>;
8
8
  run_in_background: Type.TOptional<Type.TBoolean>;
9
+ auto_reap_minutes: Type.TOptional<Type.TNumber>;
9
10
  cols: Type.TOptional<Type.TNumber>;
10
11
  rows: Type.TOptional<Type.TNumber>;
11
12
  }>;
@@ -50,10 +51,11 @@ export declare function createPtyBashTool(ctx: TerminalToolContext): {
50
51
  timeout: Type.TOptional<Type.TNumber>;
51
52
  description: Type.TOptional<Type.TString>;
52
53
  run_in_background: Type.TOptional<Type.TBoolean>;
54
+ auto_reap_minutes: Type.TOptional<Type.TNumber>;
53
55
  cols: Type.TOptional<Type.TNumber>;
54
56
  rows: Type.TOptional<Type.TNumber>;
55
57
  }>;
56
- execute(_toolCallId: string, input: PtyBashInput, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback, execCtx?: SessionEnvSource): Promise<TerminalToolResult>;
58
+ execute(toolCallId: string, input: PtyBashInput, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback, execCtx?: SessionEnvSource): Promise<TerminalToolResult>;
57
59
  };
58
60
  export {};
59
61
  //# sourceMappingURL=bash.d.ts.map
@@ -1,8 +1,12 @@
1
1
  import { Type } from "typebox";
2
+ import { getAgentDir } from "../../../../../config.js";
2
3
  import { annotateShellTrust } from "../../../../../engine-trust/shell-trust.js";
4
+ import { createSessionLogger } from "../../../../session-log.js";
5
+ import { stallThresholdMs, watchForStall } from "../../../../stall-warning.js";
3
6
  import { formatTerminalToolOutput } from "../output-format.js";
4
7
  import { BACKGROUND_START_GRACE_MS, DEFAULT_COLS, DEFAULT_ROWS, FOREGROUND_ENV_OVERRIDES, KILLED_SESSION_EXIT_GRACE_MS, TERMINAL_BASH_TOOL, } from "../shared.js";
5
8
  import { errorResult, noticedResult, textResult, } from "./context.js";
9
+ import { backgroundStart, trackBackgroundSession } from "./background-auto-reap.js";
6
10
  import { createForegroundDetachGate } from "./foreground-detach.js";
7
11
  import { resolveForegroundWindowSeconds, SLEEP_WAIT_WINDOW_SECONDS } from "./foreground-window.js";
8
12
  import { classifySleepWait } from "./sleep-wait.js";
@@ -16,6 +20,9 @@ export const ptyBashSchema = Type.Object({
16
20
  run_in_background: Type.Optional(Type.Boolean({
17
21
  description: "Run as a persistent background session: returns a bash_id immediately (read with bash_output, steer with bash_input, stop with kill_bash). A plain & job dies with the call's PTY hangup (native backend), and nohup at best leaves an untracked orphan (no bash_id, no log) that the call's timeout still group-kills. This flag is the only reliable way a job outlives the call.",
18
22
  })),
23
+ auto_reap_minutes: Type.Optional(Type.Number({
24
+ description: "Background sessions only: stop this session (TERM, then KILL to its process group) once it has run this many minutes. Defaults to the terminal.backgroundAutoReapMinutes setting, which is off; 0 turns it off for this call.",
25
+ })),
19
26
  cols: Type.Optional(Type.Number({ description: "PTY width in columns (default 120)." })),
20
27
  rows: Type.Optional(Type.Number({ description: "PTY height in rows (default 40)." })),
21
28
  });
@@ -76,6 +83,32 @@ export function createThrottledEmitter(emit, throttleMs = BASH_UPDATE_THROTTLE_M
76
83
  },
77
84
  };
78
85
  }
86
+ let defaultSessionLogger;
87
+ /**
88
+ * Spawn, logging once if the spawn outlives the call's own timeout (capped at the stall threshold). The call's
89
+ * kill timer and detach gate are armed only after spawn returns, so a spawn that never returns is otherwise
90
+ * silent (senpi#6189). The spawn itself is awaited exactly as before.
91
+ */
92
+ async function spawnReportingStall(ctx, request, toolCallId, execCtx) {
93
+ const thresholdMs = stallThresholdMs(request.timeoutMs === undefined ? undefined : request.timeoutMs / 1000);
94
+ const watch = watchForStall(thresholdMs, (elapsedMs) => {
95
+ const logger = ctx.sessionLogger ?? (defaultSessionLogger ??= createSessionLogger(getAgentDir()));
96
+ const sessionId = (execCtx ?? ctx.getSessionContext?.())?.sessionManager?.getSessionId();
97
+ logger.warn("bash_spawn_stalled", {
98
+ toolCallId,
99
+ toolName: TERMINAL_BASH_TOOL,
100
+ ...(sessionId === undefined ? {} : { sessionId }),
101
+ thresholdMs,
102
+ elapsedMs,
103
+ });
104
+ });
105
+ try {
106
+ return await spawnCommandSession(sessionSpawnContext(ctx), request);
107
+ }
108
+ finally {
109
+ watch.stop();
110
+ }
111
+ }
79
112
  function sessionSpawnContext(ctx) {
80
113
  return {
81
114
  ...ctx,
@@ -198,18 +231,18 @@ function scheduleDetachedSweep(ctx, id, runtime, timeoutMs, startedAt) {
198
231
  void ctx.manager.stop(id).catch(() => { });
199
232
  }, remainingMs);
200
233
  }
201
- async function runForeground(ctx, input, cols, rows, signal, cwd, onUpdate, execCtx) {
234
+ async function runForeground(ctx, input, cols, rows, signal, cwd, onUpdate, toolCallId, execCtx) {
202
235
  if (signal?.aborted)
203
236
  return errorResult("Command aborted");
204
237
  const timeoutMs = input.timeout !== undefined ? Math.trunc(input.timeout * 1000) : undefined;
205
- const { id, runtime } = await spawnCommandSession(sessionSpawnContext(ctx), {
238
+ const { id, runtime } = await spawnReportingStall(ctx, {
206
239
  command: input.command,
207
240
  cols,
208
241
  rows,
209
242
  timeoutMs,
210
243
  cwd,
211
244
  envOverrides: { ...sessionEnvOverrides(ctx, execCtx), ...FOREGROUND_ENV_OVERRIDES },
212
- });
245
+ }, toolCallId, execCtx);
213
246
  const startedAt = Date.now();
214
247
  const activity = `running ${input.command.slice(0, 80)}`;
215
248
  const emitOutputUpdate = () => {
@@ -277,9 +310,7 @@ async function runForeground(ctx, input, cols, rows, signal, cwd, onUpdate, exec
277
310
  }
278
311
  if (outcome === "detached") {
279
312
  scheduleDetachedSweep(ctx, id, runtime, timeoutMs, startedAt);
280
- ctx.onBackgroundStart?.(id, input.description ?? input.command, startedAt, input.command);
281
- if (ctx.onBackgroundExit)
282
- runtime.session.onExit(() => ctx.onBackgroundExit?.(id, runtime));
313
+ trackBackgroundSession(ctx, id, runtime, backgroundStart(input, ctx.backgroundAutoReapMinutes, startedAt));
283
314
  const delta = runtime.readDelta();
284
315
  const partialOutput = formatTerminalToolOutput(delta.text).text || "(no output yet)";
285
316
  const timeoutNote = input.timeout !== undefined && Number.isFinite(input.timeout)
@@ -334,19 +365,17 @@ async function runForeground(ctx, input, cols, rows, signal, cwd, onUpdate, exec
334
365
  },
335
366
  });
336
367
  }
337
- async function runBackground(ctx, input, cols, rows, cwd, execCtx) {
368
+ async function runBackground(ctx, input, cols, rows, cwd, toolCallId, execCtx) {
338
369
  // Background sessions are NEVER killed by `timeout` (bash-timeout injects a default into
339
370
  // every bash call); they live until exit or kill_bash, so no timeoutMs is passed.
340
- const { id, runtime } = await spawnCommandSession(sessionSpawnContext(ctx), {
371
+ const { id, runtime } = await spawnReportingStall(ctx, {
341
372
  command: input.command,
342
373
  cols,
343
374
  rows,
344
375
  cwd,
345
376
  envOverrides: sessionEnvOverrides(ctx, execCtx),
346
- });
347
- ctx.onBackgroundStart?.(id, input.description ?? input.command, Date.now(), input.command);
348
- if (ctx.onBackgroundExit)
349
- runtime.session.onExit(() => ctx.onBackgroundExit?.(id, runtime));
377
+ }, toolCallId, execCtx);
378
+ trackBackgroundSession(ctx, id, runtime, backgroundStart(input, ctx.backgroundAutoReapMinutes, Date.now()));
350
379
  // Capture any output the command emits within a short grace window (or its exit).
351
380
  await Promise.race([delay(BACKGROUND_START_GRACE_MS), runtime.session.waitExit()]);
352
381
  const early = formatTerminalToolOutput(runtime.readDelta().text).text;
@@ -368,13 +397,13 @@ export function createPtyBashTool(ctx) {
368
397
  promptGuidelines: ["PI_* env vars = current model + session details."],
369
398
  fullPromptGuidelines: ["You can inspect PI_* environment variables for current model and session details."],
370
399
  parameters: ptyBashSchema,
371
- async execute(_toolCallId, input, signal, onUpdate, execCtx) {
400
+ async execute(toolCallId, input, signal, onUpdate, execCtx) {
372
401
  const cols = resolveDimension(input.cols, ctx.defaultCols || DEFAULT_COLS);
373
402
  const rows = resolveDimension(input.rows, ctx.defaultRows || DEFAULT_ROWS);
374
403
  const cwd = execCtx?.cwd;
375
404
  if (input.run_in_background)
376
- return runBackground(ctx, input, cols, rows, cwd, execCtx);
377
- return runForeground(ctx, input, cols, rows, signal, cwd, onUpdate, execCtx);
405
+ return runBackground(ctx, input, cols, rows, cwd, toolCallId, execCtx);
406
+ return runForeground(ctx, input, cols, rows, signal, cwd, onUpdate, toolCallId, execCtx);
378
407
  },
379
408
  };
380
409
  }
@@ -27,11 +27,19 @@ export type TerminalBackgroundWake = {
27
27
  readonly id: string;
28
28
  readonly description: string;
29
29
  readonly startedAtMs: number;
30
+ readonly command?: string;
31
+ readonly autoReapMinutes?: number;
30
32
  } | {
31
33
  readonly event: "background_exit";
32
34
  readonly id: string;
33
35
  readonly exit: TerminalExitView["exitResult"];
34
36
  readonly output: string;
37
+ readonly autoReapedAfterMinutes?: number;
38
+ } | {
39
+ readonly event: "background_reap_failed";
40
+ readonly id: string;
41
+ readonly autoReapMinutes: number;
42
+ readonly reason: string;
35
43
  };
36
44
  export declare function lostSessionResult(id: string, generation: number): AgentToolResult<unknown>;
37
45
  export declare function terminalChildJob(ctx: TerminalToolContext, kind: (typeof TERMINAL_CHILD_JOB_KINDS)[keyof typeof TERMINAL_CHILD_JOB_KINDS]): ToolChildJob;
@@ -25,6 +25,9 @@ function childGenerationOf(id) {
25
25
  const generation = CHILD_SESSION_ID.exec(id)?.[1];
26
26
  return generation === undefined ? undefined : Number(generation);
27
27
  }
28
+ function optionalMinutes(value) {
29
+ return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : undefined;
30
+ }
28
31
  function parseBackgroundWake(value) {
29
32
  if (typeof value !== "object" || value === null)
30
33
  return undefined;
@@ -37,19 +40,46 @@ function parseBackgroundWake(value) {
37
40
  const startedAtMs = Reflect.get(value, "startedAtMs");
38
41
  if (typeof description !== "string" || typeof startedAtMs !== "number")
39
42
  return undefined;
40
- return { event, id, description, startedAtMs };
43
+ const command = Reflect.get(value, "command");
44
+ const autoReapMinutes = optionalMinutes(Reflect.get(value, "autoReapMinutes"));
45
+ return {
46
+ event,
47
+ id,
48
+ description,
49
+ startedAtMs,
50
+ ...(typeof command === "string" ? { command } : {}),
51
+ ...(autoReapMinutes === undefined ? {} : { autoReapMinutes }),
52
+ };
53
+ }
54
+ if (event === "background_reap_failed") {
55
+ const autoReapMinutes = optionalMinutes(Reflect.get(value, "autoReapMinutes"));
56
+ const reason = Reflect.get(value, "reason");
57
+ if (autoReapMinutes === undefined || typeof reason !== "string")
58
+ return undefined;
59
+ return { event, id, autoReapMinutes, reason };
41
60
  }
42
61
  if (event === "background_exit") {
43
62
  const output = Reflect.get(value, "output");
44
63
  const exit = Reflect.get(value, "exit");
45
64
  if (typeof output !== "string" || (exit !== null && typeof exit !== "object"))
46
65
  return undefined;
47
- return { event, id, exit: exit, output };
66
+ const autoReapedAfterMinutes = optionalMinutes(Reflect.get(value, "autoReapedAfterMinutes"));
67
+ return {
68
+ event,
69
+ id,
70
+ exit: exit,
71
+ output,
72
+ ...(autoReapedAfterMinutes === undefined ? {} : { autoReapedAfterMinutes }),
73
+ };
48
74
  }
49
75
  return undefined;
50
76
  }
51
- function exitView(exit, output) {
52
- return { exitResult: exit, fullOutput: () => output };
77
+ function exitView(exit, output, autoReapedAfterMinutes) {
78
+ return {
79
+ exitResult: exit,
80
+ fullOutput: () => output,
81
+ ...(autoReapedAfterMinutes === undefined ? {} : { autoReapedAfterMinutes }),
82
+ };
53
83
  }
54
84
  function bashIdOf(value) {
55
85
  if (typeof value !== "object" || value === null)
@@ -106,6 +136,9 @@ export function terminalChildJob(ctx, kind) {
106
136
  defaultRows: ctx.defaultRows,
107
137
  redactSecretsInOutput: ctx.redactSecretsInOutput === true,
108
138
  ...(ctx.timeoutAction === undefined ? {} : { timeoutAction: ctx.timeoutAction }),
139
+ ...(ctx.backgroundAutoReapMinutes === undefined
140
+ ? {}
141
+ : { backgroundAutoReapMinutes: ctx.backgroundAutoReapMinutes }),
109
142
  };
110
143
  },
111
144
  };
@@ -124,11 +157,15 @@ function backgroundRelay(ctx) {
124
157
  return;
125
158
  if (wake.event === "background_start") {
126
159
  running.set(wake.id, generation);
127
- ctx.onBackgroundStart?.(wake.id, wake.description, wake.startedAtMs);
160
+ ctx.onBackgroundStart?.(wake.id, wake.description, wake.startedAtMs, wake.command, wake.autoReapMinutes);
161
+ return;
162
+ }
163
+ if (wake.event === "background_reap_failed") {
164
+ ctx.onBackgroundReapFailed?.(wake.id, wake.autoReapMinutes, wake.reason);
128
165
  return;
129
166
  }
130
167
  running.delete(wake.id);
131
- ctx.onBackgroundExit?.(wake.id, exitView(wake.exit, wake.output));
168
+ ctx.onBackgroundExit?.(wake.id, exitView(wake.exit, wake.output, wake.autoReapedAfterMinutes));
132
169
  },
133
170
  childExited: (generation) => {
134
171
  for (const [id, startedOn] of running) {
@@ -2,6 +2,7 @@ import type { TextContent } from "@earendil-works/pi-ai";
2
2
  import type { TerminalSessionExit } from "@earendil-works/pi-pty";
3
3
  import type { TerminalManager } from "../manager.ts";
4
4
  import type { MonitorEvent, MonitorRegistry } from "../monitor-registry.ts";
5
+ import type { SessionLogger } from "../../../../session-log.ts";
5
6
  import type { TimeoutAction } from "../settings.ts";
6
7
  /** Shared dependencies handed to every terminal tool factory. */
7
8
  export interface TerminalToolContext {
@@ -16,6 +17,8 @@ export interface TerminalToolContext {
16
17
  readonly timeoutAction?: TimeoutAction;
17
18
  /** Resolved `terminal.maxDurableMonitors`; omitted contexts admit any number of persistent monitors. */
18
19
  readonly maxDurableMonitors?: number | "unlimited";
20
+ /** Resolved `terminal.backgroundAutoReapMinutes`: background sessions are stopped at this age; unset means never. */
21
+ readonly backgroundAutoReapMinutes?: number | undefined;
19
22
  /** Resolve the environment for spawned sessions (mirrors core bash `getShellEnv`). */
20
23
  readonly getEnv: () => NodeJS.ProcessEnv;
21
24
  /**
@@ -23,12 +26,17 @@ export interface TerminalToolContext {
23
26
  * expose PI_* session metadata to spawned commands like the core bash tool does.
24
27
  */
25
28
  readonly getSessionContext?: () => import("../../../types.ts").ExtensionContext | undefined;
26
- /** Registers a session when it transitions to background liveness; `command` is what it runs, when known. */
27
- readonly onBackgroundStart?: (id: string, description: string, startedAtMs: number, command?: string) => void;
29
+ /**
30
+ * Registers a session when it transitions to background liveness; `command` is what it runs, when known, and
31
+ * `autoReapMinutes` the age at which the opt-in auto-reap stops it.
32
+ */
33
+ readonly onBackgroundStart?: (id: string, description: string, startedAtMs: number, command?: string, autoReapMinutes?: number) => void;
28
34
  /** A person or the model wrote to a session's stdin (`bash_input`): it is interactive from now on. */
29
35
  readonly onSessionInput?: (id: string) => void;
30
36
  /** Notified when a background session exits, so the notify layer can wake the agent. */
31
37
  readonly onBackgroundExit?: (id: string, runtime: TerminalExitView) => void;
38
+ /** The opt-in auto-reap's one stop at `autoReapMinutes` did not end the session, for `reason`; it keeps running. */
39
+ readonly onBackgroundReapFailed?: (id: string, autoReapMinutes: number, reason: string) => void;
32
40
  /** Session-scoped monitor state, sharing the terminal manager's bash-id namespace. */
33
41
  readonly monitorRegistry?: MonitorRegistry;
34
42
  /** Receives filtered monitor line and terminal-summary events. */
@@ -48,10 +56,14 @@ export interface TerminalToolContext {
48
56
  * the server and only then lets the command run. Absent on the in-process path, which spawns exactly as before.
49
57
  */
50
58
  readonly launchBarrier?: TerminalLaunchBarrier;
59
+ /** Where a stalled spawn is reported (senpi#6189); defaults to the agent directory's session log. */
60
+ readonly sessionLogger?: Pick<SessionLogger, "warn">;
51
61
  }
52
62
  /** What a background exit notice reads: on the tool child it is a summary the child sent, not the live session. */
53
63
  export interface TerminalExitView {
54
64
  readonly exitResult: Pick<TerminalSessionExit, "exitCode" | "signal" | "cancelled" | "timedOut"> | null;
65
+ /** Set when the opt-in auto-reap stopped the session at this age (senpi#6201). */
66
+ readonly autoReapedAfterMinutes?: number;
55
67
  fullOutput(): string;
56
68
  }
57
69
  export interface TerminalLaunchBarrier {
@@ -14,6 +14,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
14
14
  import { parse } from "es-module-lexer/js";
15
15
  import { isCommonJsFile, isEsmByPackageType, parseImportClause, rewriteCommonJsImport, } from "./bun-extension-commonjs.js";
16
16
  import { ExtensionSourceError } from "./bun-extension-error.js";
17
+ import { loadSynchronously } from "./bun-extension-load-turns.js";
17
18
  import { createPreparedSourceCache } from "./bun-extension-prepared-cache.js";
18
19
  import { ExtensionGenerationDisposedError, extensionNamespace, registerExtensionGraph, runtimeModulePath, } from "./bun-extension-registry.js";
19
20
  import { hasUseStrictDirective, strictDirectiveModuleSource } from "./bun-extension-strict-directive.js";
@@ -181,7 +182,7 @@ export function createBunExtensionImporter(virtualModules, builtinRedirects = {}
181
182
  const evaluating = commonJsModules.get(id);
182
183
  if (evaluating !== undefined)
183
184
  return evaluating.exports;
184
- const result = nativeRequire(id);
185
+ const result = loadSynchronously(() => nativeRequire(id));
185
186
  return commonJs.has(id) ? result.default : result;
186
187
  },
187
188
  evaluateCommonJs(filename, body) {
@@ -240,7 +241,7 @@ export function createBunExtensionImporter(virtualModules, builtinRedirects = {}
240
241
  // A package directory loads through its main or index file, as a native import does.
241
242
  const entry = statSync(absolute).isDirectory() ? realpathSync(Bun.resolveSync(absolute, absolute)) : absolute;
242
243
  const { id } = fileTarget(entry);
243
- const module = await import(__rewriteRelativeImportExtension(id));
244
+ const module = await registration.holdDuring(import(__rewriteRelativeImportExtension(id)));
244
245
  const factory = module.default;
245
246
  if (typeof factory !== "function")
246
247
  return factory;
@@ -255,7 +256,7 @@ export function createBunExtensionImporter(virtualModules, builtinRedirects = {}
255
256
  async importNamespace(path) {
256
257
  assertActive();
257
258
  const { id } = fileTarget(realpathSync(resolve(path)));
258
- return import(__rewriteRelativeImportExtension(id));
259
+ return registration.holdDuring(import(__rewriteRelativeImportExtension(id)));
259
260
  },
260
261
  // Capture identity from the transpiler input, never from a later disk snapshot.
261
262
  compiledSources() {
@@ -0,0 +1,4 @@
1
+ export declare function loadInTurn<T>(load: () => T): T | Promise<T>;
2
+ /** Runs `require` so every module it loads is served synchronously, as a CommonJS require needs. */
3
+ export declare function loadSynchronously<T>(require: () => T): T;
4
+ //# sourceMappingURL=bun-extension-load-turns.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * One extension module per event-loop turn (senpi#6199). Bun fetches the whole static graph of an `import()` inside
3
+ * one microtask drain, so a synchronous `onLoad` holds the loop for every module of the graph: a host compiling an
4
+ * extension (its boot load, a warm, an open) answered no socket until the last one. During an import each load waits
5
+ * for its own turn instead; a synchronous `require()` cannot take a promise, so its loads still run at once.
6
+ */
7
+ const queued = [];
8
+ let draining = false;
9
+ let synchronousDepth = 0;
10
+ function drainOne() {
11
+ queued.shift()?.();
12
+ if (queued.length > 0)
13
+ setImmediate(drainOne);
14
+ else
15
+ draining = false;
16
+ }
17
+ export function loadInTurn(load) {
18
+ if (synchronousDepth > 0)
19
+ return load();
20
+ return new Promise((resolve, reject) => {
21
+ queued.push(() => {
22
+ try {
23
+ resolve(load());
24
+ }
25
+ catch (error) {
26
+ reject(error);
27
+ }
28
+ });
29
+ if (draining)
30
+ return;
31
+ draining = true;
32
+ setImmediate(drainOne);
33
+ });
34
+ }
35
+ /** Runs `require` so every module it loads is served synchronously, as a CommonJS require needs. */
36
+ export function loadSynchronously(require) {
37
+ synchronousDepth++;
38
+ try {
39
+ return require();
40
+ }
41
+ finally {
42
+ synchronousDepth--;
43
+ }
44
+ }
45
+ //# sourceMappingURL=bun-extension-load-turns.js.map
@@ -29,6 +29,7 @@ export declare class ExtensionGenerationDisposedError extends Error {
29
29
  }
30
30
  export declare function registerExtensionGraph(graph: ExtensionGraph, virtualModules: Readonly<Record<string, Readonly<Record<string, unknown>>>>): {
31
31
  generation: string;
32
+ holdDuring<T>(pending: Promise<T>): Promise<T>;
32
33
  dispose(): void;
33
34
  };
34
35
  /** Internal lifecycle diagnostics, shared by regression and compiled probes. */
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, isAbsolute } from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { loadInTurn } from "./bun-extension-load-turns.js";
4
5
  import { publishRuntimeMetadata } from "./extension-runtime-module.js";
5
6
  // esbuild reads an import() only when its second argument is fully static, and these
6
7
  // attributes are whatever the extension wrote. The call stays dynamic either way, so
@@ -33,6 +34,10 @@ function splitModuleId(id) {
33
34
  }
34
35
  const graphs = new Map();
35
36
  const collected = new FinalizationRegistry((generation) => graphs.delete(generation));
37
+ // Bun serves an import()'s modules one loop turn at a time (senpi#6199) and its loader keeps no reference to the graph
38
+ // serving them: a graph whose importer was already dropped was collected between two loads and failed the import as
39
+ // disposed (senpi#6208). A graph is held strongly while any import it serves is in flight.
40
+ const importing = new Map();
36
41
  let nextGeneration = 0;
37
42
  let installed = false;
38
43
  let pluginRegistrations = 0;
@@ -129,7 +134,7 @@ function installRegistry(virtualModules) {
129
134
  const parsed = splitModuleId(path);
130
135
  if (parsed === undefined)
131
136
  throw new ExtensionModuleIdError(path);
132
- return graphFor(parsed.generation).load(parsed.filename);
137
+ return loadInTurn(() => graphFor(parsed.generation).load(parsed.filename));
133
138
  });
134
139
  installed = true;
135
140
  },
@@ -143,6 +148,19 @@ export function registerExtensionGraph(graph, virtualModules) {
143
148
  collected.register(graph, generation, graph);
144
149
  return {
145
150
  generation,
151
+ async holdDuring(pending) {
152
+ importing.set(graph, (importing.get(graph) ?? 0) + 1);
153
+ try {
154
+ return await pending;
155
+ }
156
+ finally {
157
+ const remaining = (importing.get(graph) ?? 1) - 1;
158
+ if (remaining === 0)
159
+ importing.delete(graph);
160
+ else
161
+ importing.set(graph, remaining);
162
+ }
163
+ },
146
164
  dispose() {
147
165
  graphs.delete(generation);
148
166
  collected.unregister(graph);
@@ -53,6 +53,7 @@ import { cachedExtensionFactory, extensionModuleImporter, rememberExtensionFacto
53
53
  import { kernelToolInvokerStorage } from "./kernel-tools-context.js";
54
54
  import { createLifecycleApi } from "./lifecycle-api.js";
55
55
  import { withProviderLoginStart } from "./provider-login-start.js";
56
+ import { createSessionDialogApi } from "./session-dialog-api.js";
56
57
  import { StaleExtensionContextError } from "./stale-context-error.js";
57
58
  import { DEFAULT_EXTENSION_SESSION_PROFILE, EMPTY_SESSION_CONTEXT } from "./types.js";
58
59
  function sessionProfile(options) {
@@ -573,6 +574,10 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
573
574
  }
574
575
  return readPermissionPreset(sessionManager);
575
576
  },
577
+ sessionDialog: createSessionDialogApi(() => {
578
+ assertActive();
579
+ return runtime.createContext().ui;
580
+ }),
576
581
  getSubscriptionLimits() {
577
582
  assertActive();
578
583
  return subscriptionLimits.snapshot();
@@ -45,6 +45,15 @@ export type SessionControlSender = {
45
45
  readonly platform: string;
46
46
  readonly author?: string;
47
47
  };
48
+ /**
49
+ * The remote device a delivery's RPC connection speaks for, stamped by the engine from the connection it arrived on
50
+ * (senpi#6233). It is never taken from a caller's record or an extension's input, so it is the one provenance on a
51
+ * delivery that a remote caller cannot write.
52
+ */
53
+ export interface SessionControlRemoteOrigin {
54
+ readonly deviceId: string;
55
+ readonly deviceName?: string;
56
+ }
48
57
  export interface AdmitExternalMessageInput {
49
58
  readonly delivery_id: string;
50
59
  readonly text: string;
@@ -154,7 +163,13 @@ export interface SessionControlDeliveryDetails {
154
163
  readonly deliverAs: ExternalDeliverAs;
155
164
  readonly sender?: SessionControlSender;
156
165
  readonly display_text?: string;
166
+ /** Engine-stamped: the remote device the admitting connection speaks for. */
167
+ readonly remote_origin?: SessionControlRemoteOrigin;
168
+ /** Engine-stamped: the thread-chain hop count the delivery was admitted at. */
169
+ readonly chain_count?: number;
157
170
  }
171
+ /** The engine-stamped remote device a delivery's `details` names, when it names a well-formed one. */
172
+ export declare function sessionControlRemoteOriginOf(details: unknown): SessionControlRemoteOrigin | undefined;
158
173
  /** The sender a delivery's `details` names, when it names a well-formed one. */
159
174
  export declare function sessionControlSenderOf(details: unknown): SessionControlSender | undefined;
160
175
  /** Whether a custom-message `details` value proves it came through session-control admission. */
@@ -11,6 +11,17 @@
11
11
  export { RELOCATION_TOOL_NAMES } from "../session-relocation.js";
12
12
  /** `customType` of the transcript entry an admitted delivery becomes. */
13
13
  export const SESSION_CONTROL_DELIVERY_TYPE = "session_control_delivery";
14
+ /** The engine-stamped remote device a delivery's `details` names, when it names a well-formed one. */
15
+ export function sessionControlRemoteOriginOf(details) {
16
+ if (!isSessionControlDeliveryDetails(details) || typeof details.remote_origin !== "object")
17
+ return undefined;
18
+ const origin = { ...details.remote_origin };
19
+ if (typeof origin.deviceId !== "string" || origin.deviceId === "")
20
+ return undefined;
21
+ return typeof origin.deviceName === "string" && origin.deviceName !== ""
22
+ ? { deviceId: origin.deviceId, deviceName: origin.deviceName }
23
+ : { deviceId: origin.deviceId };
24
+ }
14
25
  /** The sender a delivery's `details` names, when it names a well-formed one. */
15
26
  export function sessionControlSenderOf(details) {
16
27
  if (!isSessionControlDeliveryDetails(details) || typeof details.sender !== "object" || details.sender === null)
@@ -0,0 +1,5 @@
1
+ import type { ExtensionUIContext } from "./types.ts";
2
+ /** Dialogs addressed to every client attached to the session (`pi.sessionDialog`). */
3
+ export type SessionDialogApi = Pick<ExtensionUIContext, "select" | "confirm" | "input" | "editor">;
4
+ export declare function createSessionDialogApi(currentUI: () => ExtensionUIContext): SessionDialogApi;
5
+ //# sourceMappingURL=session-dialog-api.d.ts.map
@@ -0,0 +1,13 @@
1
+ const toSession = (opts) => ({
2
+ ...opts,
3
+ audience: "session",
4
+ });
5
+ export function createSessionDialogApi(currentUI) {
6
+ return {
7
+ select: (title, options, opts) => currentUI().select(title, options, toSession(opts)),
8
+ confirm: (title, message, opts) => currentUI().confirm(title, message, toSession(opts)),
9
+ input: (title, placeholder, opts) => currentUI().input(title, placeholder, toSession(opts)),
10
+ editor: (title, prefill, opts) => currentUI().editor(title, prefill, toSession(opts)),
11
+ };
12
+ }
13
+ //# sourceMappingURL=session-dialog-api.js.map
@@ -51,6 +51,7 @@ import type { PermissionPresetName } from "./builtin/permission-system/types.ts"
51
51
  import type { EvalHandleHost } from "./eval-handle-host.ts";
52
52
  import type { ExtensionKernelTools } from "./kernel-tools-context.ts";
53
53
  import type { SessionControlActions, SessionControlWakeEvent } from "./session-control-types.ts";
54
+ import type { SessionDialogApi } from "./session-dialog-api.ts";
54
55
  import type { UserActivityKind } from "./user-activity.ts";
55
56
  export type { ExecOptions, ExecResult } from "../exec.ts";
56
57
  export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts";
@@ -72,6 +73,11 @@ export interface ExtensionUIDialogOptions {
72
73
  toolCallId?: string;
73
74
  /** Set with `toolCallId` when another tool (for example a codemode script) issued that call. */
74
75
  parentToolCallId?: string;
76
+ /**
77
+ * `session`: address the dialog to every client attached to the session instead of the connection whose command
78
+ * is running, so a prompt raised after that command returned still reaches someone who can answer. RPC field.
79
+ */
80
+ audience?: "session";
75
81
  }
76
82
  /** A guarded private single-line input; callers on older hosts must check capabilities first. */
77
83
  export interface ExtensionUIInputOptions extends ExtensionUIDialogOptions {
@@ -247,7 +253,7 @@ export interface ExtensionUIContext {
247
253
  /** Get the current text from the core input editor. */
248
254
  getEditorText(): string;
249
255
  /** Show a multi-line editor for text editing. */
250
- editor(title: string, prefill?: string): Promise<string | undefined>;
256
+ editor(title: string, prefill?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
251
257
  /** Stack additional autocomplete behavior on top of the built-in provider. */
252
258
  addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
253
259
  /**
@@ -2245,6 +2251,13 @@ export interface ExtensionAPI {
2245
2251
  * permission system is not running for the session, its setup failed, or the session is not bound yet.
2246
2252
  */
2247
2253
  getPermissionPreset(): PermissionPresetName | undefined;
2254
+ /**
2255
+ * Dialogs addressed to every client attached to this session, not only to the connection whose command is
2256
+ * running: a prompt raised after that command returned (a background child's permission request a task runner
2257
+ * forwards) still reaches someone who can answer. The first answer wins. Secret inputs stay with the raising
2258
+ * connection. In the terminal UI they show like any dialog.
2259
+ */
2260
+ sessionDialog: SessionDialogApi;
2248
2261
  /**
2249
2262
  * Every subscription account's latest limit windows: the records `get_subscription_limits` serves, fed by
2250
2263
  * the engine's usage poll and by turns (senpi#3684). An account nothing has reported for is absent.
@@ -13,8 +13,10 @@
13
13
  * a file that still refuses writes.
14
14
  */
15
15
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
16
- import { type AdmissionHoldReason, type AdmitExternalMessageInput, type AdmittedDeliveries, type ExternalAdmissionResult, type ExternalDeliverAs, type SessionAdmissionGate, type SessionControlDeliveryDetails } from "./extensions/session-control-types.ts";
16
+ import { type AdmissionHoldReason, type AdmitExternalMessageInput, type AdmittedDeliveries, type ExternalAdmissionResult, type ExternalDeliverAs, type SessionAdmissionGate, type SessionControlDeliveryDetails, type SessionControlRemoteOrigin } from "./extensions/session-control-types.ts";
17
17
  import type { CustomMessage } from "./messages.ts";
18
+ import type { HistoryReducer } from "./session-history-reduce.ts";
19
+ import { type InputTurnMark } from "./turn-origin.ts";
18
20
  export type SessionControlDeliveryMessage = CustomMessage<SessionControlDeliveryDetails>;
19
21
  export interface ExternalAdmissionPort {
20
22
  /** A run is active, or a prompt has claimed its start: new input queues behind it. */
@@ -28,6 +30,16 @@ export interface EditorHoldState {
28
30
  readonly hold_reason?: AdmissionHoldReason;
29
31
  readonly revision: number;
30
32
  }
33
+ /**
34
+ * What the engine itself adds to a delivery an RPC connection admits (senpi#6233): the remote device that connection
35
+ * speaks for, the thread-chain hop it was admitted at, and the origin mark its turn takes. `pi.session` never passes
36
+ * one, so neither an extension nor a caller's record can write these.
37
+ */
38
+ export interface DeliveryStamp {
39
+ readonly remoteOrigin?: SessionControlRemoteOrigin;
40
+ readonly chainCount?: number;
41
+ readonly turnMark?: InputTurnMark;
42
+ }
31
43
  export declare class ExternalAdmission {
32
44
  private turnEpochValue;
33
45
  private readonly pending;
@@ -70,7 +82,7 @@ export declare class ExternalAdmission {
70
82
  /** Undoes `close` when the hand-over it was closed for did not happen. */
71
83
  reopen(): void;
72
84
  gate(): SessionAdmissionGate;
73
- admit(input: AdmitExternalMessageInput): ExternalAdmissionResult;
85
+ admit(input: AdmitExternalMessageInput, stamp?: DeliveryStamp): ExternalAdmissionResult;
74
86
  list(): AdmittedDeliveries;
75
87
  /** A message's transcript entry was written: the file takes writes; a delivery's own entry makes it emitted. */
76
88
  observePersisted(message: AgentMessage): void;
@@ -96,5 +108,7 @@ export interface InputHold {
96
108
  /** The submission ended without being accepted (it threw, was cancelled, or never submitted). */
97
109
  end(): void;
98
110
  }
111
+ /** Every delivery id the transcript holds an entry for: what answers for the process-lifetime ledger after a restart. */
112
+ export declare const writtenDeliveryIds: HistoryReducer<Set<string>>;
99
113
  export declare function deliveryIdOf(message: AgentMessage): string | undefined;
100
114
  //# sourceMappingURL=external-admission.d.ts.map