@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
@@ -1,6 +1,7 @@
1
1
  import { isHostOwnedToolRuntimeCapability, toolRuntimeCapabilities, } from "../../cli/tool-runtime.js";
2
2
  import { messagePathProjectRoot, resolveMessagePath } from "../../core/message-path-resolution.js";
3
3
  import { startOpenTimer } from "../../core/open-timing.js";
4
+ import { admitExternalMessage } from "./admit-external-message-command.js";
4
5
  import { apiKeyLoginCredential } from "./api-key-login-credential.js";
5
6
  import { clientFacingIdentity } from "./client-admission-record.js";
6
7
  import { ExtensionOwnerLifetime } from "./extension-owner-lifetime.js";
@@ -51,12 +52,15 @@ import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } fro
51
52
  import { remoteOriginDeclaration, remoteOriginField } from "./remote-origin.js";
52
53
  import { advertisedCompactCapabilities, sessionDeclaration } from "./compact-negotiation.js";
53
54
  import { commandAuthority } from "./command-authority.js";
55
+ import { noteDelegatedRecord } from "./delegated-drive-origin.js";
54
56
  import { ConnectionRoles, DELEGATION_INCARNATION_STALE, roleConnection } from "./connection-roles.js";
55
57
  import { ACCOUNT_RESET_CAPABILITY, AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, ENTRIES_TAIL_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROVIDER_ACCOUNT_TEST_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
56
58
  import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetThinkingLevel, } from "./effect-commands.js";
57
59
  import { EffectLedger } from "./effect-ledger.js";
58
60
  import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
59
61
  import { handleModelCommand } from "./handlers-model.js";
62
+ import { SET_PERMISSION_PRESET_CAPABILITY } from "./host-capabilities.js";
63
+ import { runSetPermissionPreset } from "./permission-preset-command.js";
60
64
  import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
61
65
  import { extensionUiProgressReply, refusedExtensionUiResponse } from "./extension-ui-response.js";
62
66
  import { answerExtensionUiResponse } from "./handlers-answer.js";
@@ -86,11 +90,13 @@ import { answerHostResponse, isHostResponse, relocationChannel, } from "./sessio
86
90
  export { buildRpcSessionState } from "./rpc-session-state.js";
87
91
  /** The tool call a dialog is about, as wire fields (absent when the dialog is not a permission request). */
88
92
  function dialogCall(opts) {
93
+ const audience = opts?.audience === "session" ? { audience: "session" } : {};
89
94
  if (opts?.toolCallId === undefined)
90
- return {};
95
+ return audience;
91
96
  return {
92
97
  toolCallId: opts.toolCallId,
93
98
  ...(opts.parentToolCallId === undefined ? {} : { parentToolCallId: opts.parentToolCallId }),
99
+ ...audience,
94
100
  };
95
101
  }
96
102
  function loadedExtensionName(path) {
@@ -201,6 +207,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
201
207
  const connectionRoles = options.connectionRoles ?? new ConnectionRoles();
202
208
  const currentConnectionId = () => roleConnection(options.clientInfo?.connectionId());
203
209
  const hasRole = (role) => connectionRoles.has(currentConnectionId(), role);
210
+ const connectionOrigin = () => connectionRoles.isDelegated(currentConnectionId()) ? "delegated" : "local";
204
211
  const routingSessionId = options.sessionId;
205
212
  // True only while THIS connection's own command drives a replacement; the issuer
206
213
  // already learns the new identity from its command response.
@@ -449,7 +456,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
449
456
  ? questions.ask(request, opts)
450
457
  : degradeQuestion(createExtensionUIContext(), request, opts),
451
458
  select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
452
- confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
459
+ confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false)),
453
460
  input: (title, placeholder, opts) => opts?.secret
454
461
  ? secretInputs.input(title, placeholder, opts)
455
462
  : createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
@@ -564,7 +571,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
564
571
  // Host should track editor state locally if needed
565
572
  return "";
566
573
  },
