@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
package/docs/rpc.md CHANGED
@@ -1286,7 +1286,7 @@ from them that the host itself went away (shutdown) or handed its endpoint to a
1286
1286
  | `event_queue_failed` | `true` | The socket's event queue failed; follows its `overflow` record when it has one | `overflow`, `stall` or `transport_error` |
1287
1287
  | `event_lease_refused` | `true` | An event cannot be leased; follows its `event_lease_refused` record | `lease_limit` or `storage_unavailable` |
1288
1288
  | `compact_endpoint_unsupported` | `false` | A compact mirror of a session served only on the legacy wire; follows its `compact_refused` record | none |
1289
- | `host_shutdown` | `true` | The host is shutting down; follows its sessions' `session_closed` records (`reason: "host_shutdown"`) | none |
1289
+ | `host_shutdown` | `true` | The host is shutting down; follows its sessions' `session_closed` records (`reason: "host_shutdown"`), or `session_parked { reason: "host_shutdown" }` for a retained session on a socket host | none |
1290
1290
  | `host_handoff` | `true` | The host drains for a newer generation; follows its sessions' handoff park records | none |
1291
1291
 
1292
1292
  #### interactive_user
@@ -2514,7 +2514,8 @@ releases the caller's own attachment and closes the session only when no other c
2514
2514
 
2515
2515
  Retention limits (what `open`'s `retain_on_disconnect: true` does NOT survive): an explicit `close`, the host's own
2516
2516
  idle-eviction window (`SENPI_RPC_SESSION_IDLE_EVICTION_MS`, default 30 minutes, after which the session is parked -
2517
- any `senpi host session` verb given its path or durable id resumes it), or the host process exiting. A session that has not
2517
+ any `senpi host session` verb given its path or durable id resumes it). The host process exiting does not end it: the next
2518
+ host on the same endpoint reopens it (see "Retained sessions across a host restart"). A session that has not
2518
2519
  received a message yet may have no JSONL file on disk even though `open` already reported a `sessionPath`.
2519
2520
 
2520
2521
  A minimal orchestration run:
