@code-yeongyu/senpi 2026.9.21-2 → 2026.9.22

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 (239) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/bundle/chunks/{anthropic-messages-C3ZY2ANT.js → anthropic-messages-2P7APOOQ.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-HSX5RFAP.js → app-server-command-DLBXJ5K5.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-7TAGS2VU.js → azure-openai-responses-3AH7MJWF.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-3SLCHXZO.js → chunk-4AXZDO3E.js} +2 -2
  6. package/dist/bundle/chunks/{chunk-Z3KQB2R3.js → chunk-7ICH7UFW.js} +12 -12
  7. package/dist/bundle/chunks/{chunk-KNM6SRPE.js → chunk-AAVNXTGQ.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-IDTLJYJ5.js → chunk-AY37SZJZ.js} +4 -4
  9. package/dist/bundle/chunks/{chunk-XHMOFY4T.js → chunk-BUPX33Q4.js} +3 -3
  10. package/dist/bundle/chunks/{chunk-GLKVDRNP.js → chunk-CWCY7LXK.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-POMPEGWQ.js → chunk-F3DXL5MX.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-GVI2HP7N.js → chunk-I2HIIJPI.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-T6XA2IZR.js → chunk-JR4X6ECO.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-M2XAY6I7.js → chunk-M2HSO7MH.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-MIVOQT3Q.js → chunk-N5AQJAQ2.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-UYVPLEOL.js → chunk-NK2RNX25.js} +1 -1
  17. package/dist/bundle/chunks/chunk-NMB4C6ZZ.js +2 -0
  18. package/dist/bundle/chunks/{chunk-B7W62BM7.js → chunk-P4OHEJ7P.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-I425CMTO.js → chunk-VBNHBSBD.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-B2IM56LB.js → chunk-VYIJQQ5I.js} +1 -1
  21. package/dist/bundle/chunks/chunk-XRHUMMIW.js +8 -0
  22. package/dist/bundle/chunks/{chunk-47GF6XYZ.js → chunk-YT2ZSZOY.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-OSDJ2SYO.js → chunk-ZCBX2K34.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-D5CL7PPD.js → chunk-ZMVXBX33.js} +2 -2
  25. package/dist/bundle/chunks/{cli-main-GQTEEZ2V.js → cli-main-D2VPNIQ3.js} +1 -1
  26. package/dist/bundle/chunks/{google-generative-ai-JWIH75VF.js → google-generative-ai-Y2IYMUVC.js} +1 -1
  27. package/dist/bundle/chunks/{google-vertex-IRGAY5XR.js → google-vertex-TKBBMPHV.js} +1 -1
  28. package/dist/bundle/chunks/{host-command-KOEKIHIZ.js → host-command-IFIIOX5S.js} +1 -1
  29. package/dist/bundle/chunks/host-lifecycle-3OJZA4QQ.js +3 -0
  30. package/dist/bundle/chunks/host-lifecycle.js +3 -3
  31. package/dist/bundle/chunks/{interactive-host-runtime-2KTFEW7O.js → interactive-host-runtime-V7PBDIOJ.js} +1 -1
  32. package/dist/bundle/chunks/{interactive-mode-I3ILWXQB.js → interactive-mode-RFMMGQCT.js} +1 -1
  33. package/dist/bundle/chunks/kimi-coding.js +2 -2
  34. package/dist/bundle/chunks/{mistral-conversations-VOBYATVJ.js → mistral-conversations-ZYDVFRYF.js} +1 -1
  35. package/dist/bundle/chunks/{multi-session-host-S5FWCIWE.js → multi-session-host-45YMCQIH.js} +8 -7
  36. package/dist/bundle/chunks/openai-codex-responses-MEIUNROM.js +10 -0
  37. package/dist/bundle/chunks/{openai-completions-YK6QTYOH.js → openai-completions-4V76L5ZJ.js} +1 -1
  38. package/dist/bundle/chunks/openai-responses-YR7WRBG3.js +2 -0
  39. package/dist/bundle/chunks/{package-manager-cli-HOL6CAI6.js → package-manager-cli-7DQKRSI5.js} +1 -1
  40. package/dist/bundle/chunks/rotation-stream-YPZP3VJS.js +2 -0
  41. package/dist/bundle/chunks/{rpc-mode-L3DSGTVM.js → rpc-mode-4GWZCJKI.js} +1 -1
  42. package/dist/bundle/chunks/{session-picker-L3FYEQBL.js → session-picker-ZKRYWZGC.js} +1 -1
  43. package/dist/bundle/chunks/session-worker.js +38 -37
  44. package/dist/bundle/cli.js +1 -1
  45. package/dist/bundle/index.js +1 -1
  46. package/dist/bundle/rpc-entry.js +1 -1
  47. package/dist/core/agent-session.d.ts +6 -4
  48. package/dist/core/agent-session.d.ts.map +1 -1
  49. package/dist/core/agent-session.js +9 -7
  50. package/dist/core/agent-session.js.map +1 -1
  51. package/dist/core/credential-pool/classify.d.ts.map +1 -1
  52. package/dist/core/credential-pool/classify.js +12 -0
  53. package/dist/core/credential-pool/classify.js.map +1 -1
  54. package/dist/core/credential-pool/failover.d.ts +37 -5
  55. package/dist/core/credential-pool/failover.d.ts.map +1 -1
  56. package/dist/core/credential-pool/failover.js +60 -24
  57. package/dist/core/credential-pool/failover.js.map +1 -1
  58. package/dist/core/credential-pool/rotation-events.d.ts +5 -0
  59. package/dist/core/credential-pool/rotation-events.d.ts.map +1 -0
  60. package/dist/core/credential-pool/rotation-events.js +25 -0
  61. package/dist/core/credential-pool/rotation-events.js.map +1 -0
  62. package/dist/core/credential-pool/rotation-stream.d.ts +4 -3
  63. package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -1
  64. package/dist/core/credential-pool/rotation-stream.js +8 -11
  65. package/dist/core/credential-pool/rotation-stream.js.map +1 -1
  66. package/dist/core/extensions/builtin/ask-user/schema.d.ts +2 -25
  67. package/dist/core/extensions/builtin/ask-user/schema.d.ts.map +1 -1
  68. package/dist/core/extensions/builtin/ask-user/schema.js.map +1 -1
  69. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +2 -7
  70. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  71. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +2 -8
  72. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  73. package/dist/core/extensions/builtin/mcp/expose/naming.d.ts +0 -1
  74. package/dist/core/extensions/builtin/mcp/expose/naming.d.ts.map +1 -1
  75. package/dist/core/extensions/builtin/mcp/expose/naming.js +0 -3
  76. package/dist/core/extensions/builtin/mcp/expose/naming.js.map +1 -1
  77. package/dist/core/extensions/builtin/mcp/expose/register.d.ts +1 -2
  78. package/dist/core/extensions/builtin/mcp/expose/register.d.ts.map +1 -1
  79. package/dist/core/extensions/builtin/mcp/expose/register.js +0 -21
  80. package/dist/core/extensions/builtin/mcp/expose/register.js.map +1 -1
  81. package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts +2 -2
  82. package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts.map +1 -1
  83. package/dist/core/extensions/builtin/mcp/expose/schema-compat.js +2 -2
  84. package/dist/core/extensions/builtin/mcp/expose/schema-compat.js.map +1 -1
  85. package/dist/core/http-dispatcher.d.ts +29 -0
  86. package/dist/core/http-dispatcher.d.ts.map +1 -1
  87. package/dist/core/http-dispatcher.js +30 -3
  88. package/dist/core/http-dispatcher.js.map +1 -1
  89. package/dist/modes/interactive/components/assistant-render-descriptors.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/assistant-render-descriptors.js +8 -7
  91. package/dist/modes/interactive/components/assistant-render-descriptors.js.map +1 -1
  92. package/dist/modes/print-mode.d.ts.map +1 -1
  93. package/dist/modes/print-mode.js +7 -5
  94. package/dist/modes/print-mode.js.map +1 -1
  95. package/dist/modes/rpc/handoff-activity.d.ts +4 -0
  96. package/dist/modes/rpc/handoff-activity.d.ts.map +1 -0
  97. package/dist/modes/rpc/handoff-activity.js +6 -0
  98. package/dist/modes/rpc/handoff-activity.js.map +1 -0
  99. package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
  100. package/dist/modes/rpc/host-ensure.js +60 -17
  101. package/dist/modes/rpc/host-ensure.js.map +1 -1
  102. package/dist/modes/rpc/host-lifecycle.d.ts +3 -0
  103. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  104. package/dist/modes/rpc/host-lifecycle.js +25 -5
  105. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  106. package/dist/modes/rpc/multi-session-host.d.ts +1 -0
  107. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  108. package/dist/modes/rpc/multi-session-host.js +40 -7
  109. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  110. package/dist/modes/rpc/rpc-types.d.ts +13 -0
  111. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  112. package/dist/modes/rpc/rpc-types.js.map +1 -1
  113. package/dist/modes/rpc/session-command-router.d.ts +10 -4
  114. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  115. package/dist/modes/rpc/session-command-router.js +90 -18
  116. package/dist/modes/rpc/session-command-router.js.map +1 -1
  117. package/dist/modes/rpc/session-event-writer.d.ts +5 -3
  118. package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
  119. package/dist/modes/rpc/session-event-writer.js +21 -2
  120. package/dist/modes/rpc/session-event-writer.js.map +1 -1
  121. package/dist/modes/rpc/session-worker-client.d.ts +1 -0
  122. package/dist/modes/rpc/session-worker-client.d.ts.map +1 -1
  123. package/dist/modes/rpc/session-worker-client.js +3 -0
  124. package/dist/modes/rpc/session-worker-client.js.map +1 -1
  125. package/dist/modes/rpc/session-worker-protocol.d.ts +3 -1
  126. package/dist/modes/rpc/session-worker-protocol.d.ts.map +1 -1
  127. package/dist/modes/rpc/session-worker-protocol.js.map +1 -1
  128. package/dist/modes/rpc/session-worker.js +7 -1
  129. package/dist/modes/rpc/session-worker.js.map +1 -1
  130. package/docs/providers.md +9 -1
  131. package/docs/rpc.md +58 -8
  132. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +15 -0
  133. package/node_modules/@code-yeongyu/senpi-codemode/README.md +51 -13
  134. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  135. package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/agent-bridge.ts +53 -9
  136. package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/reserved-dispatch.ts +1 -0
  137. package/node_modules/@code-yeongyu/senpi-codemode/src/config/settings.ts +18 -0
  138. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/eval-status.ts +9 -4
  139. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +3 -0
  140. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/AGENTS.md +2 -2
  141. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/prelude.jl +10 -0
  142. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +18 -6
  143. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +2 -6
  144. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/prelude.ts +1 -1
  145. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/run-queue.ts +17 -7
  146. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +6 -1
  147. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel-contract.ts +2 -5
  148. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +37 -5
  149. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +11 -2
  150. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/prelude.rb +6 -0
  151. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-contract.ts +2 -5
  152. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +14 -4
  153. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-queue.ts +18 -5
  154. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt-template.ts +3 -2
  155. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +4 -1
  156. package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/idle-timeout.ts +9 -2
  157. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/AGENTS.md +2 -0
  158. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-execution.ts +19 -2
  159. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-runtime.ts +11 -3
  160. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-contract.ts +6 -1
  161. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +72 -20
  162. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-snapshot.ts +28 -4
  163. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-state.ts +5 -4
  164. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-status.ts +5 -2
  165. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-eval-result.ts +76 -25
  166. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-execution-event.ts +4 -0
  167. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-kernel-reset-refused-error.ts +12 -0
  168. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-request.ts +2 -1
  169. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool-options.ts +3 -1
  170. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +58 -37
  171. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/managed-cell.ts +15 -8
  172. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +20 -6
  173. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/run-eval-cell.ts +89 -25
  174. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/terminal-snapshot-store.ts +4 -0
  175. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +51 -10
  176. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  177. package/node_modules/@earendil-works/pi-ai/README.md +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +14 -56
  179. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +18 -27
  182. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.d.ts +40 -0
  184. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.d.ts.map +1 -0
  185. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.js +102 -0
  186. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.js.map +1 -0
  187. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.d.ts +4 -3
  188. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
  189. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.js +28 -14
  190. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.js.map +1 -1
  191. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.d.ts +44 -0
  192. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.d.ts.map +1 -0
  193. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.js +120 -0
  194. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.js.map +1 -0
  195. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts +2 -2
  196. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts.map +1 -1
  197. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js +2 -2
  198. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js.map +1 -1
  199. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts +3 -0
  200. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts.map +1 -1
  201. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js +21 -15
  202. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js.map +1 -1
  203. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -0
  204. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  205. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  206. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  207. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  208. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  209. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  210. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  211. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  212. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  213. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  214. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.d.ts +9 -0
  215. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.d.ts.map +1 -0
  216. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.js +43 -0
  217. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.js.map +1 -0
  218. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.d.ts.map +1 -1
  219. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.js +3 -2
  220. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.js.map +1 -1
  221. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.d.ts +19 -0
  222. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.d.ts.map +1 -0
  223. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.js +63 -0
  224. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.js.map +1 -0
  225. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts +1 -0
  226. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  227. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +1 -1
  228. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  229. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  230. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  231. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  232. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  233. package/package.json +7 -7
  234. package/dist/bundle/chunks/chunk-5J5R5AMP.js +0 -8
  235. package/dist/bundle/chunks/chunk-I3F6VZZF.js +0 -2
  236. package/dist/bundle/chunks/host-lifecycle-DGZJTBDJ.js +0 -3
  237. package/dist/bundle/chunks/openai-codex-responses-L2PO2D6F.js +0 -10
  238. package/dist/bundle/chunks/openai-responses-YDUBVF4S.js +0 -2
  239. package/dist/bundle/chunks/rotation-stream-Y4YU6HV4.js +0 -2