567
- async editor(title, prefill) {
574
+ async editor(title, prefill, opts) {
568
575
  const id = crypto.randomUUID();
569
576
  const frame = {
570
577
  type: "extension_ui_request",
@@ -572,6 +579,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
572
579
  method: "editor",
573
580
  title,
574
581
  prefill,
582
+ ...dialogCall(opts),
575
583
  ...remoteOriginField(raiserOrigin()),
576
584
  };
577
585
  return new Promise((resolve, reject) => {
@@ -1000,6 +1008,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1000
1008
  // Handle a single command
1001
1009
  const handleCommand = async (command, requestContext) => {
1002
1010
  const id = command.id;
1011
+ noteDelegatedRecord(command, connectionOrigin(), session.sessionManager);
1003
1012
  switch (command.type) {
1004
1013
  case "get_protocol_info":
1005
1014
  return {
@@ -1033,6 +1042,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1033
1042
  SESSION_MODEL_SCOPE_CAPABILITY,
1034
1043
  RESUME_SESSION_CAPABILITY,
1035
1044
  ENTRIES_TAIL_CAPABILITY,
1045
+ SET_PERMISSION_PRESET_CAPABILITY,
1036
1046
  ...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
1037
1047
  ...(options.toolRuntime ? toolRuntimeCapabilities(options.toolRuntime) : []),
1038
1048
  // Client flags cannot advertise a host guard this classic binding does not run, nor a tool runtime.
@@ -1055,7 +1065,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1055
1065
  if (holdAutomaticInput(session, command, hasRole(INTERACTIVE_USER_CAPABILITY)))
1056
1066
  return success(id, "prompt", { disposition: "handled", heldAsNotice: true });
1057
1067
  turnRequested = true;
1058
- const admission = handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY) });
1068
+ const admission = handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY), connectionOrigin: connectionOrigin() });
1059
1069
  promptCalls.add(admission);
1060
1070
  void admission.finally(() => promptCalls.delete(admission));
1061
1071
  return undefined;
@@ -1125,7 +1135,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1125
1135
  if (holdAutomaticInput(session, command, hasRole(INTERACTIVE_USER_CAPABILITY)))
1126
1136
  return success(id, command.type, { disposition: "handled", heldAsNotice: true });
1127
1137
  turnRequested = true;
1128
- await handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY) });
1138
+ await handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY), connectionOrigin: connectionOrigin() });
1129
1139
  return undefined;
1130
1140
  }
1131
1141
  case "abort":
@@ -1223,6 +1233,19 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1223
1233
  const result = (await sessionControl?.wake(deliveryIds)) ?? {};
1224
1234
  return success(id, "wake", { admitted: result.admitted ?? [] });
1225
1235
  }
