@code-yeongyu/senpi 2026.11.6-2 → 2026.11.9

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 +52 -0
  2. package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-3CRPNPTR.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-3TZQXZKH.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-45MVD7BU.js} +1 -1
  7. package/dist/bundle/chunks/chunk-4JIBEZZE.js +106 -0
  8. package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-4UR6UFBH.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-527Q2UYM.js} +9 -8
  11. package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-622VYTRI.js} +1 -1
  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-ECW7VBMB.js → chunk-HXNXISJD.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-K3UHGDUQ.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-D5MRJWUK.js → chunk-OCMGRBJ5.js} +221 -229
  20. package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-SGAFIMAU.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-STGOYIXC.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-WX2HB4DD.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-E4NDWAUD.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-5X5Z5Y3D.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-XUSBLLWI.js +2 -0
  33. package/dist/bundle/chunks/main-3TWVKTJN.js +2 -0
  34. package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-HRGM66PY.js} +20 -17
  35. package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-WSH7U4YE.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-OAPIV46G.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
@@ -0,0 +1,32 @@
1
+ /** The schema of `admit_external_message`, a relayed peer-agent message admitted as a delivery (senpi#6233). */
2
+ import Type from "typebox";
3
+ export declare const DELIVERY_RECORD_SCHEMAS: {
4
+ admit_external_message: Type.TObject<{
5
+ id: Type.TOptional<Type.TString>;
6
+ admissionId: Type.TOptional<Type.TString>;
7
+ type: Type.TLiteral<"admit_external_message">;
8
+ } & {
9
+ readonly sessionId: Type.TOptional<Type.TString>;
10
+ } & {
11
+ readonly deliveryId: Type.TString;
12
+ readonly text: Type.TString;
13
+ readonly sender: Type.TOptional<Type.TUnion<[Type.TObject<{
14
+ kind: Type.TLiteral<"agent">;
15
+ session_id: Type.TString;
16
+ name: Type.TOptional<Type.TString>;
17
+ }>, Type.TObject<{
18
+ kind: Type.TLiteral<"command_line">;
19
+ user: Type.TOptional<Type.TString>;
20
+ }>, Type.TObject<{
21
+ kind: Type.TLiteral<"external">;
22
+ platform: Type.TString;
23
+ author: Type.TOptional<Type.TString>;
24
+ }>]>>;
25
+ readonly displayText: Type.TOptional<Type.TString>;
26
+ readonly deliverAs: Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>;
27
+ readonly expectedTurnId: Type.TOptional<Type.TString>;
28
+ readonly whenIdle: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
29
+ readonly chainCount: Type.TOptional<Type.TInteger>;
30
+ }>;
31
+ };
32
+ //# sourceMappingURL=method-classes-schema-delivery.d.ts.map
@@ -0,0 +1,22 @@
1
+ /** The schema of `admit_external_message`, a relayed peer-agent message admitted as a delivery (senpi#6233). */
2
+ import Type from "typebox";
3
+ import { Id, literals, Opt, OptId, OptStr, Str, session } from "./method-classes-schema-fields.js";
4
+ /** `SessionControlSender`: each kind's fields and no other, so nothing beyond the label rides along. */
5
+ const DeliverySender = Type.Union([
6
+ Type.Object({ kind: Type.Literal("agent"), session_id: Id, name: OptId }, { additionalProperties: false }),
7
+ Type.Object({ kind: Type.Literal("command_line"), user: OptId }, { additionalProperties: false }),
8
+ Type.Object({ kind: Type.Literal("external"), platform: Id, author: OptId }, { additionalProperties: false }),
9
+ ]);
10
+ export const DELIVERY_RECORD_SCHEMAS = {
11
+ admit_external_message: session("admit_external_message", {
12
+ deliveryId: Id,
13
+ text: Str,
14
+ sender: Opt(DeliverySender),
15
+ displayText: OptStr,
16
+ deliverAs: literals("steer", "followUp"),
17
+ expectedTurnId: OptId,
18
+ whenIdle: Opt(literals("start", "queue")),
19
+ chainCount: Opt(Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER })),
20
+ }),
21
+ };
22
+ //# sourceMappingURL=method-classes-schema-delivery.js.map
@@ -1,6 +1,33 @@
1
1
  /** The schemas of the session-routed inbound records (senpi#4324). */
2
2
  import Type from "typebox";