package/docs/providers.md CHANGED
@@ -22,6 +22,7 @@ Use `/login` in interactive mode, then select a provider:
22
22
  - GitHub Copilot
23
23
  - xAI (Grok/X subscription)
24
24
  - OpenRouter (OAuth-minted API key billed from OpenRouter credits)
25
+ - Kimi Code (kimi.com / kimi.ai subscriptions)
25
26
  - Radius
26
27
  - Cursor (Pro/Ultra/Teams) — authentication only for now, see below
27
28
 
@@ -136,6 +137,13 @@ Upstreams that require explicit cache breakpoints get `cache_control` blocks thr
136
137
 
137
138
  senpi sends `prompt_cache_key` (set to the session id) on Moonshot requests. Kimi documents the field as required for the Kimi Code Plan and recommended for any multi-turn agent ([Kimi context caching](https://platform.kimi.ai/docs/guide/use-context-caching-feature-of-kimi-api)). Kimi reports cache hits as a flat `usage.cached_tokens` field, which senpi parses as cache-read tokens.
138
139
 
140
+ ### Kimi Code
141
+
142
+ - Run `/login kimi-coding`, then select **Sign in with Kimi Code** and pick the service that hosts your account: **Mainland China (kimi.com)** or **Outside mainland China (kimi.ai)**
143
+ - The region is stored with the credential; token refresh and model requests follow it (`auth.kimi.ai` + `api.kimi.ai/coding` for international accounts), so a `.ai` login keeps working after restarts and env changes
144
+ - **Use an API key** asks the same region question and routes `KIMI_API_KEY` requests by it; `KIMI_CODE_REGION=global` (or `mainland-cn`) answers it for headless setups
145
+ - `KIMI_CODE_OAUTH_HOST` / `KIMI_OAUTH_HOST` still override the auth host for new logins and for credentials saved before regions existed; a host other than the two official ones is kept verbatim and needs a `models.json` `baseUrl` for inference
146
+
139
147
  ### Radius
140
148
 
141
149
  Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
@@ -286,7 +294,7 @@ senpi
286
294
  | Fireworks | `FIREWORKS_API_KEY` | `fireworks` |
287
295
  | Together AI | `TOGETHER_API_KEY` | `together` |
288
296
  | Baseten | `BASETEN_API_KEY` | `baseten` |
289
- | Kimi For Coding | `KIMI_API_KEY` | `kimi-coding` |
297
+ | Kimi For Coding | `KIMI_API_KEY` (+ `KIMI_CODE_REGION`) | `kimi-coding` |
290
298
  | MiniMax | `MINIMAX_API_KEY` | `minimax` |
291
299
  | MiniMax (China) | `MINIMAX_CN_API_KEY` | `minimax-cn` |
292
300
  | Qwen Token Plan (existing catalog) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |
package/docs/rpc.md CHANGED
@@ -207,7 +207,13 @@ are what this decision encodes; the other two are what a client must not undo el
207
207
  `ensureHost` records a `writer: { pid, startTime }` stamp in its pidfile and stops the host it names only when
208
208
  that stamp is this process (the start time is what keeps a recycled pid from inheriting the right). A pidfile
209
209
  written by anyone else fails the ensure with `HostEnsureRefusedError { reason: "foreign_writer" }` and the host
210
- keeps running.
210
+ keeps running. That refusal is about signalling, so it holds only while something still accepts connections at
211
+ the public path: a registration that names a live process whose entry is GONE (it was replaced by another socket
212
+ and the replacement later exited) or whose entry nobody listens behind (a dead listener left it, a successor's
213
+ rename never landed) describes a generation nobody can reach, and binding a fresh generation to the free path
214
+ touches nothing of it. The ensure then starts one beside it, numbered after the stranded generation, which
215
+ keeps its registration until it exits; refusing there locked every client out until that process happened to
216
+ end (#1936). A named pipe has no entry to lose, so win32 keeps refusing.
211
217
  - **I2 - compatibility is protocol version + capabilities, never semver equality.** An ordinal that cannot be
212
218
  compared (a build without git metadata, a host that reports none) is EQUAL, and since a handoff requires
213
219
  STRICTLY greater, such a pair attaches instead of upgrading.
@@ -238,10 +244,11 @@ client's sessions. A GENERATION HANDOFF replaces the process while its sessions
238
244
  running host keeps serving.
239
245
  3. `handoffHost` then registers the successor (its own `generations/<instanceId>/host.pid`, the pointer
240
246
  moved onto it, and `settings.json { socket, generation }`) and sends
241
- the predecessor SIGUSR1 = DRAIN: stop accepting, keep every connection already proxied, park each
242
- retained session as soon as its turn settles, and exit through the ordinary idle path. Attached clients
243
- see `session_closed { reason: "handoff_parked" }` for a parked session and reopen it with
244
- `open_session { sessionPath }`.
247
+ the predecessor SIGUSR1 = DRAIN: stop accepting, announce `host_superseded` to every connection,
248
+ park attached and unattached sessions as their turns and requests settle, then exit independently of
249
+ connection occupancy. Clients see `session_closed { reason: "handoff_parked", sessionPath }` before
250
+ their connection closes and reopen with `open_session { sessionPath }` on the successor. Losing
251
+ ownership of the public socket triggers the same sequence even if SIGUSR1 never arrives.
245
252
 
246
253
  Two guards decide whether a handoff is attempted at all, and both fail closed:
247
254
 
@@ -485,7 +492,7 @@ exposing session content. The one exception is `session_closed` and `session_par
485
492
  `kind: "worker"`: they are delivered only to
486
493
  the connections attached to that session, so machine-driven work neither appears in nor disappears from a client that never
487
494
  asked for it. Every other lifecycle record, and every record of an `interactive` session, keeps today's broadcast. Host-level
488
- records (`host_stalled`, `host_memory_pressure`) are broadcast the same way and describe the host process rather than a
495
+ records (`host_superseded`, `host_stalled`, `host_memory_pressure`) are broadcast the same way and describe the host process rather than a
489
496
  session. Correlated responses and dialog extension UI
490
497
  requests (select, confirm, input, and editor) are requester-only; other extension UI state records go to the session's
491
498
  attached connections. To observe a foreign session, open it by its existing
@@ -599,6 +606,48 @@ internal directory. The supervisor also exports `SENPI_RPC_HOST_WATCH_PPID` as a
599
606
  set only by the supervisor: a host started any other way (plain `senpi --mode rpc --listen …`, embedders, hand-started
600
607
  hosts) sees neither variable and is unaffected. A host whose supervisor is alive is never touched by this binding.
601
608
 
609
+ ### Supersession lifecycle: `host_superseded`
610
+
611
+ Before parking for a generation handoff, the old host sends every connected client one unsolicited record:
612
+
613
+ ```json
614
+ {"type":"host_superseded","instanceId":"old-host-uuid","generation":0,"successor":{"socket":"/tmp/agent/rpc.sock"}}
615
+ ```
616
+
617
+ `instanceId` and `generation` identify the OLD host, matching its `get_protocol_info` response. `successor.socket`
618
+ identifies the public socket, never the supervisor's private hop. `successor` is `null` when a drain is requested
619
+ without a known replacement. This record has no request id or session handle; older clients can ignore it.
620
+
621
+ 1. The announcement is queued on every connection before handoff parking changes any session.
622
+ 2. Every parkable session, attached or unattached, is parked immediately. A session is **parkable** when no agent
623
+ run, bash/tool execution, compaction, queued session work, or in-flight host request remains. Accepted opens,
624
+ binding creation, close requests, and steer delivery must finish. This is the ordinary busy predicate with
625
+ **durable wake-source holds excluded**: persistent monitors, scheduled continuations and goal-loop holds do not
626
+ pin an old generation. They resume from the persisted session when reopened, as after a restart. Ordinary idle
627
+ eviction retains its broader activity predicate, including all wake sources.
628
+ 3. Active sessions keep serving their existing connections and park when their turns and requests settle. Each
629
+ released handle emits `session_closed { sessionId, reason: "handoff_parked", sessionPath }` after releasing its
630
+ file. This is not a session deletion: reopen `sessionPath` on the successor to obtain a new routing handle.
631
+ That `sessionPath` is the CANONICAL path the host reserved the file under, which is not necessarily the string
632
+ the client passed to `open_session` (a scratch dir under `/var/...` on macOS is reported as `/private/var/...`).
633
+ A client matching the record against its own stored path must canonicalize before comparing; reopening with
634
+ either spelling resolves to the same reservation.
635
+ The connection closes after its last attached session parks; a connection shared with another active session
636
+ stays open until that session also settles. A command racing parking on the old connection is either served
637
+ or answered by that terminal record and close, never `unknown_session` for the parked handle. New opens on a
638
+ draining connection are refused with `host_draining`.
639
+ 4. The **supervisor** starts `SENPI_RPC_HANDOFF_GRACE_MS` at supersession (default **600000 ms / 10 minutes**).
640
+ This is a soft grace, not an abort deadline or a delay before parking. At expiry it reports outstanding work
641
+ and requests another drain pass. Sessions still mid-turn or handling requests remain alive even beyond grace;
642
+ no turn is cut short. Parkable sessions never wait for grace. A bare socket host has no supervisor grace timer,
643
+ but follows the same announce/park/settle sequence.
644
+ 5. Once every session has parked and its terminal records have drained, the generation exits even if an observer
645
+ or client never voluntarily disconnects. Neither the normal idle timeout nor persistent-host mode can pin it.
646
+
647
+ A client may react to `host_superseded` early, but the old generation retains each file until its work settles;
648
+ `session_path_in_use` on the successor remains a retryable response until parking releases that claim. A client
649
+ that ignores the announcement still receives the terminal record and connection close.
650
+
602
651
  ### Shared host occupancy (idle eviction, retention, empty-host exit)
603
652
 
604
653
  **The daemon does not cap sessions.** On the in-process runtime - the default for a `--listen` socket host, i.e. the
@@ -619,8 +668,8 @@ What the host does enforce are lifecycle windows, and they only ever return memo
619
668
  `session_parked { sessionId, sessionPath }` and there is no `close_session` response, because nothing closed the session -
620
669
  the routing handle was released while the session itself stays on disk and reopens with `open_session { sessionPath }`
621
670
  (as a NEW handle). A client that does not know `session_parked` ignores it and learns the handle is gone from its next
622
- command's `unknown_session`. A GENERATION HANDOFF parks the same sessions for the same reason, but names itself in
623
- the record it emits: `session_closed { sessionId, reason: "handoff_parked" }`, so a client can tell "the host handed
671
+ command's `unknown_session`. A GENERATION HANDOFF also parks attached sessions, using the distinct activity
672
+ predicate below, and emits `session_closed { sessionId, reason: "handoff_parked", sessionPath }`, so a client can tell "the host handed
624
673
  over, reopen by path" from "this session ended". An explicit `close_session` is `reason: "client_close"`; the host
625
674
  process exiting is `reason: "host_shutdown"` (even for a retained session - the process is going away, so this is not
626
675
  a park). `reason` is optional on the wire; a client that does not know a
@@ -836,6 +885,7 @@ In the response `error` field, machine-matchable:
836
885
  - `session_reservation_limit` (this worker already holds 64 live session paths; the open or session replacement was refused without disturbing the existing session)
837
886
  - `missing_session_id` (session-scoped command without `sessionId` in multi mode)
838
887
  - `multi_session_disabled` (`open_session` in classic mode)
888
+ - `host_draining` (`open_session` on a connection whose generation is parking for a handoff; the successor already owns the public path, so re-resolve it and open there rather than retrying this connection)
839
889
  - `invalid_path` (relative `sessionPath`/`cwd`)
840
890
  - `open_failed: <detail>`
841
891
  - `invalid_session_context: <detail>` (`open_session.context` past a documented cap: more than 32 keys, a key that does not match `^[a-z][a-z0-9_]*$`, a non-string or >16 KiB value, or more than 32 KiB of JSON in total; the detail names the cap and its byte budget)
@@ -12,6 +12,21 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.22] - 2026-09-21
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ - `eval` runs up to `maxDetachedCells` background cells per session (default 15, `SENPI_CODEMODE_MAX_DETACHED_CELLS`) and queues same-language cells on their kernel instead of rejecting them; `list` observes live and recent cells, and resetting a busy language refuses with `eval_kernel_busy_reset_refused` instead of stopping live work. ([#1908](https://github.com/code-yeongyu/senpi/issues/1908))
22
+ - `agent()` forwards `isolated`, `apply`, and `merge` when the task host advertises isolation. Hosts that do not still drop those options with the existing warning. A foreground call whose isolation did not apply now raises instead of looking successful; with `handle: true` the isolation result arrives on completion. ([#1910](https://github.com/code-yeongyu/senpi/issues/1910))
23
+
24
+ ### Changed
25
+
26
+ ### Fixed
27
+
28
+ ### Removed
29
+
15
30
  ## [2026.9.21-2] - 2026-09-21
16
31
 
17
32
  ### Breaking Changes
@@ -93,6 +93,7 @@ Configuration is loaded in this order:
93
93
  "foregroundWindowSeconds": 60,
94
94
  "runBudgetSeconds": 300,
95
95
  "hardLimitSeconds": 1800,
96
+ "maxDetachedCells": 15,
96
97
  "parallelPoolWidth": 4,
97
98
  "taskTools": {
98
99
  "task": "task",
@@ -110,9 +111,10 @@ Configuration is loaded in this order:
110
111
  | --- | --- | --- |
111
112
  | `languages` | `py`/`js` enabled; `rb`/`jl` disabled | Selects desired languages before interpreter detection. |
112
113
  | `cellTimeoutSeconds` | `30` | Idle time an interactive call blocks the turn before the cell detaches. Print/json calls never detach. |
113
- | `foregroundWindowSeconds` | `60` | Caps `cellTimeoutSeconds` and the grace a bridge-parked cell gets before it detaches, so an interactive call never blocks the turn longer than this. Env override: `SENPI_CODEMODE_FOREGROUND_SECONDS`. |
114
- | `runBudgetSeconds` | `300` | Kill deadline for a cell's own execution time - child processes, network, timers, CPU. Time parked in host tool calls (`agent()`, `tool.*`) is not charged, and the budget keeps counting after the cell detaches. A per-call `timeout` replaces it for that cell. Env override: `SENPI_CODEMODE_RUN_BUDGET_SECONDS`. |
115
- | `hardLimitSeconds` | `1800` | Wall-clock kill deadline for a cell, parked or not; a per-call `timeout` above it raises it. Env override: `SENPI_CODEMODE_HARD_LIMIT_SECONDS`. |
114
+ | `foregroundWindowSeconds` | `60` | Submission-based foreground limit, including queue and bridge pauses. The cell detaches if capacity is available; otherwise it is cancelled with `eval_background_capacity_reached`. Env override: `SENPI_CODEMODE_FOREGROUND_SECONDS`. |
115
+ | `runBudgetSeconds` | `300` | Kill deadline for a cell's own execution time - child processes, network, timers, CPU. Time queued or parked in host tool calls (`agent()`, `tool.*`) is not charged, and the budget keeps counting after the cell detaches. A per-call `timeout` replaces it for that cell. Env override: `SENPI_CODEMODE_RUN_BUDGET_SECONDS`. |
116
+ | `hardLimitSeconds` | `1800` | Wall-clock kill deadline from submission, including queue and parked time; a per-call `timeout` above it raises it. Env override: `SENPI_CODEMODE_HARD_LIMIT_SECONDS`. |
117
+ | `maxDetachedCells` | `15` | Global detached-cell capacity across all kernels, including queued cells. At capacity, interactive cells stay foreground until completion or the foreground window elapses. Env override: `SENPI_CODEMODE_MAX_DETACHED_CELLS`. |
116
118
  | `parallelPoolWidth` | `4` | Maximum concurrent `parallel()` thunks. |
117
119
  | `taskTools.task` | `"task"` | Registered tool name used by `agent()`. |
118
120
  | `taskTools.output` | `"task_output"` | Registered tool name used by `output()`. |
@@ -126,6 +128,9 @@ enables; `0` or `false` disables. Any other value leaves the file setting in
126
128
  effect.
127
129
 
128
130
  Malformed JSON or invalid settings fall back to defaults with a warning.
131
+ The detached-cell environment override uses the run-budget parser: a positive
132
+ base-10 integer wins over the file value; zero, negative, and malformed values
133
+ leave the file value in effect.
129
134
 
130
135
  ## Cell helpers
131
136
 
@@ -162,13 +167,25 @@ tasks return the requested transcript. Missing tools produce a clear
162
167
  availability error instead of importing an orchestration package. `agent()`
163
168
  delegates through the tool contract, so task-engine permissions, progress
164
169
  updates, and transcripts remain owned by that engine.
165
- `isolated`, `apply`, and `merge` are accepted for compatibility but emit a
166
- warning because this task-engine integration has no isolation model.
170
+ `isolated` and `apply` accept booleans; `merge` accepts `"patch"` or `"branch"`
171
+ (and `false`/`true` aliases respectively). The bridge checks the configured task
172
+ parameter schema once per bridge, using the same host catalog as `tool_schema()`.
173
+ If it advertises `isolated`, these options are forwarded; otherwise they are
174
+ omitted with the existing warning. Senpi does not implement isolation itself.
175
+ A foreground host result with `details.isolation.changes_applied === false`
176
+ raises `AgentIsolationNotAppliedError` (`isolation_not_applied`), including any
177
+ `patch_path`, `branch_name`, and `manual_command` recovery fields in its message.
178
+ The bridge retains host `details.isolation`; successful foreground helpers still
179
+ return text or parsed JSON.
167
180
 
168
181
  Background `agent()` handles retain `id` and `agent://<id>` and include `run_epoch`.
169
182
  The host must return structured `details.task_id` (`st_` plus lowercase hex) and
170
183
  an integer `details.run_epoch >= 0`. Missing or malformed details raise
171
- `invalid_task_handle`; prose IDs are never used. Foreground text/JSON is unchanged.
184
+ `invalid_task_handle`; prose IDs are never used. Handles return immediately,
185
+ so final isolation results are not available on the initial handle: await the
186
+ completion notification or read `task_output` (via `output()`) after completion.
187
+ Any isolation metadata supplied by the host on a handle is preserved as
188
+ `details.isolation`, not interpreted as a foreground apply failure.
172
189
 
173
190
  `workpool` takes exactly one of `{category, prompt, model?}` or
174
191
  `{subagent_type, prompt, model?}` as its plain-data agent spec. Mode is `fresh`
@@ -203,22 +220,28 @@ without a `summary` fails with a teaching error.
203
220
  `eval` accepts `on_timeout: "detach"|"error"`. The default is `"detach"` in
204
221
  interactive TUI, RPC, and app-server sessions; print and JSON one-shot runs
205
222
  default to `"error"` so their result is never silently detached. A detached
206
- cell keeps only its own language kernel busy. A new same-language call returns
207
- a busy error with its cell id and output tail; calls in other languages continue
208
- normally. Do not re-run the cell.
223
+ cell keeps only its own language kernel busy. New same-language cells are admitted
224
+ into its FIFO queue; calls in other languages continue normally. Queued cells may
225
+ also detach, within the global `maxDetachedCells` cap. At capacity, the first idle
226
+ detach attempt leaves the cell foreground and re-arms one wait for the remaining
227
+ submission-based foreground window. At the window, it detaches if a slot is now
228
+ free; otherwise it settles `cancelled` with `eval_background_capacity_reached`,
229
+ listing the live cells and a stop-or-wait remedy. Cells that complete inside the
230
+ window return normally. Cancelling a queued cell never interrupts its predecessor.
231
+ Do not re-run a detached or queued cell; each detached cell completes as one notification.
209
232
 
210
233
  Queued steering also detaches an eligible interactive foreground call, including
211
234
  one paused in a host tool bridge, without cancelling its computation. If the
212
- language's detached slot is occupied, steering leaves the call waiting. Follow-up
235
+ global detached cap is reached, steering leaves the call waiting. Follow-up
213
236
  messages, explicit `on_timeout: "error"`, and print/JSON calls do not trigger this
214
237
  transition; caller abort and existing deadlines retain their cancellation behavior.
215
238
 
216
239
  Every cell, detached or not, is bounded by two kill deadlines. The run budget
217
240
  (`runBudgetSeconds`, or the call's `timeout`) charges only the cell's own
218
- execution time and is paused while a host tool call is in flight, so a cell
241
+ execution time and is paused while queued or while a host tool call is in flight, so a cell
219
242
  waiting on `agent()` survives while a runaway child process or loop does not.
220
243
  The hard limit (`hardLimitSeconds`, raised by a larger `timeout`) is wall-clock
221
- and bounds parked cells too. A cell killed by either deadline reports which one
244
+ from submission and bounds queued and parked cells too. A cell killed by either deadline reports which one
222
245
  in its result or completion notification, together with whether kernel state
223
246
  survived; the tool schema states the configured numbers. The `timeout` value
224
247
  never changes the idle detach deadline: that is `cellTimeoutSeconds` capped by
@@ -227,9 +250,24 @@ never changes the idle detach deadline: that is `cellTimeoutSeconds` capped by
227
250
  While any cell is detached, the interactive footer shows a highlighted
228
251
  `↗ <language> · <summary>` status on the extension status line (the cell id
229
252
  when the call had no summary), clearing as soon as the last detached cell settles.
253
+ Queued entries are labelled `queued`; an all-queued footer shows `(queued)` instead
254
+ of an elapsed duration. Elapsed time counts only from execution start.
255
+
256
+ Use `eval({ action: "list" })` without a language or cell id to see live and
257
+ recently settled cells across languages. Each line includes the id, language,
258
+ state, elapsed execution seconds, queue predecessors, and summary (or a short code
259
+ preview); `details.cells` carries the typed cell metadata. Listing never consumes
260
+ completion notifications.
261
+
262
+ A run with `reset: true` resets only its selected language and is refused with
263
+ `eval_kernel_busy_reset_refused` while any other cell in that language is live,
264
+ including queued cells. The requesting cell fails without changing the kernel or
265
+ cancelling existing work. Stop those cells explicitly or wait for their completion
266
+ notifications before resetting; live cells in other languages do not block reset.
230
267
 
231
268
  Use `eval({ action: "peek", cell_id })` for its state and buffered output, or
232
- `eval({ action: "stop", cell_id })` to cancel it. Python stop interrupts the
269
+ `eval({ action: "stop", cell_id })` to cancel it. Stopping a queued cell removes it
270
+ without interrupting the active cell; kernel state is retained. Python running-cell stop interrupts the
233
271
  existing kernel and preserves variables. JavaScript stop is cooperative first:
234
272
  the worker rejects the cell's pending bridge `tool.*` calls and kills the
235
273
  `Bun.spawn` children it started, and a cell that settles within the 2 s grace
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.21-2",
3
+ "version": "2026.9.22",
4
4
  "private": true,
5
5
  "description": "Source-only senpi extension package for codemode evaluation tools",
6
6
  "type": "module",
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@babel/parser": "8.0.4",
34
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.21-2",
34
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.22",
35
35
  "typebox": "1.3.34"
36
36
  },
37
37
  "peerDependencies": {
38
- "@code-yeongyu/senpi": "2026.9.21-2"
38
+ "@code-yeongyu/senpi": "2026.9.22"
39
39
  },
40
40
  "devDependencies": {
41
- "@code-yeongyu/senpi": "2026.9.21-2"
41
+ "@code-yeongyu/senpi": "2026.9.22"
42
42
  },
43
43
  "keywords": [
44
44
  "senpi",
@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@code-yeongyu/senpi";
2
2
  import { type Static, Type } from "typebox";
3
3
  import { Check, Errors } from "typebox/value";
4
4
  import type { EvalStatusEvent, ExecuteTool } from "../tool/types.ts";
5
+ import type { EvalSchemaToolInfo } from "./schema-bridge.ts";
5
6
 
6
7
  const agentArgsSchema = Type.Object(
7
8
  {
@@ -14,7 +15,7 @@ const agentArgsSchema = Type.Object(
14
15
  tools: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
15
16
  isolated: Type.Optional(Type.Boolean()),
16
17
  apply: Type.Optional(Type.Boolean()),
17
- merge: Type.Optional(Type.Boolean()),
18
+ merge: Type.Optional(Type.Union([Type.Boolean(), Type.Literal("patch"), Type.Literal("branch")])),
18
19
  },
19
20
  { additionalProperties: false },
20
21
  );
@@ -43,6 +44,9 @@ type TaskParams = {
43
44
  readonly name?: string;
44
45
  readonly run_in_background: boolean;
45
46
  readonly tools?: readonly string[];
47
+ readonly isolated?: boolean;
48
+ readonly apply?: boolean;
49
+ readonly merge?: "patch" | "branch";
46
50
  };
47
51
  type ProgressContext = { readonly fallbackId: string; readonly warning?: string };
48
52
 
@@ -54,15 +58,32 @@ export interface RunEvalAgentOptions {
54
58
  readonly callId: string;
55
59
  readonly taskToolName: string;
56
60
  readonly executeTool: AgentExecuteTool;
61
+ readonly listTools?: () => readonly EvalSchemaToolInfo[];
57
62
  readonly signal?: AbortSignal;
58
63
  readonly emitStatus?: (event: EvalStatusEvent) => void;
59
64
  }
60
65
 
61
- export type EvalAgentResult =
66
+ export type EvalAgentResult = (
62
67
  | { readonly text: string }
63
68
  | { readonly text: string; readonly data: unknown }
64
69
  | { readonly text: string; readonly parseError: string }
65
- | { readonly text: string; readonly id: string; readonly handle: string; readonly run_epoch: number };
70
+ | { readonly text: string; readonly id: string; readonly handle: string; readonly run_epoch: number }
71
+ ) & { readonly details?: { readonly isolation: Readonly<Record<string, unknown>> } };
72
+
73
+ // The session-owned executor identifies a bridge across JS and HTTP calls; do not retain retired sessions.
74
+ const isolationCapabilities = new WeakMap<AgentExecuteTool, Map<string, boolean>>();
75
+
76
+ export class AgentIsolationNotAppliedError extends Error {
77
+ readonly name = "AgentIsolationNotAppliedError";
78
+ readonly code = "isolation_not_applied";
79
+
80
+ constructor(isolation: Readonly<Record<string, unknown>>) {
81
+ const recovery = ["patch_path", "branch_name", "manual_command"]
82
+ .flatMap((key) => (typeof isolation[key] === "string" ? [`${key}: ${isolation[key]}`] : []))
83
+ .join("; ");
84
+ super(`agent() isolated changes were not applied${recovery ? `; ${recovery}` : ""}`);
85
+ }
86
+ }
66
87
 
67
88
  class AgentArgumentsError extends Error {
68
89
  readonly name = "AgentArgumentsError";
@@ -92,7 +113,8 @@ class AgentHandleError extends Error {
92
113
  export async function runEvalAgent(args: unknown, options: RunEvalAgentOptions): Promise<EvalAgentResult> {
93
114
  const parsed = parseAgentArgs(args);
94
115
  const structured = Object.hasOwn(parsed, "schema");
95
- const warning = droppedOptionsWarning(parsed);
116
+ const supportsIsolation = taskSupportsIsolation(options);
117
+ const warning = supportsIsolation ? undefined : droppedOptionsWarning(parsed);
96
118
  const fallbackId = parsed.label ?? options.callId;
97
119
  if (warning) options.emitStatus?.({ op: "agent", id: fallbackId, status: "running", warning });
98
120
 
@@ -104,7 +126,7 @@ export async function runEvalAgent(args: unknown, options: RunEvalAgentOptions):
104
126
 
105
127
  let result: AgentToolResult<unknown>;
106
128
  try {
107
- result = await options.executeTool(options.taskToolName, toTaskParams(parsed, structured), {
129
+ result = await options.executeTool(options.taskToolName, toTaskParams(parsed, structured, supportsIsolation), {
108
130
  ...(options.signal ? { signal: options.signal } : {}),
109
131
  ...(options.emitStatus
110
132
  ? {
@@ -119,12 +141,15 @@ export async function runEvalAgent(args: unknown, options: RunEvalAgentOptions):
119
141
  }
120
142
 
121
143
  const text = resultText(result);
144
+ const isolation =
145
+ isRecord(result.details) && isRecord(result.details.isolation) ? result.details.isolation : undefined;
146
+ const metadata = isolation === undefined ? {} : { details: { isolation } };
122
147
  if (parsed.handle === true) {
123
148
  const { task_id: id, run_epoch } = resultTaskHandle(result);
124
- return { text, id, handle: `agent://${id}`, run_epoch };
149
+ return { text, id, handle: `agent://${id}`, run_epoch, ...metadata };
125
150
  }
126
- if (!structured) return { text };
127
- return parseStructuredText(text);
151
+ if (isolation?.changes_applied === false) throw new AgentIsolationNotAppliedError(isolation);
152
+ return { ...(structured ? parseStructuredText(text) : { text }), ...metadata };
128
153
  }
129
154
 
130
155
  function parseAgentArgs(value: unknown): AgentArgs {
@@ -135,7 +160,21 @@ function parseAgentArgs(value: unknown): AgentArgs {
135
160
  throw new AgentArgumentsError(summary || "invalid value");
136
161
  }
137
162
 
138
- function toTaskParams(args: AgentArgs, structured: boolean): TaskParams {
163
+ function taskSupportsIsolation(options: RunEvalAgentOptions): boolean {
164
+ let capabilities = isolationCapabilities.get(options.executeTool);
165
+ if (!capabilities) {
166
+ capabilities = new Map();
167
+ isolationCapabilities.set(options.executeTool, capabilities);
168
+ }
169
+ const cached = capabilities.get(options.taskToolName);
170
+ if (cached !== undefined) return cached;
171
+ const schema = options.listTools?.().find((tool) => tool.name === options.taskToolName)?.parameters;
172
+ const supported = isRecord(schema) && isRecord(schema.properties) && schema.properties.isolated !== undefined;
173
+ capabilities.set(options.taskToolName, supported);
174
+ return supported;
175
+ }
176
+
177
+ function toTaskParams(args: AgentArgs, structured: boolean, supportsIsolation: boolean): TaskParams {
139
178
  return {
140
179
  prompt: structured
141
180
  ? `${args.prompt}\n\nRespond ONLY with JSON matching this JSON-Schema:\n${JSON.stringify(args.schema)}`
@@ -145,6 +184,11 @@ function toTaskParams(args: AgentArgs, structured: boolean): TaskParams {
145
184
  ...(args.label === undefined ? {} : { name: args.label }),
146
185
  run_in_background: args.handle === true,
147
186
  ...(args.tools === undefined ? {} : { tools: args.tools }),
187
+ ...(supportsIsolation && args.isolated !== undefined ? { isolated: args.isolated } : {}),
188
+ ...(supportsIsolation && args.apply !== undefined ? { apply: args.apply } : {}),
189
+ ...(supportsIsolation && args.merge !== undefined
190
+ ? { merge: args.merge === true ? "branch" : args.merge === false ? "patch" : args.merge }
191
+ : {}),
148
192
  };
149
193
  }
150
194
 
@@ -35,6 +35,7 @@ export async function runReservedTool(toolName: string, context: ReservedDispatc
35
35
  callId: context.callId,
36
36
  taskToolName: context.taskToolName,
37
37
  executeTool: context.executeTool,
38
+ ...(context.listTools === undefined ? {} : { listTools: context.listTools }),
38
39
  ...(context.signal === undefined ? {} : { signal: context.signal }),
39
40
  emitStatus: context.emitStatus,
40
41
  });
@@ -22,6 +22,7 @@ export const codemodeSettingsSchema = Type.Object(
22
22
  foregroundWindowSeconds: Type.Optional(Type.Number({ minimum: 1 })),
23
23
  runBudgetSeconds: Type.Optional(Type.Number({ minimum: 1 })),
24
24
  hardLimitSeconds: Type.Optional(Type.Number({ minimum: 1 })),
25
+ maxDetachedCells: Type.Optional(Type.Number({ minimum: 1 })),
25
26
  parallelPoolWidth: Type.Optional(Type.Number({ minimum: 1 })),
26
27
  taskTools: Type.Optional(
27
28
  Type.Object(
@@ -80,6 +81,8 @@ export interface CodemodeSettings {
80
81
  readonly runBudgetSeconds: number;
81
82
  /** Wall-clock kill deadline for a single cell; bounds detached and bridge-parked cells too. */
82
83
  readonly hardLimitSeconds: number;
84
+ /** Maximum detached cells across all language kernels. */
85
+ readonly maxDetachedCells?: number;
83
86
  readonly parallelPoolWidth: number;
84
87
  readonly taskTools?: CodemodeTaskTools;
85
88
  readonly outputSink?: CodemodeOutputSink;
@@ -87,6 +90,7 @@ export interface CodemodeSettings {
87
90
  }
88
91
 
89
92
  export type ResolvedCodemodeSettings = CodemodeSettings & {
93
+ readonly maxDetachedCells: number;
90
94
  readonly taskTools: CodemodeTaskTools;
91
95
  readonly outputSink: CodemodeOutputSink;
92
96
  readonly statusEvents: boolean;
@@ -129,7 +133,10 @@ export const FOREGROUND_WINDOW_ENVIRONMENT_FLAG = "SENPI_CODEMODE_FOREGROUND_SEC
129
133
  */
130
134
  export const DEFAULT_RUN_BUDGET_SECONDS = 300;
131
135
 
136
+ export const DEFAULT_MAX_DETACHED_CELLS = 15;
137
+
132
138
  export const RUN_BUDGET_ENVIRONMENT_FLAG = "SENPI_CODEMODE_RUN_BUDGET_SECONDS";
139
+ export const MAX_DETACHED_CELLS_ENVIRONMENT_FLAG = "SENPI_CODEMODE_MAX_DETACHED_CELLS";
133
140
 
134
141
  // OMP settings-schema.ts:3211-3299 has language/path settings only; eval.ts:427
135
142
  // defaults timeout to 30s, and codemode pins concurrency-bridge.ts:30 width to 4.
@@ -144,6 +151,7 @@ export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
144
151
  foregroundWindowSeconds: DEFAULT_FOREGROUND_WINDOW_SECONDS,
145
152
  runBudgetSeconds: DEFAULT_RUN_BUDGET_SECONDS,
146
153
  hardLimitSeconds: DEFAULT_HARD_LIMIT_SECONDS,
154
+ maxDetachedCells: DEFAULT_MAX_DETACHED_CELLS,
147
155
  parallelPoolWidth: 4,
148
156
  taskTools: {
149
157
  task: "task",
@@ -207,6 +215,15 @@ export function resolveRunBudgetSeconds(settings: CodemodeSettings, env: Environ
207
215
  return positiveSecondsOverride(env[RUN_BUDGET_ENVIRONMENT_FLAG]) ?? settings.runBudgetSeconds;
208
216
  }
209
217
 
218
+ /** Uses the same positive-integer environment parsing as the run budget. */
219
+ export function resolveMaxDetachedCells(settings: CodemodeSettings, env: Environment = process.env): number {
220
+ return (
221
+ positiveSecondsOverride(env[MAX_DETACHED_CELLS_ENVIRONMENT_FLAG]) ??
222
+ settings.maxDetachedCells ??
223
+ DEFAULT_MAX_DETACHED_CELLS
224
+ );
225
+ }
226
+
210
227
  function positiveSecondsOverride(value: string | undefined): number | undefined {
211
228
  if (value === undefined) return undefined;
212
229
  const parsed = Number.parseInt(value, 10);
@@ -250,6 +267,7 @@ function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
250
267
  foregroundWindowSeconds: input.foregroundWindowSeconds ?? defaultCodemodeSettings.foregroundWindowSeconds,
251
268
  runBudgetSeconds: input.runBudgetSeconds ?? defaultCodemodeSettings.runBudgetSeconds,
252
269
  hardLimitSeconds: input.hardLimitSeconds ?? defaultCodemodeSettings.hardLimitSeconds,
270
+ maxDetachedCells: input.maxDetachedCells ?? DEFAULT_MAX_DETACHED_CELLS,
253
271
  parallelPoolWidth: input.parallelPoolWidth ?? defaultCodemodeSettings.parallelPoolWidth,
254
272
  taskTools: {
255
273
  task: input.taskTools?.task ?? defaultCodemodeSettings.taskTools.task,
@@ -28,7 +28,8 @@ function packLabels(labels: readonly string[], budget: number): string {
28
28
  }
29
29
 
30
30
  function labelOf(entry: EvalDetachedCellStatusEntry): string {
31
- return entry.summary === undefined || entry.summary.length === 0 ? entry.cellId : entry.summary;
31
+ const label = entry.summary === undefined || entry.summary.length === 0 ? entry.cellId : entry.summary;
32
+ return entry.queuedBehind === undefined ? label : `queued ${label}`;
32
33
  }
33
34
 
34
35
  /**
@@ -52,10 +53,12 @@ export function formatElapsedSeconds(value: number): string {
52
53
  return `${hours}h ${remainingMinutes}m`;
53
54
  }
54
55
 
55
- /** Whole seconds since the oldest detached cell was created; never negative on clock skew. */
56
+ /** Whole seconds since the oldest executing detached cell started; queued cells do not tick. */
56
57
  export function evalCellElapsedSeconds(entries: readonly EvalDetachedCellStatusEntry[], nowMs: number): number {
57
58
  let oldest = Number.POSITIVE_INFINITY;
58
- for (const entry of entries) oldest = Math.min(oldest, entry.startedAtMs);
59
+ for (const entry of entries) {
60
+ if (entry.queuedBehind === undefined) oldest = Math.min(oldest, entry.startedAtMs);
61
+ }
59
62
  if (!Number.isFinite(oldest)) return 0;
60
63
  return Math.max(0, Math.round((nowMs - oldest) / 1000));
61
64
  }
@@ -67,7 +70,9 @@ export function formatEvalCellStatus(
67
70
  ): string | undefined {
68
71
  const first = entries[0];
69
72
  if (first === undefined) return undefined;
70
- const suffix = ` (${formatElapsedSeconds(evalCellElapsedSeconds(entries, nowMs))})`;
73
+ const suffix = entries.every((entry) => entry.queuedBehind !== undefined)
74
+ ? " (queued)"
75
+ : ` (${formatElapsedSeconds(evalCellElapsedSeconds(entries, nowMs))})`;
71
76
  if (entries.length === 1) {
72
77
  const head = `${DETACHED_GLYPH} ${first.language} · `;
73
78
  return head + truncateEnd(labelOf(first), MAX_STATUS_LENGTH - head.length - suffix.length) + suffix;
@@ -7,6 +7,7 @@ import {
7
7
  defaultCodemodeSettings,
8
8
  resolveForegroundWindowSeconds,
9
9
  resolveHardLimitSeconds,
10
+ resolveMaxDetachedCells,
10
11
  resolveRunBudgetSeconds,
11
12
  } from "./config/settings.ts";
12
13
  import { EvalNotifier } from "./extension/eval-notifier.ts";
@@ -181,6 +182,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
181
182
  settings: defaultCodemodeSettings,
182
183
  cellManager: new EvalDetachedCellManager({
183
184
  notifier,
185
+ maxDetachedCells: resolveMaxDetachedCells(defaultCodemodeSettings),
184
186
  hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
185
187
  runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
186
188
  onStatusChange: showDetachedCells,
@@ -219,6 +221,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
219
221
  const cellManager = new EvalDetachedCellManager({
220
222
  artifactsDir: runtime.artifactsDir,
221
223
  notifier,
224
+ maxDetachedCells: resolveMaxDetachedCells(runtime.settings),
222
225
  hardLimitSeconds: resolveHardLimitSeconds(runtime.settings),
223
226
  runBudgetSeconds: resolveRunBudgetSeconds(runtime.settings),
224
227
  onStatusChange: showDetachedCells,
@@ -28,8 +28,8 @@ runner/prelude assets).
28
28
  AST-parsed (Babel) and rewritten to bridge-compatible dynamic imports.
29
29
  - JS runs on worker threads with an inline-worker fallback; py/rb/jl run as
30
30
  framed subprocesses through `shared/`.
31
- - Subprocess retirement/restart, worker recovery, timeout, and interrupt
32
- semantics live here, never in the tool layer.
31
+ - Each language keeps one serial FIFO kernel. Queued cancellation removes only
32
+ that cell; active interruption and runtime recovery stay in the kernel layer.
33
33
  - Every kernel exposes the active session's `PI_*` environment (`session-env.ts`):
34
34
  inherited values are deleted before the session's values are applied, so any
35
35
  child spawned from a cell sees the same session environment a bash-tool child
@@ -254,6 +254,12 @@ function output(ids...; format="raw", offset=nothing, limit=nothing)
254
254
  senpi_with_bridge_timeout_pause(() -> senpi_call_tool("__output__", arguments))
255
255
  end
256
256
 
257
+ """
258
+ Delegate work with isolated/apply/merge; needs a host that supports isolation, otherwise a warning.
259
+ merge accepts "patch"/"branch" (false/true aliases). Unapplied foreground changes raise
260
+ an error with recovery instructions. Handles return immediately: await the completion
261
+ notification or read task_output for the isolation result.
262
+ """
257
263
  function agent(prompt::AbstractString; agent="task", model=nothing, label=nothing, schema=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
258
264
  arguments = Dict{String, Any}("prompt" => string(prompt), "agent" => agent)
259
265
  for (key, value) in (("model", model), ("label", label), ("schema", schema), ("isolated", isolated), ("apply", apply), ("merge", merge))
@@ -272,6 +278,10 @@ function agent(prompt::AbstractString; agent="task", model=nothing, label=nothin
272
278
  result["handle"] = get(record, "handle", result["id"] === nothing ? nothing : "agent://" * string(result["id"]))
273
279
  result["run_epoch"] = get(record, "run_epoch", nothing)
274
280
  schema !== nothing && (result["data"] = parsed)
281
+ details = get(record, "details", nothing)
282
+ if details isa AbstractDict && haskey(details, "isolation")
283
+ result["details"] = Dict("isolation" => details["isolation"])
284
+ end
275
285
  result
276
286
  end
277
287