1236
+ case "admit_external_message": {
1237
+ const peer = {
1238
+ origin: connectionRoles.originOf(currentConnectionId()),
1239
+ connectionOrigin: connectionOrigin(),
1240
+ };
1241
+ const answer = admitExternalMessage(session, command, peer);
1242
+ if (!answer.ok) {
1243
+ const { refusal } = answer;
1244
+ return error(id, command.type, refusal.message, refusal.errorCode, "errorData" in refusal ? refusal.errorData : undefined);
1245
+ }
1246
+ turnRequested = true;
1247
+ return success(id, command.type, answer.data);
1248
+ }
1226
1249
  case "new_session": {
1227
1250
  const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
1228
1251
  const result = await runtimeHost.newSession(options);
@@ -1289,6 +1312,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1289
1312
  // =================================================================
1290
1313
  case "set_thinking_level":
1291
1314
  return runSetThinkingLevel(effectContext(), command);
1315
+ case "set_permission_preset":
1316
+ return runSetPermissionPreset(runtimeHost, command);
1292
1317
  case "cycle_thinking_level": {
1293
1318
  const level = session.cycleThinkingLevel();
1294
1319
  if (!level) {
@@ -34,6 +34,8 @@ export declare class ConnectionRoles {
34
34
  private readonly origins;
35
35
  /** A worker isolate's first-seen delegation per connection (`retain`), kept for the worker's life. */
36
36
  private readonly retained;
37
+ /** A worker isolate's connections whose bind or command carried a delegation (a legacy one is never retained). */
38
+ private readonly carried;
37
39
  private source;
38
40
  /** The host reads each connection's live record from its writer's delegations. */
39
41
  useDelegations(source: (connectionId: string) => DelegatedConnectionView | undefined): void;
@@ -59,11 +61,15 @@ export declare class ConnectionRoles {
59
61
  declareOrigin(connectionId: string, origin: RpcRemoteOrigin): void;
60
62
  /** The remote device this connection speaks for: a delegated connection's principal, else its declared origin. */
61
63
  originOf(connectionId: string | undefined): RpcRemoteOrigin | undefined;
64
+ /** Whether the connection is delegated (a remote device's): on the host, retained by a worker, or carried to one. */
65
+ isDelegated(connectionId: string | undefined): boolean;
62
66
  has(connectionId: string | undefined, role: string): boolean;
63
67
  /** A logical detach: drops claims of earlier incarnations of a delegated socket, and everything of a local one. */
64
68
  release(connectionId: string): void;
65
69
  /** The socket is gone: nothing of it is kept. */
66
70
  forget(connectionId: string): void;
71
+ /** Worker isolate: a bind or command for `connectionId` carried a delegation, in either mode. */
72
+ noteCarriedDelegation(connectionId: string): void;
67
73
  /** Forgets every connection, for a worker isolate whose session ends. */
68
74
  clear(): void;
69
75
  }
@@ -35,6 +35,8 @@ export class ConnectionRoles {
35
35
  this.origins = new Map();
36
36
  /** A worker isolate's first-seen delegation per connection (`retain`), kept for the worker's life. */
37
37
  this.retained = new Map();
38
+ /** A worker isolate's connections whose bind or command carried a delegation (a legacy one is never retained). */
39
+ this.carried = new Set();
38
40
  }
39
41
  /** The host reads each connection's live record from its writer's delegations. */
40
42
  useDelegations(source) {
@@ -125,6 +127,11 @@ export class ConnectionRoles {
125
127
  ? { deviceId: principal.deviceId }
126
128
  : { deviceId: principal.deviceId, deviceName: principal.deviceName };
127
129
  }
130
+ /** Whether the connection is delegated (a remote device's): on the host, retained by a worker, or carried to one. */
131
+ isDelegated(connectionId) {
132
+ const key = roleConnection(connectionId);
133
+ return this.viewOf(key) !== undefined || this.carried.has(key);
134
+ }
128
135
  has(connectionId, role) {
129
136
  const key = roleConnection(connectionId);
130
137
  const store = commandAuthority.getStore();
@@ -155,12 +162,18 @@ export class ConnectionRoles {
155
162
  this.claims.delete(connectionId);
156
163
  this.origins.delete(connectionId);
157
164
  this.retained.delete(connectionId);
165
+ this.carried.delete(connectionId);
166
+ }
167
+ /** Worker isolate: a bind or command for `connectionId` carried a delegation, in either mode. */
168
+ noteCarriedDelegation(connectionId) {
169
+ this.carried.add(connectionId);
158
170
  }
159
171
  /** Forgets every connection, for a worker isolate whose session ends. */
160
172
  clear() {
161
173
  this.claims.clear();
162
174
  this.origins.clear();
163
175
  this.retained.clear();
176
+ this.carried.clear();
164
177
  }
165
178
  }
166
179
  //# sourceMappingURL=connection-roles.js.map
@@ -0,0 +1,9 @@
1
+ import type { SessionManager } from "../../core/session-manager.ts";
2
+ import { type ConnectionOrigin } from "../../core/turn-origin.ts";
3
+ /**
4
+ * Marks a session as remote-driven when a delegated connection sends it any record that can act on its work, decided
5
+ * by the engine's own method-class table (`drive`, and `device_admin`, which grants more), never by a hand-kept list of
6
+ * commands, so a new run starter (`continue_from_leaf`, `wake`, `resume_for_run`, ...) cannot slip past the rule.
7
+ */
8
+ export declare function noteDelegatedRecord(command: object, origin: ConnectionOrigin, sessionManager: Pick<SessionManager, "getEntries" | "appendCustomEntry">): void;
9
+ //# sourceMappingURL=delegated-drive-origin.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { recordDelegatedOrigin } from "../../core/turn-origin.js";
2
+ import { classifyRpcMethod } from "./method-classes.js";
3
+ const ACTING_CLASSES = new Set(["drive", "device_admin"]);
4
+ /**
5
+ * Marks a session as remote-driven when a delegated connection sends it any record that can act on its work, decided
6
+ * by the engine's own method-class table (`drive`, and `device_admin`, which grants more), never by a hand-kept list of
7
+ * commands, so a new run starter (`continue_from_leaf`, `wake`, `resume_for_run`, ...) cannot slip past the rule.
8
+ */
9
+ export function noteDelegatedRecord(command, origin, sessionManager) {
10
+ if (origin !== "delegated")
11
+ return;
12
+ const methodClass = classifyRpcMethod(command);
13
+ if (methodClass !== undefined && ACTING_CLASSES.has(methodClass))
14
+ recordDelegatedOrigin(sessionManager);
15
+ }
16
+ //# sourceMappingURL=delegated-drive-origin.js.map
@@ -37,7 +37,7 @@ export declare function runAbort(ctx: EffectCommandContext, command: Command<"ab
37
37
  * or no longer exists.
38
38
  */
39
39
  export declare function runQueuedInputChange(ctx: EffectCommandContext, command: Command<"dequeue_message"> | Command<"edit_queued_message">): Promise<RpcResponse>;
40
- export declare function runSetThinkingLevel(ctx: EffectCommandContext, command: Command<"set_thinking_level">): Promise<import("./session-move-wire.ts").RpcMoveSessionResponse | import("./compact-admission-error.ts").UnknownAdmissionResponse | {
40
+ export declare function runSetThinkingLevel(ctx: EffectCommandContext, command: Command<"set_thinking_level">): Promise<import("./admit-external-message-wire.ts").RpcAdmitExternalMessageResponse | import("./session-move-wire.ts").RpcMoveSessionResponse | import("./compact-admission-error.ts").UnknownAdmissionResponse | {
41
41
  readonly id?: string;
42
42
  readonly type: "response";
43
43
  readonly command: "attach_session";
@@ -430,6 +430,12 @@ export declare function runSetThinkingLevel(ctx: EffectCommandContext, command:
430
430
  type: "response";
431
431
  command: "set_auto_compaction";
432
432
  success: true;
433
+ } | {
434
+ id?: string;
435
+ type: "response";
436
+ command: "set_permission_preset";
437
+ success: true;
438
+ data: import("../../core/permission-mode.ts").PermissionModeReply;
433
439
  } | {
434
440
  id?: string;
435
441
  type: "response";
@@ -0,0 +1,7 @@
1
+ export interface BareEndpointRegistration {
2
+ readonly agentDir: string;
3
+ readonly socket: string;
4
+ readonly log: (message: string) => void;
5
+ }
6
+ export declare function bindRegisteredEndpoint<T>(registration: BareEndpointRegistration, bind: () => Promise<T>): Promise<T>;
7
+ //# sourceMappingURL=host-bare-endpoint-registration.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The registry record of a socket host that binds its own public socket (`--listen`, `--headless`). No
3
+ * `host ensure` runs for such a host, so nothing else writes its `endpoint.json`, and `host gc` removes the
4
+ * record with the rest of a dead endpoint's directory: after one outage the restarted host would serve
5
+ * while every registry reader (`host status --all`, a relay device service) finds nothing (senpi#6190).
6
+ *
7
+ * So the host writes the record itself before it serves, and binds inside the endpoint's ensure lock, the
8
+ * lock `host gc` judges and removes under: a gc that judged the socket dead has finished its removal
9
+ * before this write, and one that starts later finds the socket bound and keeps the endpoint. A supervised
10
+ * host never comes here - its ensure wrote the record and holds that lock while it starts the host.
11
+ */
12
+ import { createDaemonDirectories, createHostDaemonPaths } from "./host-daemon-paths.js";
13
+ import { acquireHostEnsureLock } from "./host-ensure-lock.js";
14
+ /** Longer than a gc's hold of one endpoint (its 2 s lock wait, the evidence probes, the removal). */
15
+ const REGISTRATION_LOCK_WAIT_MS = 10_000;
16
+ export async function bindRegisteredEndpoint(registration, bind) {
17
+ // An abstract socket has no path, so it has no daemon directory either.
18
+ if (registration.socket.startsWith("\0"))
19
+ return bind();
20
+ const release = await acquireHostEnsureLock(registration.socket, REGISTRATION_LOCK_WAIT_MS).catch((cause) => {
21
+ registration.log(`endpoint ensure lock unavailable, registering without it: ${messageOf(cause)}`);
22
+ return undefined;
23
+ });
24
+ try {
25
+ await createDaemonDirectories(createHostDaemonPaths({ socket: registration.socket, agentDir: registration.agentDir })).catch((cause) => registration.log(`endpoint registry record not written: ${messageOf(cause)}`));
26
+ return await bind();
27
+ }
28
+ finally {
29
+ await release?.();
30
+ }
31
+ }
32
+ function messageOf(cause) {
33
+ return cause instanceof Error ? cause.message : String(cause);
34
+ }
35
+ //# sourceMappingURL=host-bare-endpoint-registration.js.map
@@ -5,6 +5,8 @@ export declare const UNLOAD_SESSION_CAPABILITY = "unload_session";
5
5
  export declare const SUBSCRIPTION_LIMITS_CAPABILITY = "subscription_limits";
6
6
  /** `resolve_path`: which file a message's path means, the engine's one rule (senpi#3415). */
7
7
  export declare const RESOLVE_PATH_CAPABILITY = "resolve_path";
8
+ /** `set_permission_preset`: a live session's approval mode can be set and is echoed back (classic and multi-session). */
9
+ export declare const SET_PERMISSION_PRESET_CAPABILITY = "set_permission_preset_v1";
8
10
  /**
9
11
  * The capabilities a multi-session host advertises in `get_protocol_info`, in their wire order. `warm` is added only
10
12
  * when the runtime is in-process, `tool_runtime:<kind>` names where heavy tool work runs, and `negotiated` carries
@@ -13,6 +13,8 @@ export const UNLOAD_SESSION_CAPABILITY = "unload_session";
13
13
  export const SUBSCRIPTION_LIMITS_CAPABILITY = "subscription_limits";
14
14
  /** `resolve_path`: which file a message's path means, the engine's one rule (senpi#3415). */
15
15
  export const RESOLVE_PATH_CAPABILITY = "resolve_path";
16
+ /** `set_permission_preset`: a live session's approval mode can be set and is echoed back (classic and multi-session). */
17
+ export const SET_PERMISSION_PRESET_CAPABILITY = "set_permission_preset_v1";
16
18
  /**
17
19
  * The capabilities a multi-session host advertises in `get_protocol_info`, in their wire order. `warm` is added only
18
20
  * when the runtime is in-process, `tool_runtime:<kind>` names where heavy tool work runs, and `negotiated` carries
@@ -56,6 +58,7 @@ export function multiSessionHostCapabilities(options) {
56
58
  // `get_state`/`get_entries` marked `observe` never attach, and history reads page backward too (senpi#4267).
57
59
  OBSERVE_READS_CAPABILITY,
58
60
  ENTRIES_TAIL_CAPABILITY,
61
+ SET_PERMISSION_PRESET_CAPABILITY,
59
62
  // Host capabilities, not client opt-ins: only a multi-session host owns the
60
63
  // attachment refcount `open_session.retain_on_disconnect` detaches from, the
61
64
  // per-session launch profile `context`/`auto_title` travel on, and the session
@@ -4,6 +4,7 @@ import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
4
4
  import type { SessionContext, SessionKind } from "../../core/extensions/types.ts";
5
5
  import { type SessionEnv } from "./session-caller-env.ts";
6
6
  import { type SessionContextOverlay } from "./session-context-patch.ts";
7
+ import { type ResidentWriter } from "./host-handover-resident-writer.ts";
7
8
  export interface HandoverParkedEntry {
8
9
  readonly sessionPath: string;
9
10
  readonly durableSessionId?: string;
@@ -41,9 +42,19 @@ export interface HandoverParkedEntry {
41
42
  * Absent on entries an older generation wrote, which every reader treats as a predecessor's park.
42
43
  */
43
44
  readonly parkedBy?: string;
45
+ /**
46
+ * Present while the session is open on its host (senpi#6190); a park drops it. A successor reopens a stamped
47
+ * entry only once that writer process is gone, so it never opens a session its live predecessor still holds.
48
+ */
49
+ readonly residentWriter?: ResidentWriter;
44
50
  }
45
51
  export declare function recordHandoverParkedSession(daemonDir: string, entry: HandoverParkedEntry): Promise<void>;
46
52
  export declare function readHandoverParkedSessions(daemonDir: string): Promise<readonly HandoverParkedEntry[]>;
47
53
  export declare function clearHandoverParkedSession(daemonDir: string, sessionPath: string): Promise<void>;
54
+ /**
55
+ * Clears an inherited entry after its reopen, unless the reopen re-recorded it as resident on
56
+ * `holder` (a retained session is recorded by the host it is open on, so the next start reopens it).
57
+ */
58
+ export declare function clearInheritedParkedSession(daemonDir: string, sessionPath: string, holder: string | undefined): Promise<void>;
48
59
  export declare function dropHandoverParkedSessionIfFileGone(daemonDir: string, sessionPath: string): Promise<void>;
49
60
  //# sourceMappingURL=host-handover-parked.d.ts.map
@@ -21,6 +21,7 @@ import { parsePermissionPresetName } from "../../core/extensions/builtin/permiss
21
21
  import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
22
22
  import { sessionEnvShapeError } from "./session-caller-env.js";
23
23
  import { applyContextPatch } from "./session-context-patch.js";
24
+ import { isResidentWriter } from "./host-handover-resident-writer.js";
24
25
  const recordDir = (daemonDir) => join(daemonDir, "handover-parked");
25
26
  const entryFile = (daemonDir, sessionPath) => join(recordDir(daemonDir), `${createHash("sha256").update(sessionPath, "utf8").digest("hex").slice(0, 16)}.json`);
26
27
  let mutationChain = Promise.resolve();
@@ -38,6 +39,7 @@ function isEntry(value) {
38
39
  (record.durableSessionId === undefined || typeof record.durableSessionId === "string") &&
39
40
  (record.cwd === undefined || typeof record.cwd === "string") &&
40
41
  (record.parkedBy === undefined || typeof record.parkedBy === "string") &&
42
+ (record.residentWriter === undefined || isResidentWriter(record.residentWriter)) &&
41
43
  (record.thinkingLevel === undefined || typeof record.thinkingLevel === "string") &&
42
44
  (record.permissionPreset === undefined ||
43
45
  (typeof record.permissionPreset === "string" &&
@@ -100,6 +102,21 @@ export async function readHandoverParkedSessions(daemonDir) {
100
102
  export function clearHandoverParkedSession(daemonDir, sessionPath) {
101
103
  return enqueueMutation(() => rm(entryFile(daemonDir, sessionPath), { force: true }));
102
104
  }
105
+ /**
106
+ * Clears an inherited entry after its reopen, unless the reopen re-recorded it as resident on
107
+ * `holder` (a retained session is recorded by the host it is open on, so the next start reopens it).
108
+ */
109
+ export function clearInheritedParkedSession(daemonDir, sessionPath, holder) {
110
+ return enqueueMutation(async () => {
111
+ const file = entryFile(daemonDir, sessionPath);
112
+ if (holder !== undefined) {
113
+ const recorded = await readFile(file, "utf8").then((text) => JSON.parse(text), () => undefined);
114
+ if (isEntry(recorded) && recorded.parkedBy === holder)
115
+ return;
116
+ }
117
+ await rm(file, { force: true });
118
+ });
119
+ }
103
120
  export function dropHandoverParkedSessionIfFileGone(daemonDir, sessionPath) {
104
121
  if (existsSync(sessionPath))
105
122
  return Promise.resolve();
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { createHostDaemonPaths } from "./host-daemon-paths.js";
3
- import { clearHandoverParkedSession, dropHandoverParkedSessionIfFileGone, readHandoverParkedSessions, } from "./host-handover-parked.js";
3
+ import { clearHandoverParkedSession, clearInheritedParkedSession, dropHandoverParkedSessionIfFileGone, readHandoverParkedSessions, } from "./host-handover-parked.js";
4
+ import { residentWriterProbe } from "./host-handover-resident-writer.js";
4
5
  import { RpcSessionRegistryError } from "./session-registry.js";
5
6
  // The watch budget must outlast a heavily loaded drain: a predecessor whose event loop is blocked
6
7
  // (parallel hosts, a busy turn settling) can take tens of seconds to write its park after this
@@ -37,6 +38,7 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
37
38
  const deadline = Date.now() + HANDOVER_PARKED_REOPEN_BUDGET_MS;
38
39
  const reopenAttempts = new Map();
39
40
  const heldPolls = new Map();
41
+ const writerAlive = residentWriterProbe();
40
42
  let emptyStreak = 0;
41
43
  let sawAnyPark = false;
42
44
  for (;;) {
@@ -46,12 +48,12 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
46
48
  // it by path with the same durable id.
47
49
  if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
48
50
  return;
49
- const parked = await inheritedParks(daemonDir, holder);
51
+ const parked = await inheritedParks(daemonDir, holder, writerAlive);
50
52
  for (const entry of parked) {
51
53
  sawAnyPark = true;
52
54
  await reopenOne(daemonDir, open, entry, reopenAttempts, heldPolls, holder);
53
55
  }
54
- const remaining = (await inheritedParks(daemonDir, holder)).length;
56
+ const remaining = (await inheritedParks(daemonDir, holder, writerAlive)).length;
55
57
  if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
56
58
  return;
57
59
  emptyStreak = remaining === 0 ? emptyStreak + 1 : 0;
@@ -67,9 +69,19 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
67
69
  * parks retained sessions into the same record) is for its successor: reopening it here would put a
68
70
  * second open of the path in flight against the client reopening it (senpi#3485).
69
71
  */
70
- async function inheritedParks(daemonDir, holder) {
72
+ async function inheritedParks(daemonDir, holder, writerAlive) {
71
73
  const parked = await readHandoverParkedSessions(daemonDir);
72
- return parked.filter((entry) => entry.parkedBy === undefined || entry.parkedBy !== holder?.hostInstance);
74
+ const inherited = [];
75
+ for (const entry of parked) {
76
+ if (entry.parkedBy !== undefined && entry.parkedBy === holder?.hostInstance)
77
+ continue;
78
+ // A session still resident in its live writer (a handoff's draining predecessor) is that process's to park,
79
+ // and its unstamped park is what this host reopens; only a writer that is gone leaves it here to restore.
80
+ if (entry.residentWriter !== undefined && (await writerAlive(entry.residentWriter)))
81
+ continue;
82
+ inherited.push(entry);
83
+ }
84
+ return inherited;
73
85
  }
74
86
  async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
75
87
  const taskStatus = entry.contextOverlay && Object.hasOwn(entry.contextOverlay, "task_status")
@@ -93,7 +105,7 @@ async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
93
105
  // reopens it; clearing it here would lose the session when the drain closes it (senpi#3282).
94
106
  if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
95
107
  return;
96
- await clearHandoverParkedSession(daemonDir, entry.sessionPath);
108
+ await clearInheritedParkedSession(daemonDir, entry.sessionPath, holder?.hostInstance);
97
109
  }
98
110
  catch (cause) {
99
111
  // A `session_path_in_use` refusal means the predecessor still holds the path's cross-generation
@@ -0,0 +1,16 @@
1
+ import { type WriterProbes } from "../../core/session-writer-claims.ts";
2
+ export interface ResidentWriter {
3
+ readonly host: string;
4
+ readonly pid: number;
5
+ readonly bootAtMs: number;
6
+ readonly processStartedAtMs: number;
7
+ }
8
+ export declare function currentResidentWriter(probes?: WriterProbes): ResidentWriter;
9
+ export declare function isResidentWriter(value: unknown): value is ResidentWriter;
10
+ /**
11
+ * A liveness probe for one reopen watch, which polls: a writer found gone stays gone, and a live pid's start time
12
+ * (a process spawn on some platforms) is read once. A writer on another host cannot be probed from here, so it is
13
+ * never displaced.
14
+ */
15
+ export declare function residentWriterProbe(probes?: WriterProbes): (writer: ResidentWriter) => Promise<boolean>;
16
+ //# sourceMappingURL=host-handover-resident-writer.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The process that holds a retained session resident (senpi#6190). A host records each retained session it has
3
+ * open, stamped with this writer, so the next host on the endpoint can restore it after a crash; while that
4
+ * writer is still alive (a generation handoff's draining predecessor) the session is not its successor's to
5
+ * reopen - the predecessor parks it, and the unstamped park is what the successor consumes. Liveness follows the
6
+ * session writer claims: same host, same boot, live pid whose start time matches.
7
+ */
8
+ import { BOOT_INSTANT_TOLERANCE_MS, sameBoot } from "../../core/extensions/builtin/terminal/process-identity.js";
9
+ import { realWriterProbes } from "../../core/session-writer-claims.js";
10
+ export function currentResidentWriter(probes = realWriterProbes) {
11
+ const self = probes.self();
12
+ return {
13
+ host: probes.host(),
14
+ pid: self.pid,
15
+ bootAtMs: probes.bootAtMs(),
16
+ processStartedAtMs: self.processStartedAtMs,
17
+ };
18
+ }
19
+ export function isResidentWriter(value) {
20
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21
+ return false;
22
+ const record = value;
23
+ return (typeof record.host === "string" &&
24
+ Number.isInteger(record.pid) &&
25
+ typeof record.bootAtMs === "number" &&
26
+ typeof record.processStartedAtMs === "number");
27
+ }
28
+ /**
29
+ * A liveness probe for one reopen watch, which polls: a writer found gone stays gone, and a live pid's start time
30
+ * (a process spawn on some platforms) is read once. A writer on another host cannot be probed from here, so it is
31
+ * never displaced.
32
+ */
33
+ export function residentWriterProbe(probes = realWriterProbes) {
34
+ const gone = new Set();
35
+ const verified = new Set();
36
+ return async (writer) => {
37
+ const key = `${writer.host}:${writer.pid}:${writer.processStartedAtMs}`;
38
+ if (gone.has(key))
39
+ return false;
40
+ if (writer.host !== probes.host())
41
+ return true;
42
+ if (!sameBoot(probes.bootAtMs(), writer.bootAtMs) || !probes.pidAlive(writer.pid)) {
43
+ gone.add(key);
44
+ return false;
45
+ }
46
+ if (verified.has(key))
47
+ return true;
48
+ const startedAtMs = await probes.readProcessStartMs(writer.pid);
49
+ if (startedAtMs !== undefined && Math.abs(startedAtMs - writer.processStartedAtMs) > BOOT_INSTANT_TOLERANCE_MS) {
50
+ gone.add(key);
51
+ return false;
52
+ }
53
+ verified.add(key);
54
+ return true;
55
+ };
56
+ }
57
+ //# sourceMappingURL=host-handover-resident-writer.js.map
@@ -15,6 +15,7 @@ const NEW_WORK_COMMANDS = new Set([
15
15
  "bash",
16
16
  "compact",
17
17
  "wake",
18
+ "admit_external_message",
18
19
  "edit_assistant_message",
19
20
  "edit_user_message",
20
21
  ]);
@@ -30,6 +30,11 @@ export declare class HostWarmer {
30
30
  private readonly load;
31
31
  constructor(load: (profile: HostWarmProfile) => Promise<void>);
32
32
  run(profile: HostWarmProfile): Promise<HostWarmState>;
33
+ /**
34
+ * Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
35
+ * that starts later, or one of another cwd, is not part of it.
36
+ */
37
+ inFlight(cwd: string): Promise<void> | undefined;
33
38
  private loadOnce;
34
39
  }
35
40
  /**
@@ -40,7 +45,17 @@ export declare class HostWarmer {
40
45
  export declare function createRegistryWarm(prepare: NonNullable<PreparableRuntimeFactory["prepare"]>, host: {
41
46
  agentDir: string;
42
47
  mcpRegistry?: HostMcpRegistry;
43
- }): HostWarm;
48
+ }): {
49
+ readonly warm: HostWarm;
50
+ readonly inFlight: (cwd: string) => Promise<void> | undefined;
51
+ };
52
+ /** How an open's wait for the warms of its cwd ended. */
53
+ export type WarmWaitOutcome = "settled" | "warm_wait_deadline";
54
+ /**
55
+ * Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
56
+ * timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
57
+ */
58
+ export declare function waitForWarms(warming: Promise<void>, boundMs: number, cwd: string): Promise<WarmWaitOutcome>;
44
59
  type WarmCommand = Extract<RpcCommand, {
45
60
  type: "warm";
46
61
  }>;
@@ -34,14 +34,24 @@ export class HostWarmer {
34
34
  const key = profileKey(profile);
35
35
  if (this.warm.has(key))
36
36
  return "already_warm";
37
- let pending = this.loading.get(key);
38
- if (pending === undefined) {
39
- pending = this.loadOnce(key, profile);
40
- this.loading.set(key, pending);
37
+ let load = this.loading.get(key);
38
+ if (load === undefined) {
39
+ load = { cwd: profile.cwd, pending: this.loadOnce(key, profile) };
40
+ this.loading.set(key, load);
41
41
  }
42
- await pending;
42
+ await load.pending;
43
43
  return "warmed";
44
44
  }
45
+ /**
46
+ * Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
47
+ * that starts later, or one of another cwd, is not part of it.
48
+ */
49
+ inFlight(cwd) {
50
+ const pending = [...this.loading.values()].filter((load) => load.cwd === cwd).map((load) => load.pending);
51
+ if (pending.length === 0)
52
+ return undefined;
53
+ return Promise.allSettled(pending).then(() => undefined);
54
+ }
45
55
  async loadOnce(key, profile) {
46
56
  try {
47
57
  await this.load(profile);
@@ -85,7 +95,24 @@ export function createRegistryWarm(prepare, host) {
85
95
  scope.close();
86
96
  }
87
97
  });
88
- return (profile) => warmer.run(profile);
98
+ return { warm: (profile) => warmer.run(profile), inFlight: (cwd) => warmer.inFlight(cwd) };
99
+ }
100
+ /**
101
+ * Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
102
+ * timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
103
+ */
104
+ export async function waitForWarms(warming, boundMs, cwd) {
105
+ let deadline;
106
+ const outcome = await Promise.race([
107
+ warming.then(() => "settled"),
108
+ new Promise((resolve) => {
109
+ deadline = setTimeout(() => resolve("warm_wait_deadline"), boundMs);
110
+ }),
111
+ ]);
112
+ clearTimeout(deadline);
113
+ if (outcome === "warm_wait_deadline")
114
+ process.stderr.write(`senpi rpc open: warm_wait_deadline: a warm of ${cwd} still loading after ${boundMs} ms; opening anyway\n`);
115
+ return outcome;
89
116
  }
90
117
  function refusal(command, code) {
91
118
  return { id: command.id, type: "response", command: "warm", success: false, error: code };