@@ -2801,8 +2802,8 @@ viewer opened to the next one (senpi#5808). So every `open_session` on a delegat
2801
2802
  stays `open` with `attachments: 0` and attachable from any connection, exactly like a retained session (see "Retained
2802
2803
  sessions"). The host decides this when it reads the line, so a connection that ends while its open is still running
2803
2804
  leaves that session retained too. Like any retained session it is still closed by an explicit `close_session`, parked
2804
- by the idle-eviction window, and recorded at park for the next host generation, which reopens it on startup, so a
2805
- parked session survives into the next generation. `attach_session` never turns retention on: attaching over a delegated
2805
+ by the idle-eviction window, and recorded (at open, and again at park) for the next host on the endpoint, which reopens
2806
+ it on startup, so it survives into the next generation and across a host restart. `attach_session` never turns retention on: attaching over a delegated
2806
2807
  connection to a session that is not retained leaves it closing with its last attachment. One consequence to know: a
2807
2808
  delegated `open_session` by `sessionPath` (which needs `device_admin`) that lands on a session already live and opened by
2808
2809
  someone else joins it as an attach that asks for retention, so it turns retention on for that live session and every
@@ -3297,8 +3298,9 @@ What the host does enforce are lifecycle windows, and they only ever return memo
3297
3298
  command's `unknown_session`. A GENERATION HANDOFF also parks attached sessions, using the distinct activity
3298
3299
  predicate below, and emits `session_closed { sessionId, reason: "handoff_parked", sessionPath }`, so a client can tell "the host handed
3299
3300
  over, reopen by path" from "this session ended". An explicit `close_session` is `reason: "client_close"`; the host
3300
- process exiting is `reason: "host_shutdown"` (even for a retained session - the process is going away, so this is not
3301
- a park). `reason` is optional on the wire; a client that does not know a
3301
+ process exiting is `reason: "host_shutdown"`, except for a retained session on a socket host, which PARKS instead
3302
+ (`session_parked { reason: "host_shutdown" }`) because the next host on the endpoint reopens it (see "Retained sessions
3303
+ across a host restart"). `reason` is optional on the wire; a client that does not know a
3302
3304
  value, or receives a record with no `reason` field, treats the record exactly as a reason-less one. "Session-owned
3303
3305
  work" is the complete activity contract, not just a streaming turn: an agent run, a running bash command,
3304
3306
  background terminal jobs and any other published wake source (terminal monitors, loop-guard holds), compaction,
@@ -3334,17 +3336,45 @@ What the host does enforce are lifecycle windows, and they only ever return memo
3334
3336
  in-flight turn to settlement, keeps its path reservation and worker slot, and stays in `list_sessions`. A later
3335
3337
  `open_session` with that `sessionPath` — from any connection — attaches to the same routing handle and returns
3336
3338
  `attached: true`. Retention never outranks an explicit teardown: a `close_session` from an attached connection still
3337
- reaches zero attachments and closes the session, host shutdown closes it, and the idle-eviction window above still
3339
+ reaches zero attachments and closes the session, a socket host's shutdown parks it (see below), and the idle-eviction
3340
+ window above still
3338
3341
  parks it (or the completed-detached-worker rule above parks it sooner) — announced as
3339
3342
  `session_parked { sessionId, sessionPath }`, after which the file reopens by path like any
3340
3343
  evicted session, and the parked session counts as gone for the empty-host exit below. The park is also recorded
3341
3344
  for the NEXT generation, which reopens it on startup; the host that parked it never reopens it on its own, so a
3342
- client reopening the session right after `session_parked` is the only open of that path (senpi#3485). It is also bounded by the empty-host exit
3343
- and the supervisor's idle-exit window below: retention survives a client, not the host. Any attach may turn retention
3345
+ client reopening the session right after `session_parked` is the only open of that path (senpi#3485). Retention survives
3346
+ the host process too (next item). Any attach may turn retention
3344
3347
  on for a live session; no attach turns it off for clients that already rely on it. Omitting the flag is byte-identical
3345
3348
  to the previous behavior — the session is closed with its last connection — except on a delegated connection
3346
3349
  (`declare_delegation`), where every `open_session` is retained (see "Delegated connections"; senpi#5808). Probe `retain_on_disconnect` in
3347
3350
  `get_protocol_info` capabilities before relying on it: an older host silently ignores the field.
3351
+ - **Retained sessions across a host restart**: every retained session with a session path is recorded in the
3352
+ endpoint's daemon directory (`handover-parked/`, one entry per session path, the same record a handoff park writes)
3353
+ as soon as its open succeeds; the entry follows the session's `set_session_context` patches and its park, and a
3354
+ transcript switch (`new_session`, `switch_session`, a fork) moves it to the new file, so only the transcript the
3355
+ session is on comes back. An explicit `close_session` or a reclaim removes every key the session was recorded
3356
+ under. While the session is open the entry names the process that holds it; a park drops that, and a host never
3357
+ reopens an entry whose holding process is still alive. So during a generation handoff the successor does not open
3358
+ a session its draining predecessor still has loaded, idle and detached ones included: it reopens the predecessor's
3359
+ park once the drain writes it (or the entry, if the predecessor dies without parking). A host
3360
+ that starts on the same socket and agent dir - after a handoff, a clean stop, a crash, or a kill in the middle of a
3361
+ turn - reopens, in the background right after it binds, every entry an earlier process wrote, replaying the
3362
+ recorded launch profile as `open_session { sessionPath, durableSessionId, retain_on_disconnect: true, ... }`: the same
3363
+ open-by-path a TUI resume runs on its session file. The session is `open` in `list_sessions` again with the same
3364
+ `durableSessionId`, `sessionPath` and transcript, announced by a `session_opened` record, and no client has to
3365
+ re-open it. A clean stop (SIGTERM, idle or empty exit) parks each retained session first: attached clients receive
3366
+ `session_parked { sessionId, sessionPath, reason: "host_shutdown" }`, never `session_closed`, and a turn running at
3367
+ the stop ends with `agent_end { aborted: true, abortSource: "system", abortCause: { kind: "host_shutdown" } }`, never
3368
+ a user abort. A killed host sends nothing. Either way the turn the stopped process was running is not resumed: the
3369
+ transcript holds what was persisted before the stop, as after a TUI crash. The routing handle (`sessionId`, `rpc-N`) is new, since handles are unique per process;
3370
+ a client holding the old one gets `session_not_found` and re-resolves the session from `list_sessions` (or
3371
+ `session_opened`) by `durableSessionId`. A relay that mirrors `list_sessions` (relayd keeps no session store of its
3372
+ own) therefore sees the session again unchanged. Not reopened: an entry whose session file is gone (a session that
3373
+ never received a message has none), an explicitly closed or reclaimed session (its entry is removed), and a session
3374
+ whose context `task_status` is terminal (`completed`, `cancelled`, `error`, `failed`, `lost`, `interrupted`). There is
3375
+ no separate resume-by-durable-id request: `open_session { sessionPath }` already resumes a known transcript, but a
3376
+ remote client learns sessions only from `list_sessions`, so a session the restarted host did not list could not be
3377
+ resumed by one at all.
3348
3378
  - **Empty-host exit**: when the registry holds zero sessions AND no client is connected, continuously for
3349
3379
  `SENPI_RPC_HOST_EMPTY_EXIT_MS` (default 15 minutes), the host exits through its clean shutdown path (flush, socket
3350
3380
  removal), for stdio and `--listen` hosts alike. A connected client counts as occupancy even with no session open,
@@ -4740,11 +4770,26 @@ Response:
4740
4770
  "autoCompactionEnabled": true,
4741
4771
  "messageCount": 5,
4742
4772
  "pendingMessageCount": 0,
4743
- "pendingDialogs": []
4773
+ "pendingDialogs": [],
4774
+ "permissionPreset": "ask",
4775
+ "permissionPresetSource": "client",
4776
+ "permissionPresetStatus": "applied",
4777
+ "effectivePermissionPreset": "ask"
4744
4778
  }
4745
4779
  }
4746
4780
  ```
4747
4781
 
4782
+ `permissionPreset`, `permissionPresetSource`, `permissionPresetStatus` and `effectivePermissionPreset` report the approval mode the session enforces. `permissionPresetSource` is `client` when a client named a valid preset for this session (`open_session.permissionPreset`, an attach naming one, or `set_permission_preset`) and `host_default` when the host's own default applies (its `--permission-preset` flag, then project and global settings, then `full-access`). `permissionPresetStatus` is `invalid` when the named preset or the host's setting cannot be parsed: the open is still accepted, but every gated tool call is refused, and an invalid preset never counts as one the client set. `effectivePermissionPreset` is the preset the next permission decision uses, `null` while the status is `invalid`. A client confirms a mode it asked for only when `permissionPreset` equals it and `permissionPresetStatus` is `applied`. The `open_session` reply carries the first three fields in `data` as well as in `data.state`.
4783
+
4784
+ A preset a client named is recorded in the session file as a `permission_mode` entry. The entry is a record only: a later open that names no preset still uses the host's own default, and the launch flag always wins.
4785
+
4786
+ Each gated decision picks its preset in this order:
4787
+
4788
+ 1. A preset a client named for the session (`open_session.permissionPreset`, an attach naming one, or `set_permission_preset`) wins.
4789
+ 2. Work a remote device started asks: a turn started by a `prompt`, `follow_up` or `steer` (`whenIdle: "start"`) from a delegated connection, and engine-started work (extension messages, goal and loop turns, `continue_from_leaf`, `wake`, restored input) on a session a remote device has driven. A session counts as remote-driven once a delegated connection sends it any record whose method class is `drive` or `device_admin`; the engine's method-class table decides, so every run starter is covered. That includes records that start no turn, such as `abort`, `set_session_name` or `clear_queue`, and the mark lasts for the session's lifetime on purpose: telling which drive records can shape later engine-started work would need a second hand-kept list beside the table, and a wrong guess there would run remote-influenced work without asking, so the engine errs toward asking. When the operator named no `--permission-preset`, it uses the preset a client recorded in the session file instead. An embedder's host-level preset is the host default, not a launch flag. This rule is decided before `spawnedBy`: a parent named in `open_session`, real or not, never takes a remote device's work off it.
4790
+ 3. A child session (`spawnedBy`) follows its live parent's current mode, including later changes to it. A child whose parent is not live in this process asks, unless the operator named a preset for it with `--permission-preset`.
4791
+ 4. Everything else uses the host's own default. A local connection's turn keeps the host default even on a session a remote device has driven, and a `steer` never changes the origin of the turn it joins.
4792
+
4748
4793
  `currentTurnId` is the id of the running turn, present only while one runs; a steer or follow-up names it as `expectedTurnId` (on `prompt`, `steer` or `follow_up`).
4749
4794
 
4750
4795
  The `model` field is a full [Model](#model) object or `null`. `pendingModelSwitch` is always present: `null` means no held switch, while an object with `provider` and `id` identifies a switch that applies after the next compaction. The `sessionName` field is the display name set via `set_session_name`, or omitted if not set.
@@ -5198,6 +5243,21 @@ Response:
5198
5243
  {"type": "response", "command": "set_auto_compaction", "success": true}
5199
5244
  ```
5200
5245
 
5246
+ #### set_permission_preset
5247
+
5248
+ Move this session to another approval mode. It works on a classic `--mode rpc` connection, which refuses `open_session`, and on a multi-session host with the `sessionId` envelope, so a client on another device can change a live session's mode without knowing its session file. It is classed `device_admin`: it changes what the agent may do without asking. Hosts that take it advertise `set_permission_preset_v1`.
5249
+
5250
+ ```json
5251
+ {"type": "set_permission_preset", "permissionPreset": "ask"}
5252
+ ```
5253
+
5254
+ Response:
5255
+ ```json
5256
+ {"type": "response", "command": "set_permission_preset", "success": true, "data": {"permissionPreset": "ask", "permissionPresetSource": "client", "permissionPresetStatus": "applied"}}
5257
+ ```
5258
+
5259
+ The new preset applies from the next permission decision, and the session file records it like a preset named at open. An unknown preset is refused with `errorCode` `invalid_launch_profile` and changes nothing, so the session keeps the mode it had. Extensions hear a change on the `permission_preset_changed` event and read the enforced preset with `pi.getPermissionPreset()`.
5260
+
5201
5261
  ### Retry
5202
5262
 
5203
5263
  #### set_auto_retry
@@ -5893,6 +5953,32 @@ Response:
5893
5953
 
5894
5954
  `kind` is the admission outcome (`started`, `queued`, `steered`, `already_admitted`, `held_draft`, `turn_conflict`); `admitted` is `[]` when no drain is registered.
5895
5955
 
5956
+ #### admit_external_message
5957
+
5958
+ Admits a message another agent sent this session (a relayed peer delivery) exactly as `pi.session.admitExternalMessage` does in process (multi-session hosts: session-scoped). The engine writes its own `session_control_delivery` custom entry, with `details.sender` and `details.display_text` for human surfaces: never a `role: "user"` message, never the extension `input` event, and a `/`-led `text` is not dispatched as a command or expanded as a template. Method class `drive`.
5959
+
5960
+ ```json
5961
+ {"type": "admit_external_message", "sessionId": "rpc-1", "deliveryId": "d7", "text": "[from peer] build is green", "sender": {"kind": "agent", "session_id": "s-42", "name": "reviewer"}, "displayText": "build is green", "deliverAs": "followUp"}
5962
+ ```
5963
+
5964
+ Response:
5965
+ ```json
5966
+ {
5967
+ "type": "response",
5968
+ "command": "admit_external_message",
5969
+ "success": true,
5970
+ "data": { "kind": "started", "turnId": "0" }
5971
+ }
5972
+ ```
5973
+
5974
+ - `deliveryId` (1-256 chars), `text` (non-empty) and `deliverAs` (`steer` | `followUp`) are required. `sender` is `{ kind: "agent", session_id, name? }`, `{ kind: "command_line", user? }` or `{ kind: "external", platform, author? }` with no other field.
5975
+ - `whenIdle`: `start` (default) starts a turn on an idle session; `queue` holds it for the next turn.
5976
+ - `expectedTurnId` is `get_state.currentTurnId` (a string). A steer whose `expectedTurnId` is not the current turn, or a mid-turn steer that names none, answers `kind: "turn_conflict"` and admits nothing. The reply's `turnId` is the session's turn at admission, in the same form.
5977
+ - `chainCount` is a thread call's hop count, checked against `threads.chainCountLimit` before anything is admitted (refused with `chain_count_limit` / `invalid_chain_count`, as on `prompt`/`steer`/`follow_up`); the admitted hop is written as `details.chain_count`.
5978
+ - `kind` is the same outcome `wake` reports (`held_draft`: retry; nothing was queued). The ledger keys a local connection's `deliveryId` as `rpc:local/<deliveryId>` and a remote device's as `device:<deviceId>/<deliveryId>`, so RPC ids never collide with in-process deliveries or with another device's. An id the runtime already holds, or that the transcript already holds an entry for (after a restart too), answers `already_admitted`.
5979
+ - On a connection that speaks for a remote device (a delegated connection, or one whose `set_client_info` declared `remoteOrigin`), the engine stamps that device as `details.remote_origin` and writes `text` unchanged, so the model reads exactly what a delivery on a local connection carries and no device (senpi#6250). It keeps `text` as `details.display_text` when no `displayText` is given, and refuses a `command_line` sender (`sender_not_allowed`). A delegated connection that names no remote device is refused (`delegated_origin_unknown`), never admitted as a local delivery. A record that names `remote_origin` is refused on every connection. The turn that consumes the delivery takes the connection's origin, so a remote device's follow-up runs under the delegated approval-mode rule even inside a local turn.
5980
+ - Once `release_session` has closed the session's admission, the record answers `success: false` with the release's message: the delivery stays with its sender.
5981
+
5896
5982
  #### release_session
5897
5983
 
5898
5984
  Multi-session hosts only. Hands the session to a runtime outside the host; see "Handing a session over (`release_session`)".
@@ -6259,7 +6345,7 @@ When a multi-session host ends a routing handle it may name why:
6259
6345
  | `client_close` | An attached client sent `close_session` |
6260
6346
  | `idle_evicted` | The idle sweep ended a session that was not retained |
6261
6347
  | `session_dir_removed` | The session's transcript directory was deleted and nothing can reopen it: the idle sweep ended it because no client held it (whatever the idle window), or a generation handoff drain ended it instead of parking it. No `sessionPath` |
6262
- | `host_shutdown` | The host process is exiting (SIGTERM, idle-exit, empty-host). A retained session is closed, not parked |
6348
+ | `host_shutdown` | The host process is exiting (SIGTERM, idle-exit, empty-host). A retained session on a socket host is parked instead (`session_parked { reason: "host_shutdown" }`) |
6263
6349
  | `replaced` | The routing handle ended because the live session behind it was replaced |
6264
6350
  | `handoff_parked` | A generation handoff drained this host; reopen with `open_session { sessionPath }` |
6265
6351
  | `released` | `release_session` handed the session to a runtime outside this host. Carries `sessionPath`, the file that runtime now writes: do NOT reopen it here |
@@ -6387,6 +6473,37 @@ capability:
6387
6473
  }
6388
6474
  ```
6389
6475
 
6476
+ #### Background session age
6477
+
6478
+ The terminal builtin emits `terminal_background_age` on the same path, and on the in-process `pi.events`
6479
+ bus, when a background bash session is still running at 10 minutes and then every 30 minutes (10, 40,
6480
+ 70, ...), and when the opt-in auto-reap (`terminal.backgroundAutoReapMinutes` or the `bash` parameter
6481
+ `auto_reap_minutes`) has stopped one, or once when its stop did not end the session. `data` has `kind`
6482
+ (`running`, `reaped`, or `reap_failed`), `id` (the `bash_id`), `description`, optional `command`,
6483
+ `startedAtMs`, `ageMs`, `ageMinutes`, `autoReapMinutes` (the age at which the session is stopped
6484
+ automatically, or `null` when nothing will stop it), `reason` (on `reap_failed` only: why the stop did not
6485
+ end it), and `stop`, the call that stops it. After `reap_failed` the session is still running, the
6486
+ `running` reports go on, and the auto-reap does not try again. Nothing is stopped by this event; a client that wants the session gone calls `kill_bash`
6487
+ with `stop.args`. A report that came due while the host slept arrives once, not once per missed interval.
6488
+
6489
+ ```json
6490
+ {
6491
+ "type": "extension_event",
6492
+ "name": "terminal_background_age",
6493
+ "data": {
6494
+ "kind": "running",
6495
+ "id": "bash_3",
6496
+ "description": "npm run dev",
6497
+ "command": "npm run dev",
6498
+ "startedAtMs": 1710000000000,
6499
+ "ageMs": 600000,
6500
+ "ageMinutes": 10,
6501
+ "autoReapMinutes": null,
6502
+ "stop": { "tool": "kill_bash", "args": { "bash_id": "bash_3" } }
6503
+ }
6504
+ }
6505
+ ```
6506
+
6390
6507
  #### Job widgets
6391
6508
 
6392
6509
  The `job-widgets` builtin emits `job_widget_state` on the same path: live, display-ready widgets for the
@@ -6450,7 +6567,7 @@ cannot replace the ID on a queued start event. Older engines omit this additive
6450
6567
 
6451
6568
  ### agent_end
6452
6569
 
6453
- Emitted when one low-level agent run completes. Contains all messages generated during this run. If `willRetry` is true, an automatic retry will follow. A run stopped because its session was replaced also carries `abortCause: { kind: "session_replaced", reason, targetSessionFile?, targetSessionId? }` (see `session_replaced`); extensions' own `agent_end` does not carry it, they learn of the replacement from `session_shutdown`.
6570
+ Emitted when one low-level agent run completes. Contains all messages generated during this run. If `willRetry` is true, an automatic retry will follow. A run stopped because its session was replaced also carries `abortCause: { kind: "session_replaced", reason, targetSessionFile?, targetSessionId? }` (see `session_replaced`); a run the host's own stop cut short ends with `aborted: true`, `abortSource: "system"` and `abortCause: { kind: "host_shutdown" }`, never `abortSource: "user"` (senpi#6190); extensions' own `agent_end` does not carry it, they learn of the replacement from `session_shutdown`.
6454
6571
 
6455
6572
  ```json
6456
6573
  {
@@ -21,6 +21,28 @@ Background sessions are NEVER killed by `timeout` (they live until they exit or
21
21
  call `kill_bash`), even though the bash-timeout extension injects a default `timeout`
22
22
  into every `bash` call. Foreground calls behave like the classic `bash` tool.
23
23
 
24
+ ### Long-running background sessions
25
+
26
+ A background session that is still running (started with `run_in_background`, or a
27
+ foreground command that auto-detached) is reported once it has run 10 minutes, then
28
+ every 30 minutes after that (10, 40, 70, ... minutes). Each report names the session's
29
+ `bash_id` and its command or label, says how long it has been running, and gives the
30
+ call that stops it (`kill_bash({ bash_id })`). It reaches the agent like a completion
31
+ notice (subject to `notify`), shows in the TUI, and goes to RPC clients as the
32
+ `terminal_background_age` event ([RPC](rpc.md#background-session-age)). A report that came
33
+ due while the machine slept is sent once, not once per missed interval.
34
+
35
+ Nothing is stopped automatically by default. To stop long sessions at a set age, opt in
36
+ with the `terminal.backgroundAutoReapMinutes` setting, or per call with the `bash`
37
+ parameter `auto_reap_minutes` (which wins over the setting; `0` turns it off for that
38
+ call). At that age the session is stopped the way `kill_bash` stops it (TERM to its
39
+ process group, then KILL after a grace), its completion notice says it was stopped
40
+ automatically, and a `terminal_background_age` event with `kind: "reaped"` is sent.
41
+ It counts as reaped only once that stop has really ended the session. If the stop fails
42
+ or the session survives it, one notice (and a `kind: "reap_failed"` event) says
43
+ auto-reap failed and why; the session keeps running, its age reports go on, and the
44
+ auto-reap does not retry.
45
+
24
46
  Background sessions and monitors also survive a session reload (`/reload`): existing
25
47
  `bash_N` ids stay addressable, watchers keep injecting events, and completion
26
48
  notifications keep arriving. Quitting or switching sessions still tears everything down.
@@ -214,7 +236,8 @@ Configure the suite under `terminal` in `settings.json` (global
214
236
  "scrollback": 10000,
215
237
  "maxSessions": 32,
216
238
  "timeoutAction": "background",
217
- "notify": "wake"
239
+ "notify": "wake",
240
+ "backgroundAutoReapMinutes": 120
218
241
  }
219
242
  }
220
243
  ```
@@ -223,6 +246,7 @@ Configure the suite under `terminal` in `settings.json` (global
223
246
  - `scrollback` — xterm scrollback lines per session (default 10000).
224
247
  - `maxSessions` — concurrent sessions before least-recently-used exited sessions are pruned (default 32).
225
248
  - `timeoutAction` — fate of a foreground timeout (default `background`).
249
+ - `backgroundAutoReapMinutes` — stop a background session once it has run this many minutes (default unset: never). A whole number of minutes of at least 1 turns it on; `0` or any other value leaves it off. The `bash` parameter `auto_reap_minutes` overrides it per call.
226
250
  - `notify` — async completion behavior: `wake` (wake an idle interactive agent once), `next-turn`, or `off` (default `wake`). Non-interactive `-p` / `--print` / `--mode json` runs never wake.
227
251
  - `killGuard` — `on` (default) or `off`, read from the global settings file only (a project value is ignored). While on, an agent command or eval cell that would signal every process (`kill -1`, a `pkill` pattern such as `a||b`, `.*` or one letter), a protected process (pid 1, the agent, its ancestors, the user's service manager) or a process group holding one is refused before it runs with a `[senpi:kill-guard]` message naming the rule and a narrow alternative. Narrow kills by pid, exact name (`pkill -x`) or anchored pattern run as before. Values only known when a command runs (`pkill -f "$P"`, `xargs kill`, a script's `kill $PID`) are judged then: on macOS and Linux every agent shell, monitor and eval kernel gets `kill`, `pkill` and `killall` shims first on `PATH` (in `<agent dir>/kill-guard/<version>/`) and a `BASH_ENV` that wraps bash's builtin `kill`, chaining any `BASH_ENV` you already set; an interactive bash the agent starts gets the same wrapper from a `PROMPT_COMMAND` hook before its first prompt, after your startup files, and your own `PROMPT_COMMAND` still runs after it; a refused command exits 126 with the same message. This also judges what the agent types into a running shell with `bash_input` (edited, recalled or history-expanded lines included), because the shell judges the line it actually runs. Builtin `kill` in dash or zsh, in `bash --posix -c` and in `bash -i -c`, in an interactive bash whose startup files replace `PROMPT_COMMAND`, and a `kill` named by absolute path, are judged only from the command text. A command you type yourself with `!` is never judged and gets no shims. `off` restores the unguarded behavior (no shims, no `BASH_ENV`, `PROMPT_COMMAND` left as you set it) and shows one notice per session.
228
252
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi",
3
- "version": "2026.11.6-2",
3
+ "version": "2026.11.9",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -92,9 +92,9 @@
92
92
  },
93
93
  "dependencies": {
94
94
  "@earendil-works/chord": "0.99.1",
95
- "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.11.6-2",
96
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.11.6-2",
97
- "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.11.6-2",
95
+ "@earendil-works/pi-agent-core": "npm:@code-yeongyu/senpi-agent-core@2026.11.9",
96
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.11.9",
97
+ "@earendil-works/pi-tui": "npm:@code-yeongyu/senpi-tui@2026.11.9",
98
98
  "@silvia-odwyer/photon-node": "0.3.4",
99
99
  "ajv": "8.20.0",
100
100
  "chalk": "6.0.0",
@@ -115,8 +115,8 @@
115
115
  "yaml": "2.9.1",
116
116
  "@anthropic-ai/claude-agent-sdk": "0.3.306",
117
117
  "@anthropic-ai/sdk": "0.127.0",
118
- "@code-yeongyu/senpi-codemode": "2026.11.6-2",
119
- "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.11.6-2",
118
+ "@code-yeongyu/senpi-codemode": "2026.11.9",
119
+ "@earendil-works/pi-pty": "npm:@code-yeongyu/senpi-pty@2026.11.9",
120
120
  "@modelcontextprotocol/sdk": "1.32.1",
121
121
  "@mozilla/readability": "0.6.0",
122
122
  "linkedom": "0.18.13",
@@ -1,2 +0,0 @@
1
- import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url); { const __piWorkerThreads = require("node:worker_threads"); if (typeof __piWorkerThreads.markAsUncloneable !== "function") { __piWorkerThreads.markAsUncloneable = () => {}; } }
2
- import{rateLimitModelFamily}from"./chunk-6VWIMHMH.js";import{describeProviderFailureForUser,isQuotaExhaustionMessage}from"./chunk-OKMHSVCA.js";var BILLING_ERROR_PATTERN=new RegExp(["credit[- ]balance","insufficient[_ -]quota","\\bbilling\\b","purchase credits","credits[-_ ]required","credits are required","usage_limit_reached","usage_not_included","usage limit has been reached"].join("|"),"i"),STRONG_BILLING_ERROR_PATTERN=new RegExp(["credit[- ]balance","insufficient[_ -]quota","purchase credits","credits[-_ ]required","credits are required","usage_limit_reached","usage_not_included","usage limit has been reached"].join("|"),"i"),TRANSIENT_SERVER_SIGNAL_PATTERN=/(?:^|[^0-9])(?:500|502|503|504|520|522|524)(?:[^0-9]|$)|"type"\s*:\s*"server_error"/i;function isBillingErrorMessage(errorMessage){return errorMessage===void 0?!1:STRONG_BILLING_ERROR_PATTERN.test(errorMessage)?!0:BILLING_ERROR_PATTERN.test(errorMessage)?!TRANSIENT_SERVER_SIGNAL_PATTERN.test(errorMessage):!1}var AccountUsageLimitError=class extends Error{constructor(provider,raw="usage limit reached",resetsAt){super(describeProviderFailureForUser(raw,{model:provider,usageLimit:"account",...resetsAt===void 0?{}:{resetsAt:formatResetTime(resetsAt)}})),this.name="AccountUsageLimitError"}};function formatResetTime(epochMs){return new Date(epochMs).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",timeZoneName:"short"})}var SUBSCRIPTION_LIMIT_PATTERN=/\bhit\s+your\b[^.]*\blimit\b|\b(?:session|weekly|monthly|daily|hourly|\d+[- ]hour|usage)\s+limit\b|\bblocking_limit\b|\brapid_refill_breaker\b/i,PLAN_MODEL_LIMIT_PATTERN=/\breached\b[^.]*\b(?:free|plan|tier|model)\b[^.]*\blimit\b[\s\S]*\bswitch to (?:a |an )?(?:different|another) model\b/i,MODEL_SCOPE_PATTERN=/\bmodels?\b|\bpremium\b|\b(?:opus|sonnet|haiku|fable|mythos)\b/i;function usageLimitScope(failure){if(typeof failure=="object"&&failure.errorCause?.type==="usage_limit"){let scope=failure.errorCause.scope;if(scope==="account"||scope==="model")return scope}let errorMessage=typeof failure=="object"?failure.errorMessage:failure;if(errorMessage===void 0)return;if(rateLimitModelFamily(errorMessage)!==void 0)return"model";if(isQuotaExhaustionMessage(errorMessage)||isBillingErrorMessage(errorMessage)||SUBSCRIPTION_LIMIT_PATTERN.test(errorMessage)||PLAN_MODEL_LIMIT_PATTERN.test(errorMessage))return MODEL_SCOPE_PATTERN.test(errorMessage)?"model":"account"}function describeUsageLimitMessage(message){let scope=usageLimitScope(message);return!scope||message.errorCause?.type==="usage_limit"&&message.errorCause.scope===scope?message:{...message,errorCause:{type:"usage_limit",scope},...scope==="account"?{errorMessage:describeProviderFailureForUser(message.errorMessage,{model:`${message.provider}/${message.model}`,usageLimit:scope})}:{}}}function describeUsageLimitEntry(entry){if(entry.type!=="message"||entry.message.role!=="assistant")return entry;let message=describeUsageLimitMessage(entry.message);return message===entry.message?entry:{...entry,message}}function usageLimitCause(from,limit){if(limit==="model")return`${from} hit its usage limit`;if(limit==="account")return`the ${from.slice(0,Math.max(0,from.indexOf("/")))||from} account hit its usage limit, so its other models were skipped`}var SecretInputUnavailableError=class extends Error{constructor(reason){super("This client cannot collect a secret; use the TUI or the host's settings route."),this.code="secret_input_unavailable",this.name="SecretInputUnavailableError",this.reason=reason}};function resolveSecretInputTimeout(opts){let requested=opts?.timeout??18e5;if(!Number.isFinite(requested)||requested<=0)throw new Error("Invalid secret input timeout");let timeout=Math.ceil(requested);if(!Number.isSafeInteger(Date.now()+timeout))throw new Error("Invalid secret input timeout");return timeout}function armSecretInputDeadline(deadline,expire){let timer,check=()=>{let remaining=deadline-Date.now();remaining<=0?expire():timer=setTimeout(check,Math.min(remaining,2147483647))};return timer=setTimeout(check,Math.max(0,Math.min(deadline-Date.now(),2**31-1))),()=>clearTimeout(timer)}function isSecretInputValue(value){return!/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value)}import{realpathSync,watch}from"node:fs";import{opendir}from"node:fs/promises";var FS_WATCH_RETRY_DELAY_MS=5e3;function closeWatcher(watcher){if(watcher)try{watcher.close()}catch{}}function canonicalWatchPath(path){if(process.platform!=="win32")return path;try{return realpathSync.native(path)}catch{return path}}async function probeDirectoryOpenable(directory){let handle=await opendir(directory);try{await handle.read()}finally{await handle.close()}}function watchWithErrorHandler(path,listener,onError,options){try{let watcher=watch(canonicalWatchPath(path),{...options,encoding:"utf8"},listener);return watcher.on("error",onError),watcher}catch{return onError(),null}}export{FS_WATCH_RETRY_DELAY_MS,closeWatcher,canonicalWatchPath,probeDirectoryOpenable,watchWithErrorHandler,isBillingErrorMessage,AccountUsageLimitError,usageLimitScope,describeUsageLimitMessage,describeUsageLimitEntry,usageLimitCause,SecretInputUnavailableError,resolveSecretInputTimeout,armSecretInputDeadline,isSecretInputValue};