3
3
  export declare const SESSION_RECORD_SCHEMAS: {
4
+ admit_external_message: Type.TObject<{
5
+ id: Type.TOptional<Type.TString>;
6
+ admissionId: Type.TOptional<Type.TString>;
7
+ type: Type.TLiteral<"admit_external_message">;
8
+ } & {
9
+ readonly sessionId: Type.TOptional<Type.TString>;
10
+ } & {
11
+ readonly deliveryId: Type.TString;
12
+ readonly text: Type.TString;
13
+ readonly sender: Type.TOptional<Type.TUnion<[Type.TObject<{
14
+ kind: Type.TLiteral<"agent">;
15
+ session_id: Type.TString;
16
+ name: Type.TOptional<Type.TString>;
17
+ }>, Type.TObject<{
18
+ kind: Type.TLiteral<"command_line">;
19
+ user: Type.TOptional<Type.TString>;
20
+ }>, Type.TObject<{
21
+ kind: Type.TLiteral<"external">;
22
+ platform: Type.TString;
23
+ author: Type.TOptional<Type.TString>;
24
+ }>]>>;
25
+ readonly displayText: Type.TOptional<Type.TString>;
26
+ readonly deliverAs: Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>;
27
+ readonly expectedTurnId: Type.TOptional<Type.TString>;
28
+ readonly whenIdle: Type.TOptional<Type.TUnion<[Type.TLiteral<string>, ...Type.TLiteral<string>[]]>>;
29
+ readonly chainCount: Type.TOptional<Type.TInteger>;
30
+ }>;
4
31
  prompt: Type.TObject<{
5
32
  id: Type.TOptional<Type.TString>;
6
33
  admissionId: Type.TOptional<Type.TString>;
@@ -478,6 +505,15 @@ export declare const SESSION_RECORD_SCHEMAS: {
478
505
  } & {
479
506
  readonly enabled: Type.TBoolean;
480
507
  }>;
508
+ set_permission_preset: Type.TObject<{
509
+ id: Type.TOptional<Type.TString>;
510
+ admissionId: Type.TOptional<Type.TString>;
511
+ type: Type.TLiteral<"set_permission_preset">;
512
+ } & {
513
+ readonly sessionId: Type.TOptional<Type.TString>;
514
+ } & {
515
+ readonly permissionPreset: Type.TString;
516
+ }>;
481
517
  set_auto_retry: Type.TObject<{
482
518
  id: Type.TOptional<Type.TString>;
483
519
  admissionId: Type.TOptional<Type.TString>;
@@ -1,6 +1,7 @@
1
1
  /** The schemas of the session-routed inbound records (senpi#4324). */
2
2
  import Type from "typebox";
3
3
  import { Bool, Id, Images, literals, ModelEntries, Num, Opt, OptBool, OptId, OptNum, OptPath, OptStr, PathStr, RetryFallback, RPC_INBOUND_LIMITS, Secret, Str, Strings, session, ThinkingLevel, } from "./method-classes-schema-fields.js";
4
+ import { DELIVERY_RECORD_SCHEMAS } from "./method-classes-schema-delivery.js";
4
5
  /** A thread call's hop count: 1 for the first call of a chain, one more per hop. */
5
6
  const ChainCount = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
6
7
  const steerOrFollowUp = (type) => session(type, {
@@ -131,6 +132,7 @@ const STATE_AND_MODEL_SCHEMAS = {
131
132
  set_follow_up_mode: session("set_follow_up_mode", { mode: literals("all", "one-at-a-time") }),
132
133
  compact: session("compact", { customInstructions: OptStr }),
133
134
  set_auto_compaction: session("set_auto_compaction", { enabled: Bool }),
135
+ set_permission_preset: session("set_permission_preset", { permissionPreset: Id }),
134
136
  set_auto_retry: session("set_auto_retry", { enabled: Bool }),
135
137
  abort_retry: session("abort_retry", {}),
136
138
  set_retry_fallback: session("set_retry_fallback", { retryFallback: RetryFallback }),
@@ -219,6 +221,7 @@ const SURFACE_AND_ACCOUNT_SCHEMAS = {
219
221
  };
220
222
  export const SESSION_RECORD_SCHEMAS = {
221
223
  ...PROMPTING_SCHEMAS,
224
+ ...DELIVERY_RECORD_SCHEMAS,
222
225
  ...STATE_AND_MODEL_SCHEMAS,
223
226
  ...BASH_AND_TREE_SCHEMAS,
224
227
  ...SURFACE_AND_ACCOUNT_SCHEMAS,
@@ -29,6 +29,33 @@ export type RpcInboundSchemaDisagreements<Schemas extends RpcInboundSchemaTable>
29
29
  [K in RpcMethodType]: Agrees<RpcInboundRecordOf<K>, Static<Schemas[K]>> extends true ? never : K;
30
30
  }[RpcMethodType];
31
31
  export declare const RPC_INBOUND_SCHEMAS: {
32
+ readonly admit_external_message: import("typebox").TObject<{
33
+ id: import("typebox").TOptional<import("typebox").TString>;
34
+ admissionId: import("typebox").TOptional<import("typebox").TString>;
35
+ type: import("typebox").TLiteral<"admit_external_message">;
36
+ } & {
37
+ readonly sessionId: import("typebox").TOptional<import("typebox").TString>;
38
+ } & {
39
+ readonly deliveryId: import("typebox").TString;
40
+ readonly text: import("typebox").TString;
41
+ readonly sender: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TObject<{
42
+ kind: import("typebox").TLiteral<"agent">;
43
+ session_id: import("typebox").TString;
44
+ name: import("typebox").TOptional<import("typebox").TString>;
45
+ }>, import("typebox").TObject<{
46
+ kind: import("typebox").TLiteral<"command_line">;
47
+ user: import("typebox").TOptional<import("typebox").TString>;
48
+ }>, import("typebox").TObject<{
49
+ kind: import("typebox").TLiteral<"external">;
50
+ platform: import("typebox").TString;
51
+ author: import("typebox").TOptional<import("typebox").TString>;
52
+ }>]>>;
53
+ readonly displayText: import("typebox").TOptional<import("typebox").TString>;
54
+ readonly deliverAs: import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>;
55
+ readonly expectedTurnId: import("typebox").TOptional<import("typebox").TString>;
56
+ readonly whenIdle: import("typebox").TOptional<import("typebox").TUnion<[import("typebox").TLiteral<string>, ...import("typebox").TLiteral<string>[]]>>;
57
+ readonly chainCount: import("typebox").TOptional<import("typebox").TInteger>;
58
+ }>;
32
59
  readonly get_protocol_info: import("typebox").TObject<{
33
60
  id: import("typebox").TOptional<import("typebox").TString>;
34
61
  admissionId: import("typebox").TOptional<import("typebox").TString>;
@@ -852,6 +879,15 @@ export declare const RPC_INBOUND_SCHEMAS: {
852
879
  } & {
853
880
  readonly enabled: import("typebox").TBoolean;
854
881
  }>;
882
+ readonly set_permission_preset: import("typebox").TObject<{
883
+ id: import("typebox").TOptional<import("typebox").TString>;
884
+ admissionId: import("typebox").TOptional<import("typebox").TString>;
885
+ type: import("typebox").TLiteral<"set_permission_preset">;
886
+ } & {
887
+ readonly sessionId: import("typebox").TOptional<import("typebox").TString>;
888
+ } & {
889
+ readonly permissionPreset: import("typebox").TString;
890
+ }>;
855
891
  readonly set_auto_retry: import("typebox").TObject<{
856
892
  id: import("typebox").TOptional<import("typebox").TString>;
857
893
  admissionId: import("typebox").TOptional<import("typebox").TString>;
@@ -140,10 +140,12 @@ export declare const RPC_METHOD_CLASS: {
140
140
  readonly version: 1;
141
141
  };
142
142
  readonly set_auto_compaction: "drive";
143
+ readonly set_permission_preset: "device_admin";
143
144
  readonly set_retry_fallback: "drive";
144
145
  readonly pause_for_run: "drive";
145
146
  readonly resume_for_run: "drive";
146
147
  readonly wake: "drive";
148
+ readonly admit_external_message: "drive";
147
149
  readonly release_tool_barrier: "drive";
148
150
  readonly request_self_settle: "drive";
149
151
  readonly "eval_cells.stop": "drive";
@@ -187,10 +187,14 @@ export const RPC_METHOD_CLASS = {
187
187
  set_model: { classify: setModelClass, version: 1 },
188
188
  // a session override; set_steering_mode, set_follow_up_mode and set_auto_retry write global settings (device_admin)
189
189
  set_auto_compaction: "drive",
190
+ // like a preset-naming open_session: it changes what the agent may do without asking
191
+ set_permission_preset: "device_admin",
190
192
  set_retry_fallback: "drive",
191
193
  pause_for_run: "drive",
192
194
  resume_for_run: "drive",
193
195
  wake: "drive",
196
+ // the engine writes its own fixed delivery entry: no role=user, input event, command or template expansion (senpi#6233)
197
+ admit_external_message: "drive",
194
198
  release_tool_barrier: "drive",
195
199
  request_self_settle: "drive",
196
200
  "eval_cells.stop": "drive",
@@ -13,6 +13,7 @@ import { answerDelegation, relayExecRefusal } from "./delegated-answer.js";
13
13
  import { parseClientCapabilities } from "./custom-capability.js";
14
14
  import { ClientOccupancy } from "./host-client-occupancy.js";
15
15
  import { HostCoreGate } from "./host-core-gate.js";
16
+ import { bindRegisteredEndpoint } from "./host-bare-endpoint-registration.js";
16
17
  import { createHostDaemonPaths } from "./host-daemon-paths.js";
17
18
  import { ENDPOINT_OWNER_FENCE_CAPABILITY, GENERATION_HANDOFF_CAPABILITY } from "./host-decision.js";
18
19
  import { HandoffAnnouncement } from "./host-handoff-announcement.js";
@@ -549,7 +550,11 @@ async function runSocketHost(options, socketPath) {
549
550
  });
550
551
  // Everything the internal listener needs is initialized: the runtime milestone of a coordinated start (senpi#4602).
551
552
  reportStartupMilestone("host_runtime");
552
- boundIdentity = await listen(server, socketPath, secret);
553
+ const bind = () => listen(server, socketPath, secret);
554
+ // A host binding its own public socket keeps its registry record; a supervised one's ensure keeps it.
555
+ boundIdentity = await (hostWatchdogBinding() === undefined
556
+ ? bindRegisteredEndpoint({ agentDir: options.agentDir, socket: socketPath, log: hostLog }, bind)
557
+ : bind());
553
558
  // A key record that cannot be published safely ends this host: held connections are refused, never served keyless.
554
559
  await transport.publish(() => shutdown(1));
555
560
  if (supervisorPublicOwnerFile) {
@@ -0,0 +1,6 @@
1
+ import type { AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
2
+ import { type RpcCommand, type RpcResponse } from "./rpc-types.ts";
3
+ export declare function runSetPermissionPreset(runtime: Pick<AgentSessionRuntime, "session" | "setPermissionPreset">, command: Extract<RpcCommand, {
4
+ type: "set_permission_preset";
5
+ }>): RpcResponse;
6
+ //# sourceMappingURL=permission-preset-command.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { parsePermissionPresetName } from "../../core/extensions/builtin/permission-system/cli.js";
2
+ import { permissionModeOf, permissionModeReply } from "../../core/permission-mode.js";
3
+ import { RPC_ERROR_INVALID_LAUNCH_PROFILE } from "./rpc-types.js";
4
+ // The live setter a classic `--mode rpc` session has no other way to receive (it refuses `open_session`), and the
5
+ // path-free one for a session on another device. An unknown preset is refused before anything changes, so the
6
+ // session keeps the mode it had; a valid one is applied through the runtime the way an attach naming it applies it.
7
+ export function runSetPermissionPreset(runtime, command) {
8
+ if (parsePermissionPresetName(command.permissionPreset) === undefined) {
9
+ return {
10
+ id: command.id,
11
+ type: "response",
12
+ command: "set_permission_preset",
13
+ success: false,
14
+ error: `${RPC_ERROR_INVALID_LAUNCH_PROFILE}: unknown permissionPreset "${command.permissionPreset}"`,
15
+ errorCode: RPC_ERROR_INVALID_LAUNCH_PROFILE,
16
+ };
17
+ }
18
+ runtime.setPermissionPreset(command.permissionPreset);
19
+ return {
20
+ id: command.id,
21
+ type: "response",
22
+ command: "set_permission_preset",
23
+ success: true,
24
+ data: permissionModeReply(permissionModeOf(runtime.session)),
25
+ };
26
+ }
27
+ //# sourceMappingURL=permission-preset-command.js.map
@@ -1,6 +1,7 @@
1
1
  import { isBrowserEngine } from "../../core/browser-engine.js";
2
2
  import { clientMessageIdentitySchema } from "../../core/client-message-identity.js";
3
3
  import { MAX_TOOL_BARRIER_ID_LENGTH, toolBarrierSpecError } from "../../core/tool-barrier.js";
4
+ import { admitExternalMessageFieldError } from "./admit-external-message-wire.js";
4
5
  import { validatePauseCommand } from "./pause-command-validation.js";
5
6
  export const MAX_RPC_MESSAGE_CHARACTERS = 1_000_000;
6
7
  /**
@@ -141,9 +142,9 @@ export function rpcCommandPayloadError(command) {
141
142
  !(typeof value.effectId === "string" && value.effectId.length > 0 && value.effectId.length <= 256)) {
142
143
  return "effectId must be a non-empty string of at most 256 characters.";
143
144
  }
144
- const pauseFieldError = validatePauseCommand(value);
145
- if (pauseFieldError !== undefined)
146
- return pauseFieldError;
145
+ const fieldError = validatePauseCommand(value) ?? admitExternalMessageFieldError(value, MAX_RPC_MESSAGE_CHARACTERS);
146
+ if (fieldError !== undefined)
147
+ return fieldError;
147
148
  if (value.type === "get_pending_inputs" && value.knownClientMessageIds !== undefined) {
148
149
  const ids = value.knownClientMessageIds;
149
150
  if (!Array.isArray(ids) ||
@@ -6,6 +6,7 @@ import type { ClientMessageIdentity } from "../../core/client-message-identity.t
6
6
  import type { CompactionResult } from "../../core/compaction/index.ts";
7
7
  import type { ServiceTier } from "../../core/extensions/builtin/service-tier.ts";
8
8
  import type { SessionControlAdmission } from "../../core/extensions/types.ts";
9
+ import type { PermissionModeReply } from "../../core/permission-mode.ts";
9
10
  import type { ApiKeyVerification } from "../../core/verify-api-key.ts";
10
11
  import type { ClientMessageAdmission } from "./client-admission-record.ts";
11
12
  import type { RpcProjectSettings, RpcProjectTrust } from "./project-workspace-commands.ts";
@@ -234,6 +235,12 @@ export type RpcMutationResponse = {
234
235
  type: "response";
235
236
  command: "set_auto_compaction";
236
237
  success: true;
238
+ } | {
239
+ id?: string;
240
+ type: "response";
241
+ command: "set_permission_preset";
242
+ success: true;
243
+ data: PermissionModeReply;
237
244
  } | {
238
245
  id?: string;
239
246
  type: "response";
@@ -1,3 +1,4 @@
1
+ import type { RpcAdmitExternalMessageResponse } from "./admit-external-message-wire.ts";
1
2
  import type { RpcAttachmentResponse } from "./attachment-types.ts";
2
3
  import type { UnknownAdmissionResponse } from "./compact-admission-error.ts";
3
4
  import type { RpcHostCommandResponse } from "./rpc-host-response-types.ts";
@@ -12,5 +13,5 @@ export type RpcResponse = {
12
13
  data?: {
13
14
  capabilities: readonly string[];
14
15
  };
15
- } | UnknownAdmissionResponse | RpcAttachmentResponse | RpcHostCommandResponse | RpcReadResponse | RpcMutationResponse | RpcPauseResponse;
16
+ } | UnknownAdmissionResponse | RpcAttachmentResponse | RpcHostCommandResponse | RpcReadResponse | RpcMutationResponse | RpcAdmitExternalMessageResponse | RpcPauseResponse;
16
17
  //# sourceMappingURL=rpc-response-types.d.ts.map
@@ -5,6 +5,7 @@ import type { BashResult } from "../../core/bash-executor.ts";
5
5
  import type { ClientMessageIdentity } from "../../core/client-message-identity.ts";
6
6
  import type { SessionEntry } from "../../core/session-manager.ts";
7
7
  import type { ToolBarrierSpec } from "../../core/tool-barrier.ts";
8
+ import type { RpcAdmitExternalMessageCommand } from "./admit-external-message-wire.ts";
8
9
  import type { RpcRecoveryCommand } from "./rpc-recovery-command-types.ts";
9
10
  import type { RpcRemoteOrigin } from "./remote-origin.ts";
10
11
  import type { RpcSessionModelEntry } from "./rpc-session-state-types.ts";
@@ -196,7 +197,7 @@ export type RpcSessionCommand = {
196
197
  id?: string;
197
198
  type: "wake";
198
199
  delivery_ids?: string[];
199
- } | {
200
+ } | RpcAdmitExternalMessageCommand | {
200
201
  id?: string;
201
202
  type: "get_state";
202
203
  } | {
@@ -293,6 +294,10 @@ export type RpcSessionCommand = {
293
294
  id?: string;
294
295
  type: "set_auto_compaction";
295
296
  enabled: boolean;
297
+ } | {
298
+ id?: string;
299
+ type: "set_permission_preset";
300
+ permissionPreset: string;
296
301
  } | {
297
302
  id?: string;
298
303
  type: "set_auto_retry";
@@ -109,8 +109,11 @@ export type RpcSessionParkedEvent = {
109
109
  sessionId: string;
110
110
  /** Session file to reopen this session by. */
111
111
  sessionPath: string;
112
- /** `idle`: the idle sweep parked it; `unloaded`: `unload_session` did. Absent from older hosts. */
113
- reason?: "idle" | "unloaded";
112
+ /**
113
+ * `idle`: the idle sweep parked it; `unloaded`: `unload_session` did; `host_shutdown`: the host stopped with the session
114
+ * retained, and the next host on the endpoint reopens it. Absent from older hosts.
115
+ */
116
+ reason?: "idle" | "unloaded" | "host_shutdown";
114
117
  };
115
118
  /**
116
119
  * Sent only to the connections a headless engine's quiet takeover let go (senpi#5732): this connection no longer holds
@@ -3,6 +3,7 @@ import type { Model, ProviderDiagnostic, ThinkingSelection } from "@earendil-wor
3
3
  import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
4
4
  import type { QueuedInput } from "../../core/agent-session.ts";
5
5
  import type { HoldKind } from "../../core/engine-turn-hold.ts";
6
+ import type { PermissionMode } from "../../core/permission-mode.ts";
6
7
  import type { ProviderOrigin } from "../../core/provider-origin.ts";
7
8
  import type { ServiceTier } from "../../core/extensions/builtin/service-tier.ts";
8
9
  import type { ContextUsage } from "../../core/extensions/types.ts";
@@ -105,7 +106,7 @@ export interface RpcSessionModelEntry {
105
106
  thinkingSelection?: ThinkingSelection;
106
107
  serviceTier?: ServiceTier;
107
108
  }
108
- export interface RpcSessionState {
109
+ export interface RpcSessionState extends Partial<PermissionMode> {
109
110
  /** The running turn's id; absent when no turn is running. A steer or follow-up names it as `expectedTurnId` (senpi#5776). */
110
111
  currentTurnId?: string;
111
112
  /** Latest 64 UI-answer receipts in this session generation, oldest first. */
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { existsSync } from "node:fs";
6
6
  import { sanitizeProviderDiagnostic } from "@earendil-works/pi-ai";
7
+ import { permissionModeOf } from "../../core/permission-mode.js";
7
8
  import { ProjectTrustStore } from "../../core/trust-manager.js";
8
9
  import { clientFacingIdentity } from "./client-admission-record.js";
9
10
  import { sessionQuestionBridges } from "./connection-question-bridge.js";
@@ -38,6 +39,7 @@ export function buildRpcSessionState(session, lastAbortSource) {
38
39
  lifecycle: observeLifecycle(session.lifecycle),
39
40
  pendingQuestions: sessionQuestionBridges.get(session)?.pendingQuestions(),
40
41
  pendingDialogs: sessionDialogRequests.get(session)?.snapshot() ?? [],
42
+ ...permissionModeOf(session),
41
43
  model: session.model,
42
44
  pendingModelSwitch: session.pendingModelSwitch
43
45
  ? {
@@ -1,6 +1,6 @@
1
1
  import { takeOver } from "./attach-takeover.js";
2
2
  import { compactOpen } from "./compact-open-admission.js";
3
- import { clearHandoverParkedSession } from "./host-handover-parked.js";
3
+ import { clearRetainedRecord } from "./session-router-residency.js";
4
4
  import { hostInstanceId } from "./protocol-identity.js";
5
5
  import { canonicalSessionPath } from "./session-path-key.js";
6
6
  import { recordAttachment, releaseDisconnectAttachment, releaseOwnerAttachment } from "./session-router-attachments.js";
@@ -116,8 +116,8 @@ export async function reclaimSession(host, command) {
116
116
  if (host.sessionsByConnection.get(owner)?.size === 0)
117
117
  host.sessionsByConnection.delete(owner);
118
118
  connectionLeft(host, row.sessionId, owner);
119
- if (host.handoverParkedDir !== undefined && entry.sessionPath !== undefined)
120
- await clearHandoverParkedSession(host.handoverParkedDir, entry.sessionPath).catch(() => { });
119
+ if (host.handoverParkedDir !== undefined)
120
+ await clearRetainedRecord(host, entry);
121
121
  if (claim.finalizer) {
122
122
  const binding = host.bindings.get(row.sessionId);
123
123
  binding?.cancelPendingExtensionUiRequests?.();
@@ -23,6 +23,7 @@ export class SessionEventWriterConnections extends SessionEventWriterLanes {
23
23
  record.type === "host_request" ||
24
24
  record.type === "relocation_due" ||
25
25
  (record.type === "extension_ui_request" &&
26
+ record.audience !== "session" &&
26
27
  ["select", "confirm", "input", "editor"].includes(String(record.method))));
27
28
  }
28
29
  isSessionSealed(sessionId) {
@@ -51,7 +51,7 @@ export declare class SessionEventWriter extends SessionEventWriterDestinations {
51
51
  * session's reaches every connection. No client asked for this teardown, so there is
52
52
  * no close response to answer.
53
53
  */
54
- parkSession(sessionId: string, sessionPath: string, reason: "idle" | "unloaded"): void;
54
+ parkSession(sessionId: string, sessionPath: string, reason: "idle" | "unloaded" | "host_shutdown"): void;
55
55
  /**
56
56
  * Seal a session `release_session` handed to a runtime outside this host and publish
57
57
  * `session_closed { reason: "released", sessionPath }`. Unlike a park the file must NOT be reopened
@@ -1,12 +1,3 @@
1
- /**
2
- * Attach-on-open: a live session outlives individual client attachments, so a resume (or a second
3
- * surface) for an already-hosted path joins the existing runtime instead of failing. An entry
4
- * still opening holds the reservation, and an explicit open for that path waits on the entry's
5
- * openCompletion (bounded on the waiter only, default 45s) and attaches once it opens; on that
6
- * wait expiring, only the one waiting reopen fails with a retryable session_path_in_use
7
- * {reason: "open_in_progress", retry_after_ms} naming an attached current owner, and the in-flight
8
- * open keeps running untouched.
9
- */
10
1
  import type { SessionPathReservations } from "./host-reservations.ts";
11
2
  import { type OpenRpcSession, type RpcSessionEntry, type RpcSessionLaunchProfile, type RpcSessionOpenOptions } from "./session-registry-types.ts";
12
3
  export declare function attachToOpenSession(entries: ReadonlyMap<string, RpcSessionEntry>, sessionPath: string, profile: RpcSessionLaunchProfile, options: RpcSessionOpenOptions | undefined, pathReservations: SessionPathReservations | undefined, now: number): Promise<OpenRpcSession>;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Attach-on-open: a live session outlives individual client attachments, so a resume (or a second
3
+ * surface) for an already-hosted path joins the existing runtime instead of failing. An entry
4
+ * still opening holds the reservation, and an explicit open for that path waits on the entry's
5
+ * openCompletion (bounded on the waiter only, default 45s) and attaches once it opens; on that
6
+ * wait expiring, only the one waiting reopen fails with a retryable session_path_in_use
7
+ * {reason: "open_in_progress", retry_after_ms} naming an attached current owner, and the in-flight
8
+ * open keeps running untouched.
9
+ */
10
+ import { clientNamedPreset, withClientPreset } from "../../core/permission-mode.js";
1
11
  import { hostInstanceId } from "./protocol-identity.js";
2
12
  import { frozenProfile, RpcSessionRegistryError, } from "./session-registry-types.js";
3
13
  /** How long ONE explicit reopen waits on an in-flight open before giving up (the open keeps going). */
@@ -104,9 +114,12 @@ export async function attachToOpenSession(entries, sessionPath, profile, options
104
114
  }
105
115
  // The preset is applied even when the record already names it: the record is not the live session,
106
116
  // so a resent attach also repairs a session that drifted from it (senpi#2842).
107
- if (profile.permissionPreset !== undefined) {
108
- entry.profile = frozenProfile({ ...entry.profile, permissionPreset: profile.permissionPreset });
109
- entry.runtime?.setPermissionPreset(profile.permissionPreset);
117
+ // Only a preset the client named moves the session: an embedder's host default folded into a preset-less attach is
118
+ // not a choice, and applying it would turn the host default into a client-named mode (remote approval-mode rule).
119
+ const namedPreset = clientNamedPreset(profile);
120
+ if (namedPreset !== undefined) {
121
+ entry.profile = frozenProfile(withClientPreset(entry.profile, namedPreset));
122
+ entry.runtime?.setPermissionPreset(namedPreset);
110
123
  }
111
124
  entry.lastCommandAt = now;
112
125
  if (wasParked) {
@@ -57,6 +57,8 @@ export interface RpcSessionEntry {
57
57
  * Requested per `open_session`; an attach may turn it on, never off.
58
58
  */
59
59
  retainOnDisconnect?: boolean;
60
+ /** The session path this host's handover-parked record entry is keyed by; it follows a transcript switch. */
61
+ recordedPath?: string;
60
62
  /** Timestamp of the last routed command / observed activity; drives idle eviction. */
61
63
  lastCommandAt: number;
62
64
  /**
@@ -14,6 +14,7 @@ export declare class RpcSessionRegistry {
14
14
  readonly closeGraceMs: number;
15
15
  /** Present only when the runtime factory can build a session's services alone (`host-warm.ts`). */
16
16
  readonly warm?: HostWarm;
17
+ private readonly warmsInFlight?;
17
18
  /** The agent directory this host keeps its sessions (and session-move folders) under. */
18
19
  get agentDir(): string;
19
20
  constructor(options: RpcSessionRegistryOptions);
@@ -22,6 +23,7 @@ export declare class RpcSessionRegistry {
22
23
  /** Daemon-family identities come from the endpoint's claims and generation registry. */
23
24
  holderPids(observedStarts?: ReadonlyMap<number, number | undefined>): Promise<readonly number[]>;
24
25
  openSession(requested: RpcSessionLaunchProfile, options?: RpcSessionOpenOptions): Promise<OpenRpcSession>;
26
+ private openNow;
25
27
  /**
26
28
  * Read-only lookup with no state transitions or attachment accounting.
27
29
  * Exists so lifecycle decisions (e.g. deferring a dropped connection's
@@ -1,6 +1,6 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { assertValidSessionId } from "../../core/session-manager.js";
3
- import { createRegistryWarm } from "./host-warm.js";
3
+ import { createRegistryWarm, waitForWarms } from "./host-warm.js";
4
4
  import { refreshesSessionActivity } from "./session-command-activity.js";
5
5
  import { publishedSessionContext } from "./session-context-patch.js";
6
6
  import { attachLiveSession, markPathAttached } from "./session-live-attachment.js";
@@ -20,11 +20,14 @@ export class RpcSessionRegistry {
20
20
  this.reservations = new Set();
21
21
  this.pendingClaims = new Map();
22
22
  this.nextHandle = 0;
23
- if (options.createRuntime.prepare)
24
- this.warm = createRegistryWarm(options.createRuntime.prepare, {
23
+ if (options.createRuntime.prepare) {
24
+ const warming = createRegistryWarm(options.createRuntime.prepare, {
25
25
  agentDir: options.agentDir,
26
26
  ...(options.mcpRegistry !== undefined ? { mcpRegistry: options.mcpRegistry } : {}),
27
27
  });
28
+ this.warm = warming.warm;
29
+ this.warmsInFlight = warming.inFlight;
30
+ }
28
31
  this.options =
29
32
  options.mcpRegistry === undefined
30
33
  ? options
@@ -70,6 +73,16 @@ export class RpcSessionRegistry {
70
73
  return this.options.pathReservations?.holderPids?.(observedStarts) ?? Promise.resolve([]);
71
74
  }
72
75
  openSession(requested, options) {
76
+ // A warm in flight (the boot load of senpi#6147 included) loads on this loop and yields it between modules
77
+ // (senpi#6199): an open builds its runtime after the warms of ITS cwd that are loading when it arrives, so
78
+ // its factories never run beside that load. Other cwds and later warms are not waited on, and the wait is
79
+ // bounded by `closeGraceMs` like the closing-reservation wait.
80
+ const warming = this.warmsInFlight?.(requested.cwd);
81
+ if (warming === undefined)
82
+ return this.openNow(requested, options);
83
+ return waitForWarms(warming, this.closeGraceMs, requested.cwd).then(() => this.openNow(requested, options));
84
+ }
85
+ openNow(requested, options) {
73
86
  return openRegistrySession({
74
87
  entries: this.entries,
75
88
  reservations: this.reservations,
@@ -1,8 +1,30 @@
1
- import { clearHandoverParkedSession } from "./host-handover-parked.js";
2
1
  import { RPC_ERROR_UNKNOWN_SESSION } from "./rpc-types.js";
3
2
  import { forgetSessionOwnership, releaseOwnerAttachment } from "./session-router-attachments.js";
4
3
  import { code, error } from "./session-router-errors.js";
5
4
  import { stopSweep } from "./session-router-occupancy.js";
5
+ import { clearRetainedRecord, recordRetainedSession } from "./session-router-residency.js";
6
+ /** How long a stop lets a cut turn settle before its teardown proceeds anyway: the default close grace. */
7
+ const HOST_SHUTDOWN_ABORT_BOUND_MS = 10_000;
8
+ /** Ends the running turn as a host shutdown (senpi#6190), bounded so a run its abort cannot release never holds the stop. */
9
+ async function abortForHostShutdownWithin(sessionId, entry) {
10
+ // Async wrapper: a session that throws synchronously still only fails its own abort, never the whole stop.
11
+ const aborting = (async () => {
12
+ await entry?.runtime?.session.abortForHostShutdown();
13
+ })().catch((cause) => {
14
+ process.stderr.write(`senpi rpc host shutdown abort ${sessionId} failed: ${String(cause)}\n`);
15
+ });
16
+ let timer;
17
+ const bound = new Promise((resolve) => {
18
+ timer = setTimeout(resolve, HOST_SHUTDOWN_ABORT_BOUND_MS);
19
+ timer.unref?.();
20
+ });
21
+ try {
22
+ await Promise.race([aborting, bound]);
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
6
28
  /**
7
29
  * Releases one attachment (or every attachment when draining) and reports
8
30
  * whether this caller became the finalizer. The join token is installed inside
@@ -66,10 +88,10 @@ export async function close(host, command) {
66
88
  releaseOwnerAttachment(host, owner, command.sessionId);
67
89
  if (claim.finalizer) {
68
90
  // An explicit close ends the session for good: drop any handover-parked record entry, or
69
- // the session would come back on the next host start (MEDIUM-1, senpi#3282 review).
70
- const entry = host.registry.peek(command.sessionId);
71
- if (host.handoverParkedDir !== undefined && entry?.sessionPath !== undefined)
72
- await clearHandoverParkedSession(host.handoverParkedDir, entry.sessionPath).catch(() => { });
91
+ // the session would come back on the next host start (MEDIUM-1, senpi#3282 review). Awaited only when a record
92
+ // exists, so a host without one starts the teardown in the same tick as the claim.
93
+ if (host.handoverParkedDir !== undefined)
94
+ await clearRetainedRecord(host, host.registry.peek(command.sessionId));
73
95
  await finalizeClose(host, command.sessionId, host.bindings.get(command.sessionId), () => reply.complete(true, "client_close"));
74
96
  }
75
97
  else {
@@ -189,11 +211,26 @@ export async function dispose(host) {
189
211
  if (host.admissions)
190
212
  await host.admissions.dispose();
191
213
  await Promise.all([...host.bindings.entries()].map(async ([sessionId, binding]) => {
214
+ const entry = host.registry.peek(sessionId);
215
+ // A retained session outlives this process: it parks under the transcript it is on now (its record stays
216
+ // for the next host on the endpoint, which reopens it) instead of closing, so attached clients do not read
217
+ // it as ended.
218
+ const parkedPath = host.handoverParkedDir !== undefined && entry?.retainOnDisconnect === true
219
+ ? (entry.runtime?.session.sessionFile ?? entry.sessionPath)
220
+ : undefined;
221
+ // Claimed before any await, like close(): the stop fences the session in the tick it starts.
192
222
  const claim = tryClaimClose(host, sessionId, { drainAttachments: true });
193
223
  if (!claim)
194
224
  return;
195
225
  if (claim.finalizer) {
196
- await finalizeClose(host, sessionId, binding, () => host.writer.closeSession(sessionId, { type: "response", command: "close_session", success: true, data: {} }, "host_shutdown"));
226
+ await abortForHostShutdownWithin(sessionId, entry);
227
+ if (entry !== undefined && parkedPath !== undefined)
228
+ await recordRetainedSession(host, entry, parkedPath, false).catch((cause) => {
229
+ process.stderr.write(`senpi rpc host shutdown park record ${sessionId} failed: ${String(cause)}\n`);
230
+ });
231
+ await finalizeClose(host, sessionId, binding, () => parkedPath !== undefined
232
+ ? host.writer.parkSession(sessionId, parkedPath, "host_shutdown")
233
+ : host.writer.closeSession(sessionId, { type: "response", command: "close_session", success: true, data: {} }, "host_shutdown"));
197
234
  }
198
235
  else
199
236
  await host.finalizations.get(sessionId)?.promise;