@code-yeongyu/senpi 2026.9.27-4 → 2026.9.28

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 (271) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/bundle/chunks/{app-server-command-3GVJZFFH.js → app-server-command-TOPTGVQB.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-4SUOOZZ4.js → azure-openai-responses-FCZNBRWE.js} +1 -1
  4. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  5. package/dist/bundle/chunks/{chunk-PSCNHY7D.js → chunk-3GMFUEU7.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-HBG6SQU5.js → chunk-62XHQXCZ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-SLL375O7.js → chunk-7DY2JPUX.js} +1 -1
  8. package/dist/bundle/chunks/chunk-7YUGCTEV.js +15 -0
  9. package/dist/bundle/chunks/{chunk-FWBSGMAE.js → chunk-A4UMPXQF.js} +1 -1
  10. package/dist/bundle/chunks/chunk-BBK6SSYT.js +2 -0
  11. package/dist/bundle/chunks/{chunk-C2425O2Q.js → chunk-CNQ4JB45.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-27IFXX3B.js → chunk-DRN4FQCE.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-22MGY5JQ.js → chunk-EHSZ7UCW.js} +1 -1
  14. package/dist/bundle/chunks/chunk-FHJYIU3U.js +5 -0
  15. package/dist/bundle/chunks/chunk-GJUCSTAG.js +19 -0
  16. package/dist/bundle/chunks/{chunk-VMKAYXHL.js → chunk-KIOTFUG6.js} +25 -20
  17. package/dist/bundle/chunks/{chunk-2BMD37YV.js → chunk-LUPPSEQW.js} +1 -1
  18. package/dist/bundle/chunks/chunk-OAIVTS5X.js +8 -0
  19. package/dist/bundle/chunks/{chunk-NRGMT74E.js → chunk-QOVUQZO4.js} +1 -1
  20. package/dist/bundle/chunks/chunk-SN4ZHPMN.js +135 -0
  21. package/dist/bundle/chunks/{chunk-IKXPBYSO.js → chunk-UJFLZ2M2.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-4T4GFAGK.js → chunk-VFSXUT7K.js} +1 -1
  23. package/dist/bundle/chunks/{cli-main-66NFBKVH.js → cli-main-ARR54ET6.js} +1 -1
  24. package/dist/bundle/chunks/{google-generative-ai-ZTGKZTGU.js → google-generative-ai-LFCBHG3Y.js} +1 -1
  25. package/dist/bundle/chunks/{google-vertex-CCZ376YK.js → google-vertex-C474FXU7.js} +1 -1
  26. package/dist/bundle/chunks/{help-fast-path-NZ3I2UEN.js → help-fast-path-3U3YDEZ6.js} +1 -1
  27. package/dist/bundle/chunks/{host-command-RORPAC5X.js → host-command-6CJ7YM6W.js} +1 -1
  28. package/dist/bundle/chunks/{host-lifecycle-UDMJGLOR.js → host-lifecycle-RZIMOFDM.js} +1 -1
  29. package/dist/bundle/chunks/host-lifecycle.js +6 -6
  30. package/dist/bundle/chunks/interactive-host-runtime-A3GEIFTU.js +4 -0
  31. package/dist/bundle/chunks/{interactive-mode-LOAADKLH.js → interactive-mode-ITSFLGF6.js} +1 -1
  32. package/dist/bundle/chunks/{list-tips-S5CANO5W.js → list-tips-FPXDXN7R.js} +1 -1
  33. package/dist/bundle/chunks/multi-session-host-B4WMZE2G.js +21 -0
  34. package/dist/bundle/chunks/{openai-codex-responses-YSGZZDD5.js → openai-codex-responses-2LDIFL6Z.js} +1 -1
  35. package/dist/bundle/chunks/{openai-completions-KWHRWCBN.js → openai-completions-7KZLPKYP.js} +1 -1
  36. package/dist/bundle/chunks/{openai-images-6U5FCN5S.js → openai-images-I7DR7ERP.js} +1 -1
  37. package/dist/bundle/chunks/{openai-responses-7PWMBGYX.js → openai-responses-LCHS3AF3.js} +1 -1
  38. package/dist/bundle/chunks/{openrouter-images-QIKWQZUD.js → openrouter-images-IPNDRK6C.js} +1 -1
  39. package/dist/bundle/chunks/{package-manager-cli-5EEDA35I.js → package-manager-cli-DXHQ6RKA.js} +1 -1
  40. package/dist/bundle/chunks/{rotation-stream-KZG4OVR7.js → rotation-stream-ZVJEGA2G.js} +1 -1
  41. package/dist/bundle/chunks/{rpc-mode-HOEA5NGQ.js → rpc-mode-PFPNP65F.js} +1 -1
  42. package/dist/bundle/chunks/session-picker-KOPOV6Q4.js +2 -0
  43. package/dist/bundle/chunks/session-worker.js +311 -302
  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/cli/args.js +1 -1
  48. package/dist/cli/args.js.map +1 -1
  49. package/dist/cli/host-command.d.ts +8 -1
  50. package/dist/cli/host-command.d.ts.map +1 -1
  51. package/dist/cli/host-command.js +66 -14
  52. package/dist/cli/host-command.js.map +1 -1
  53. package/dist/core/agent-session.d.ts +18 -1
  54. package/dist/core/agent-session.d.ts.map +1 -1
  55. package/dist/core/agent-session.js +119 -3
  56. package/dist/core/agent-session.js.map +1 -1
  57. package/dist/core/retry-fallback/candidates.d.ts +26 -0
  58. package/dist/core/retry-fallback/candidates.d.ts.map +1 -0
  59. package/dist/core/retry-fallback/candidates.js +41 -0
  60. package/dist/core/retry-fallback/candidates.js.map +1 -0
  61. package/dist/core/retry-fallback/circuit-probes.d.ts +30 -0
  62. package/dist/core/retry-fallback/circuit-probes.d.ts.map +1 -0
  63. package/dist/core/retry-fallback/circuit-probes.js +60 -0
  64. package/dist/core/retry-fallback/circuit-probes.js.map +1 -0
  65. package/dist/core/retry-fallback/circuit.d.ts +101 -0
  66. package/dist/core/retry-fallback/circuit.d.ts.map +1 -0
  67. package/dist/core/retry-fallback/circuit.js +175 -0
  68. package/dist/core/retry-fallback/circuit.js.map +1 -0
  69. package/dist/core/retry-fallback/controller-types.d.ts +58 -0
  70. package/dist/core/retry-fallback/controller-types.d.ts.map +1 -0
  71. package/dist/core/retry-fallback/controller-types.js +2 -0
  72. package/dist/core/retry-fallback/controller-types.js.map +1 -0
  73. package/dist/core/retry-fallback/controller.d.ts +21 -49
  74. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  75. package/dist/core/retry-fallback/controller.js +88 -42
  76. package/dist/core/retry-fallback/controller.js.map +1 -1
  77. package/dist/core/sdk.d.ts.map +1 -1
  78. package/dist/core/sdk.js +25 -17
  79. package/dist/core/sdk.js.map +1 -1
  80. package/dist/core/session-failure-report.d.ts +33 -0
  81. package/dist/core/session-failure-report.d.ts.map +1 -0
  82. package/dist/core/session-failure-report.js +62 -0
  83. package/dist/core/session-failure-report.js.map +1 -0
  84. package/dist/core/settings-manager.d.ts +3 -0
  85. package/dist/core/settings-manager.d.ts.map +1 -1
  86. package/dist/core/settings-manager.js +4 -0
  87. package/dist/core/settings-manager.js.map +1 -1
  88. package/dist/core/slash-commands.d.ts.map +1 -1
  89. package/dist/core/slash-commands.js +1 -0
  90. package/dist/core/slash-commands.js.map +1 -1
  91. package/dist/index.d.ts +1 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +1 -1
  94. package/dist/index.js.map +1 -1
  95. package/dist/modes/index.d.ts +1 -0
  96. package/dist/modes/index.d.ts.map +1 -1
  97. package/dist/modes/index.js +2 -0
  98. package/dist/modes/index.js.map +1 -1
  99. package/dist/modes/interactive/help-content.d.ts.map +1 -1
  100. package/dist/modes/interactive/help-content.js +3 -0
  101. package/dist/modes/interactive/help-content.js.map +1 -1
  102. package/dist/modes/interactive/interactive-host-attach.d.ts +10 -0
  103. package/dist/modes/interactive/interactive-host-attach.d.ts.map +1 -0
  104. package/dist/modes/interactive/interactive-host-attach.js +14 -0
  105. package/dist/modes/interactive/interactive-host-attach.js.map +1 -0
  106. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
  107. package/dist/modes/interactive/interactive-host-runtime.js +3 -6
  108. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
  109. package/dist/modes/interactive/interactive-mode.d.ts +7 -0
  110. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  111. package/dist/modes/interactive/interactive-mode.js +66 -1
  112. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  113. package/dist/modes/interactive/session-failure-info.d.ts +3 -0
  114. package/dist/modes/interactive/session-failure-info.d.ts.map +1 -0
  115. package/dist/modes/interactive/session-failure-info.js +21 -0
  116. package/dist/modes/interactive/session-failure-info.js.map +1 -0
  117. package/dist/modes/interactive/tips/catalog/model-tips.d.ts +7 -7
  118. package/dist/modes/interactive/tips/catalog/model-tips.d.ts.map +1 -1
  119. package/dist/modes/interactive/tips/catalog/model-tips.js +7 -1
  120. package/dist/modes/interactive/tips/catalog/model-tips.js.map +1 -1
  121. package/dist/modes/interactive/tips/catalog/session-tips.d.ts.map +1 -1
  122. package/dist/modes/interactive/tips/catalog/session-tips.js +1 -1
  123. package/dist/modes/interactive/tips/catalog/session-tips.js.map +1 -1
  124. package/dist/modes/rpc/host-attach-hold.d.ts +16 -0
  125. package/dist/modes/rpc/host-attach-hold.d.ts.map +1 -0
  126. package/dist/modes/rpc/host-attach-hold.js +9 -0
  127. package/dist/modes/rpc/host-attach-hold.js.map +1 -0
  128. package/dist/modes/rpc/host-cleanup-paths.d.ts +20 -0
  129. package/dist/modes/rpc/host-cleanup-paths.d.ts.map +1 -0
  130. package/dist/modes/rpc/host-cleanup-paths.js +13 -0
  131. package/dist/modes/rpc/host-cleanup-paths.js.map +1 -0
  132. package/dist/modes/rpc/host-client-occupancy.d.ts +24 -0
  133. package/dist/modes/rpc/host-client-occupancy.d.ts.map +1 -0
  134. package/dist/modes/rpc/host-client-occupancy.js +53 -0
  135. package/dist/modes/rpc/host-client-occupancy.js.map +1 -0
  136. package/dist/modes/rpc/host-daemon-paths.d.ts +61 -8
  137. package/dist/modes/rpc/host-daemon-paths.d.ts.map +1 -1
  138. package/dist/modes/rpc/host-daemon-paths.js +128 -11
  139. package/dist/modes/rpc/host-daemon-paths.js.map +1 -1
  140. package/dist/modes/rpc/host-daemon-registration.d.ts +11 -1
  141. package/dist/modes/rpc/host-daemon-registration.d.ts.map +1 -1
  142. package/dist/modes/rpc/host-daemon-registration.js +28 -15
  143. package/dist/modes/rpc/host-daemon-registration.js.map +1 -1
  144. package/dist/modes/rpc/host-endpoints.d.ts +12 -0
  145. package/dist/modes/rpc/host-endpoints.d.ts.map +1 -0
  146. package/dist/modes/rpc/host-endpoints.js +59 -0
  147. package/dist/modes/rpc/host-endpoints.js.map +1 -0
  148. package/dist/modes/rpc/host-ensure-lock.d.ts +11 -0
  149. package/dist/modes/rpc/host-ensure-lock.d.ts.map +1 -0
  150. package/dist/modes/rpc/host-ensure-lock.js +49 -0
  151. package/dist/modes/rpc/host-ensure-lock.js.map +1 -0
  152. package/dist/modes/rpc/host-ensure.d.ts +7 -0
  153. package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
  154. package/dist/modes/rpc/host-ensure.js +42 -65
  155. package/dist/modes/rpc/host-ensure.js.map +1 -1
  156. package/dist/modes/rpc/host-gc-evidence.d.ts +18 -0
  157. package/dist/modes/rpc/host-gc-evidence.d.ts.map +1 -0
  158. package/dist/modes/rpc/host-gc-evidence.js +128 -0
  159. package/dist/modes/rpc/host-gc-evidence.js.map +1 -0
  160. package/dist/modes/rpc/host-gc.d.ts +28 -0
  161. package/dist/modes/rpc/host-gc.d.ts.map +1 -0
  162. package/dist/modes/rpc/host-gc.js +110 -0
  163. package/dist/modes/rpc/host-gc.js.map +1 -0
  164. package/dist/modes/rpc/host-generations.d.ts +10 -2
  165. package/dist/modes/rpc/host-generations.d.ts.map +1 -1
  166. package/dist/modes/rpc/host-generations.js +12 -7
  167. package/dist/modes/rpc/host-generations.js.map +1 -1
  168. package/dist/modes/rpc/host-handoff.d.ts +6 -0
  169. package/dist/modes/rpc/host-handoff.d.ts.map +1 -1
  170. package/dist/modes/rpc/host-handoff.js +31 -4
  171. package/dist/modes/rpc/host-handoff.js.map +1 -1
  172. package/dist/modes/rpc/host-lifecycle-policy.d.ts +53 -0
  173. package/dist/modes/rpc/host-lifecycle-policy.d.ts.map +1 -0
  174. package/dist/modes/rpc/host-lifecycle-policy.js +64 -0
  175. package/dist/modes/rpc/host-lifecycle-policy.js.map +1 -0
  176. package/dist/modes/rpc/host-lifecycle.d.ts +1 -44
  177. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  178. package/dist/modes/rpc/host-lifecycle.js +23 -85
  179. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  180. package/dist/modes/rpc/host-observe-request.d.ts +17 -0
  181. package/dist/modes/rpc/host-observe-request.d.ts.map +1 -0
  182. package/dist/modes/rpc/host-observe-request.js +30 -0
  183. package/dist/modes/rpc/host-observe-request.js.map +1 -0
  184. package/dist/modes/rpc/host-probe.d.ts +12 -0
  185. package/dist/modes/rpc/host-probe.d.ts.map +1 -1
  186. package/dist/modes/rpc/host-probe.js +28 -1
  187. package/dist/modes/rpc/host-probe.js.map +1 -1
  188. package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
  189. package/dist/modes/rpc/host-protocol-info.d.ts.map +1 -1
  190. package/dist/modes/rpc/host-protocol-info.js +1 -0
  191. package/dist/modes/rpc/host-protocol-info.js.map +1 -1
  192. package/dist/modes/rpc/host-readiness.d.ts +22 -0
  193. package/dist/modes/rpc/host-readiness.d.ts.map +1 -0
  194. package/dist/modes/rpc/host-readiness.js +44 -0
  195. package/dist/modes/rpc/host-readiness.js.map +1 -0
  196. package/dist/modes/rpc/host-runner.d.ts +13 -0
  197. package/dist/modes/rpc/host-runner.d.ts.map +1 -1
  198. package/dist/modes/rpc/host-runner.js +23 -4
  199. package/dist/modes/rpc/host-runner.js.map +1 -1
  200. package/dist/modes/rpc/host-status-all.d.ts +31 -0
  201. package/dist/modes/rpc/host-status-all.d.ts.map +1 -0
  202. package/dist/modes/rpc/host-status-all.js +78 -0
  203. package/dist/modes/rpc/host-status-all.js.map +1 -0
  204. package/dist/modes/rpc/host-status-rows.d.ts +35 -0
  205. package/dist/modes/rpc/host-status-rows.d.ts.map +1 -0
  206. package/dist/modes/rpc/host-status-rows.js +42 -0
  207. package/dist/modes/rpc/host-status-rows.js.map +1 -0
  208. package/dist/modes/rpc/host-status.d.ts +32 -1
  209. package/dist/modes/rpc/host-status.d.ts.map +1 -1
  210. package/dist/modes/rpc/host-status.js +33 -27
  211. package/dist/modes/rpc/host-status.js.map +1 -1
  212. package/dist/modes/rpc/host-successor.d.ts +3 -1
  213. package/dist/modes/rpc/host-successor.d.ts.map +1 -1
  214. package/dist/modes/rpc/host-successor.js +105 -42
  215. package/dist/modes/rpc/host-successor.js.map +1 -1
  216. package/dist/modes/rpc/host-watchdog.d.ts +6 -0
  217. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
  218. package/dist/modes/rpc/host-watchdog.js +12 -13
  219. package/dist/modes/rpc/host-watchdog.js.map +1 -1
  220. package/dist/modes/rpc/loop-lag-watchdog.d.ts +16 -0
  221. package/dist/modes/rpc/loop-lag-watchdog.d.ts.map +1 -1
  222. package/dist/modes/rpc/loop-lag-watchdog.js +34 -2
  223. package/dist/modes/rpc/loop-lag-watchdog.js.map +1 -1
  224. package/dist/modes/rpc/multi-session-host.d.ts +2 -1
  225. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  226. package/dist/modes/rpc/multi-session-host.js +33 -6
  227. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  228. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  229. package/dist/modes/rpc/rpc-mode.js +2 -2
  230. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  231. package/dist/modes/rpc/rpc-types.d.ts +16 -0
  232. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-types.js.map +1 -1
  234. package/dist/modes/rpc/session-command-router.d.ts +9 -1
  235. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  236. package/dist/modes/rpc/session-command-router.js +4 -1
  237. package/dist/modes/rpc/session-command-router.js.map +1 -1
  238. package/dist/modes/rpc/session-path-key.d.ts +3 -1
  239. package/dist/modes/rpc/session-path-key.d.ts.map +1 -1
  240. package/dist/modes/rpc/session-path-key.js +3 -1
  241. package/dist/modes/rpc/session-path-key.js.map +1 -1
  242. package/docs/rpc.md +186 -19
  243. package/docs/settings.md +25 -1
  244. package/docs/usage.md +5 -2
  245. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
  246. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  247. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  248. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  249. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  250. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts +6 -0
  251. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts.map +1 -1
  252. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js +36 -0
  253. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js.map +1 -1
  254. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts +1 -0
  255. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  256. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +14 -1
  257. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  258. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  259. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  260. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  261. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  262. package/package.json +7 -7
  263. package/dist/bundle/chunks/chunk-3DEXCYNN.js +0 -135
  264. package/dist/bundle/chunks/chunk-5X4KSPK5.js +0 -12
  265. package/dist/bundle/chunks/chunk-KMD5ZONU.js +0 -5
  266. package/dist/bundle/chunks/chunk-PEHZYGYA.js +0 -8
  267. package/dist/bundle/chunks/chunk-PRC5XIIH.js +0 -2
  268. package/dist/bundle/chunks/chunk-QKY33652.js +0 -18
  269. package/dist/bundle/chunks/interactive-host-runtime-TIFT2SKR.js +0 -4
  270. package/dist/bundle/chunks/multi-session-host-GRHKB7Q2.js +0 -21
  271. package/dist/bundle/chunks/session-picker-FTFCQYHH.js +0 -2
package/docs/rpc.md CHANGED
@@ -166,6 +166,9 @@ parsing a version string:
166
166
  `core.extensions` (absolute roots, deduplicated and sorted). `profile_id` is the sha256 of the canonical JSON
167
167
  of `core` with its keys in that sorted order (`extensions`, `multi_session`, `session_runtime`), so any client
168
168
  can recompute it and compare two hosts without comparing paths.
169
+ - `memory_pressure` - multi-session hosts only: `true` while this host's memory sampler reads its RSS above
170
+ `SENPI_RPC_HOST_RSS_WARN_MB`, else `false`. Not identity and never an admission signal; `senpi host status`
171
+ reports it per endpoint. Classic hosts and hosts released before the field omit it.
169
172
 
170
173
  Compatibility is decided from `protocolVersion` + `capabilities`, and "is my build newer?" from `engineOrdinal`.
171
174
  A `serverVersion` string comparison is never a compatibility test: two hosts with different version strings can
@@ -306,6 +309,7 @@ directory can never read each other's state:
306
309
  <agentDir>/rpc-host-daemon/ flat directory (shared; also a legacy host's own state)
307
310
  layout.json { "layout": 2, "dir": "<sha256(canonical socket)[:16]>" }
308
311
  <sha256(canonical socket)[:16]>/ 0700
312
+ endpoint.json { layout: 2, socket, created_at } - durable identity
309
313
  host.pid POINTER: { layout, instance_id, generation_dir, writer }
310
314
  settings.json what the supervisor reads at boot
311
315
  daemon.lock stderr.log
@@ -317,9 +321,15 @@ directory can never read each other's state:
317
321
  reservations/ cross-generation session-path claims
318
322
  ```
319
323
 
320
- Directories are `0700` and every state file is `0600`. The canonical socket is the socket path on POSIX
321
- and the normalized lower-cased path on win32, so a client recomputes the directory name from the socket
322
- alone. `generation_dir` is relative to the directory holding the pointer (`generations/<instanceId>`), and
324
+ Directories are `0700` and every state file is `0600`. The canonical socket is, on POSIX, the socket path
325
+ with its directory resolved through its deepest existing ancestor (`realpath`; a missing tail is kept
326
+ verbatim) - the same identity the ensure lock is keyed by - and on win32 the normalized lower-cased path, so
327
+ a client recomputes the directory name from the socket alone and every spelling of one socket (`/tmp` vs
328
+ `/private/tmp`, a path through a symlinked directory) shares one directory, one registration and one lock.
329
+ A socket already spelled canonically hashes exactly as it did when the name was taken from the spelling
330
+ itself, so an existing endpoint keeps its directory. A directory an older build named after a
331
+ non-canonical spelling is still listed by `status --all` and collected by `gc`, but new state goes to the
332
+ canonical directory. `generation_dir` is relative to the directory holding the pointer (`generations/<instanceId>`), and
323
333
  `instance_id` is the same id the host reports as `instanceId` in `get_protocol_info` - so a pointer that
324
334
  names a different id than the socket answers describes a generation that is no longer serving.
325
335
 
@@ -335,7 +345,20 @@ names is alive an ensure refuses (`legacy_host`) rather than starting a second h
335
345
  `ensureHost` fails with a typed `HostDaemonStateError` naming the directory it could not create or write,
336
346
  and starts no host in that case.
337
347
 
338
- The directory is PRUNED of what is no longer running on every registration write and on every `host status`:
348
+ `endpoint.json` names the socket the directory serves, in the spelling its first ensure used (its name
349
+ is the hash of that spelling's canonical form). It is
350
+ written `0600` when the directory is created and re-asserted under the ensure lock. It is written whole to a
351
+ temporary name and linked into place, so no reader ever sees half a file, and a valid one is never rewritten, so
352
+ `created_at` is the endpoint's first ensure. Under the lock the ensure does replace a file that does not name a
353
+ socket hashing to this directory (torn by a crash of an older build, or foreign): left alone, such a file would
354
+ leave the endpoint listed as `socket: null` and kept by `gc` as `unknown_identity` forever. It is the one file a generation's release
355
+ leaves behind: a supervisor that exits (idle, drained, or after its host child crashed) removes the pointer,
356
+ `settings.json` and its generation directory, and without `endpoint.json` such an endpoint could not even be
357
+ enumerated. `stderr.log` and `crashes.jsonl` stay too. The only thing that ever removes an endpoint directory
358
+ (`endpoint.json` included) is the explicit `senpi host gc` below, and only on proof that nothing runs behind it.
359
+
360
+ The directory is PRUNED of what is no longer running on every registration write and on every single-socket
361
+ `host status` (never by `host status --all`):
339
362
  a `generations/<instanceId>/` whose record names a pid nobody is running is removed, the pointer goes with it
340
363
  while it still names one, and claims in `reservations/` whose owner is gone are removed too. A record that
341
364
  cannot be parsed is left alone - an ensure writing one right now must not be mistaken for a generation that
@@ -370,10 +393,12 @@ Everything above is reachable from one command, so a terminal, a desktop and a t
370
393
  daemon the same way instead of each re-implementing the decision:
371
394
 
372
395
  ```
373
- senpi host ensure [--json] [--launch-spec <file>] [--policy upgrade|fallback|never] [--socket <path>]
374
- senpi host status [--json] [--include-workers] [--socket <path>]
375
- senpi host stop [--json] [--drain] [--force] [--socket <path>]
376
- senpi host handoff [--json] [--launch-spec <file>] [--socket <path>]
396
+ senpi host ensure [--json] [--launch-spec <file>] [--policy upgrade|fallback|never] [--socket <path>]
397
+ senpi host status [--json] [--include-workers] [--all] [--socket <path>]
398
+ senpi host stop [--json] [--drain] [--force] [--socket <path>]
399
+ senpi host handoff [--json] [--launch-spec <file>] [--socket <path>]
400
+ senpi host shard-path --kind <p|i> --owner <id> [--root <dir>] [--json]
401
+ senpi host gc [--json] [--agent-dir <dir>]
377
402
  ```
378
403
 
379
404
  The contract is machine-first: EXACTLY ONE JSON line on stdout and nothing else, diagnostics on stderr,
@@ -388,7 +413,8 @@ and an exit code that classifies the outcome without parsing the line.
388
413
  | `4` | fallback: `{ action: "fallback", reason, host }` - under `--policy fallback`, no host is better than this one |
389
414
 
390
415
  The socket is `--socket`, else `SENPI_RPC_SOCKET`, else `<agentDir>/rpc/rpc.sock`. `--json` is accepted for
391
- symmetry with other commands; the answer is always JSON.
416
+ symmetry with other commands; the answer is always JSON (the one exception is `shard-path` without `--json`,
417
+ which prints the bare socket path).
392
418
 
393
419
  - `ensure` prints `{ action, socket, pid, instanceId, generation, engineVersion, engineOrdinal,
394
420
  capabilities, launchProfileId, reused, upgradeable }`. `--policy upgrade` (the default) allows a
@@ -400,7 +426,8 @@ symmetry with other commands; the answer is always JSON.
400
426
  `senpi host handoff` command.
401
427
  - `status` prints `{ reachable, socket, pid, instanceId, generation, engineVersion, capabilities,
402
428
  launchProfile, sessions: { total, interactive, worker, retained, foreign_attached, foreign_retained },
403
- zombies, rss_mb, host_rss_mb, open_fds, env_keys, generations }` and exits 3 when nothing answers - with the same
429
+ zombies, rss_mb, host_rss_mb, open_fds, memory_pressure, env_keys, generations, crashes, shard, session_rows,
430
+ claims_live, claims }` and exits 3 when nothing answers - with the same
404
431
  field set, so a caller parses one shape and branches on one boolean. `sessions` is what `list_sessions`
405
432
  reports under the same flag, so `worker` stays `0` without `--include-workers`; `foreign_*` is the same
406
433
  count from the point of view of a client holding none of those sessions itself. `rss_mb`, `open_fds` and
@@ -413,12 +440,100 @@ symmetry with other commands; the answer is always JSON.
413
440
  still claims in `reservations/` - the one occupancy number that is observable for a generation which no
414
441
  longer answers on the socket. Records of generations that ended are pruned by the read itself, so a status
415
442
  never lists a dead pid.
443
+
444
+ The last five fields and `memory_pressure` are additive (a client that does not know them ignores them):
445
+ - `memory_pressure`: whether the ANSWERING generation's memory sampler currently reads its RSS above
446
+ `SENPI_RPC_HOST_RSS_WARN_MB` (the state its `host_memory_pressure` records announce), read from its
447
+ `get_protocol_info`; `null` when nothing answers or the host predates the field. It lives in the host process
448
+ alone, so a draining predecessor's state is not reported, and it is observability only: a pressured endpoint
449
+ admits every open.
450
+ - `crashes`: records in the endpoint's `crashes.jsonl` - supervised host children that died rather than
451
+ stopped; `0` when the file is absent.
452
+ - `shard`: `{ kind: "p" | "i", key }` when the socket's basename is `<kind>-<16 hex>.sock` (the naming
453
+ contract below), else `null`.
454
+ - `session_rows`: under `--include-workers` only (else `[]`), every row of that same `list_sessions
455
+ { include_workers: true }` reply as `{ id, kind, session_path, attachments, context }`. `session_path` is
456
+ the host's canonical path, the key a client matches a session by; `context` is the published labels
457
+ including the host's own `host_socket`/`host_instance`, `null` where none were published.
458
+ - `claims_live`: session-path claims in `reservations/` whose owner process is still running, `0` when the
459
+ directory is absent.
460
+ - `claims`: under `--include-workers` only (else `[]`), every claim in `reservations/` whichever generation
461
+ wrote it, as `{ session_path, owner_pid, instance_id, generation, attached, live }` - so a path a
462
+ draining predecessor still holds is visible although the answering generation no longer lists it.
463
+ `generation` is the owner's ordinal while its generation record exists, else `null`; `attached` is `null`
464
+ for a claim written before that flag existed.
465
+ - `status --all` reports every endpoint the agent directory holds state for and ignores `--socket`:
466
+ `{ endpoints: [<status row>, ...] }`, each row the single-socket report above plus `dir` (the endpoint's
467
+ daemon directory) and `identity` (what named its socket: `endpoint` = `endpoint.json`, `settings` = the
468
+ boot `settings.json`, `generation-settings` = a generation's own `settings.json`; each accepted only when
469
+ that socket hashes to the directory it was found in). A directory none of them names is still listed with
470
+ `socket: null` and `identity: "unknown"`, built from the directory alone - the ensure lock is keyed by a
471
+ longer hash of the socket's transport address and cannot be rebuilt from the 16-hex name, so it can be
472
+ shown but never addressed. Before layout 2 (no `layout.json`) the answer is `{ "endpoints": [] }`. Unlike
473
+ the single-socket form, `--all` REMOVES NOTHING: a dead generation is a row with `alive: false`, an endpoint
474
+ whose host exited stays listed through its `endpoint.json` with `reachable: false` and `generations: []`,
475
+ and reclaiming what ended is left to the explicit, evidence-gated `gc`. Every endpoint is read at
476
+ once, each under its own budget (10 s per read), and a socket that does not answer the identity probe is
477
+ not asked for its session listing, so hung hosts cost about one budget in total; rows stay in directory order. Exit `0` while at least one endpoint answers, `3` when none does or none exists.
478
+ - `shard-path` computes a shard socket without contacting any host, so a client that does not link senpi
479
+ can check its own copy of the naming contract against the engine: `{ kind, key, socket }` under `--json`,
480
+ the socket path otherwise. `--root` defaults to `<agentDir>/rpc/shards`.
481
+ - `gc` removes the state of endpoints whose host is PROVABLY gone, in the agent directory (`--agent-dir`, else
482
+ the current one; `--socket` is ignored), and answers `{ removed: [{ socket, dir, reason }], kept: [{ socket,
483
+ dir, reason }] }` with exit `0` whatever it found (`2` for a bad flag). It never signals a process and never
484
+ runs implicitly - not inside `ensure`, not inside `status`. For each endpoint whose socket is known it takes
485
+ that socket's ENSURE lock (`<tmp>/senpi-rpc-host-locks/<sha256(transport address)[:32]>.lock`, the one
486
+ `ensureHost` serializes on - not `daemon.lock`; on POSIX the address is the socket path with its directory
487
+ realpath-canonicalized, so every spelling of one socket shares one lock) for at most 2 s, and inside it requires all three: (a) no
488
+ `generations/*/host.pid` names a live process (pid live and start time matching; a live pid whose identity
489
+ cannot be read counts as live; the generation the pointer names is one of them), (b) no claim in
490
+ `reservations/` has a live owner, and (c) connecting to the socket fails with `ENOENT`, or with `ECONNREFUSED`
491
+ on an entry that is a socket (a regular file where the socket should be is kept as `reachable`) - and so does
492
+ every `<socket>.next-*` successor bind beside it. Only then does it unlink the `<socket>.next-*` /
493
+ `<socket>.shield-*` siblings, then the socket, then remove the endpoint directory LAST, still under the lock;
494
+ `reason` is `socket_refused` or `socket_absent`. A sibling that is a directory is not unlinked: it stays and
495
+ the removed entry lists it as `skipped: [{ path, type: "directory" }]`. Because the directory goes last, a
496
+ removal that fails part-way leaves the endpoint listed for the next gc. Everything else is kept with its
497
+ reason: `live_generation`, `live_claim`, `reachable`, `locked` (an ensure held the lock for 2 s),
498
+ `legacy_layout` (a flat directory without `layout.json` - a legacy host's, never touched), `unknown_identity`
499
+ (nothing names the socket, so its lock cannot be taken), or `failed` (reading the evidence or removing threw;
500
+ the entry carries `error`) - and a `failed` endpoint never stops gc from going on to the others.
501
+ An `unknown_identity` directory is never removed by gc; remove it by hand only after checking that no pid in
502
+ its `generations/*/host.pid` or `reservations/*.json` is running and that no process holds files under it.
503
+ An ensure that raced a gc simply re-creates `endpoint.json` under the lock after gc released it.
416
504
  - `stop` is the I1 carve-out: a plain stop needs a validated pidfile AND `foreign_attached +
417
505
  foreign_retained == 0`, or it refuses with exit 3 and prints the counts it refused on; `--force`
418
506
  overrides after printing the same counts; `--drain` (SIGUSR1) is always permitted, because it ends no
419
507
  work.
420
508
  - `handoff` forces a generation handoff from THIS binary. A host that cannot drain and a platform that
421
- cannot rename answer alike: exit 3 `{ reason: "upgrade_unsupported", detail }`.
509
+ cannot rename answer alike: exit 3 `{ reason: "upgrade_unsupported", detail }`. The handoff runs inside the
510
+ endpoint's ensure lock (the one `ensure` and `gc` take), so an `ensure` arriving while the successor comes up
511
+ waits and then attaches to the successor, and `gc` reports the endpoint `locked` rather than judging it
512
+ halfway. An `ensure` waits for the lock as long as the longest holder can keep it: a probe plus either a
513
+ stop-and-restart or a handoff (probe, successor start, 30 s readiness). A REFUSED handoff leaves the endpoint
514
+ as it found it: the successor it started is killed and, once it has exited (up to 5 s), its generation record
515
+ and directory are released, so `status --all` never lists it; the boot `settings.json` it rewrote before
516
+ spawning is restored byte for byte. A cleanup that fails is reported in the refusal's `detail`.
517
+
518
+ #### Shard naming contract (`shardKey`, `shardSocketPath`)
519
+
520
+ A client may run many endpoints under one agent directory - omo one per parent session (`p`), the Desktop
521
+ one per thread (`i`), under `<agentDir>/rpc/shards/`. Every client derives the socket identically:
522
+
523
+ ```
524
+ key = sha256("<kind>:<ownerId>") in hex, first 16 characters
525
+ socket = <root>/<kind>-<key>.sock
526
+ ```
527
+
528
+ `packages/coding-agent/src/modes/rpc/host-daemon-paths.ts` exports `shardKey(kind, ownerId)`,
529
+ `shardSocketPathForKey(root, kind, key)` (no hashing) and `shardSocketPath(root, kind, ownerId)`. Fixed
530
+ vectors every implementation must reproduce:
531
+
532
+ | kind | owner | key | socket under `/r` |
533
+ |---|---|---|---|
534
+ | `p` | `01a0e28d-40e4-7402-bac7-8de6e76ad84c` | `6d410ba846ba1550` | `/r/p-6d410ba846ba1550.sock` |
535
+ | `i` | `thread-0001` | `da99f196e11b1cf9` | `/r/i-da99f196e11b1cf9.sock` |
536
+ | `p` | (empty) | `3ba7290d74188485` | `/r/p-3ba7290d74188485.sock` |
422
537
 
423
538
  #### Launch spec (`--launch-spec <file>`)
424
539
 
@@ -547,6 +662,15 @@ auth, model, extension or resource resolution, and they are never merged into th
547
662
  `^[a-z][a-z0-9_]*$`, every value at most 16 KiB, and at most 32 KiB of JSON in total. Anything else is refused with
548
663
  `invalid_session_context: <detail>`, where the detail names the cap that was broken.
549
664
 
665
+ A socket host also stamps its own identity into every session's context, overwriting any client-supplied
666
+ value for the same keys, because the host is the authority: `host_socket` is the PUBLIC endpoint clients
667
+ address (the supervisor's path for a supervised host, the bound path for a bare one), realpath-canonicalized
668
+ through the deepest existing ancestor of its directory (so the first generation of a shard, which starts before
669
+ `rpc/shards/` exists, and every successor stamp the same string), and stays the same across a generation handoff; `host_instance` is the answering generation's `instanceId`
670
+ (`get_protocol_info`) and changes with every handoff. So an extension can tell which endpoint and generation
671
+ it runs behind without an environment variable. `host_socket` is omitted where no public path exists (an
672
+ abstract socket, a supervised win32 host); a stdio host adds neither key.
673
+
550
674
  One shared host therefore loads ONE extension set and still lets an extension recognize the session it was loaded for
551
675
  (`pi.sessionKind`, `pi.sessionContext` - see
552
676
  [ExtensionAPI session identity](extensions.md#pisessionkind--pisessioncontext--pisharedhostenabled)). Probe
@@ -614,17 +738,34 @@ Environment overrides beat the file, and invalid values fall through to the next
614
738
  (`transient`|`persistent`) and `SENPI_RPC_HOST_IDLE_EXIT_MS` (positive integer milliseconds).
615
739
 
616
740
  The host exits only after the window elapses with NO attached client connections and NO active turns — continuously.
617
- Any connection or agent turn resets the window, so a busy host never exits. The supervisor learns about turns through
741
+ Any attached connection or agent turn resets the window, so a busy host never exits. A connection attaches with its
742
+ first request line, unless that line is an OBSERVING read: `get_protocol_info` or `list_sessions` carrying
743
+ `"observe": true`. A connection that only ever sends observing reads never counts and never resets the window, so a
744
+ poller (`senpi host status [--all]`, which marks both of its reads, a doctor loop, a runtime panel) can look at every
745
+ endpoint as often as it likes without keeping any of them alive; the first request that is anything else attaches the
746
+ connection from then on, and `observe` on any other command is ignored. While a connection has not sent its first
747
+ line yet the host does not exit, and the window keeps running. Hosts that predate the field ignore it, so a marked read
748
+ works against every generation. The supervisor learns about turns through
618
749
  its observer connection to the host; while that connection is unhealthy it cannot see turns, so it treats activity as
619
750
  unknown and keeps the host open as if a turn were running — but only for one idle window, during which it keeps
620
751
  reconnecting. An observer that stays unhealthy for longer than the window stops counting as busy, and the connection
621
752
  count alone decides from there. A `persistent` host has an infinite window and so keeps its infinite benefit of the
622
753
  doubt. The exit itself is clean: the RPC host
623
- receives SIGTERM first, flushes pending output, removes its socket, and the supervisor then removes `host.pid` and
624
- `settings.json` (the stderr log stays for diagnostics). After an idle exit, the next `ensureHost()` transparently
754
+ receives SIGTERM first, flushes pending output, removes its socket, and the supervisor then removes `settings.json`,
755
+ its generation directory and, last, the registration pointer (`host.pid`); the stderr log stays for diagnostics. The
756
+ crash-path cleanup the host runs when its supervisor dies uses the same order, so a missing pointer always means the
757
+ teardown is complete. After an idle exit, the next `ensureHost()` transparently
625
758
  starts a fresh host. `get_protocol_info` over the public socket behaves exactly as before; the supervisor is
626
759
  wire-transparent.
627
760
 
761
+ `ensureHost()` hands its caller an attach hold: the connection its readiness answer arrived on stays open and counts
762
+ as an attached client, so the idle window cannot close between the ensure and the caller's own attach, however slow
763
+ that attach is. Every caller must call `release()` on the returned `EnsuredHost` once its own client is
764
+ attached, or right away when it attaches later on its own connections; the idle window starts from there. Until then
765
+ the host counts the calling process as an attached client, so a transient host whose ensuring process is still running
766
+ never starts its idle window: a long-lived caller that ensures and never releases keeps the host up for its whole
767
+ lifetime. The hold never keeps the calling process alive, and the operating system ends it when that process exits.
768
+
628
769
  The RPC host can never outlive its supervisor. It is spawned with an extra inherited pipe on fd 3 whose write end the
629
770
  supervisor holds and never writes to; the kernel closes that end whenever the supervisor dies — including `SIGKILL`, an
630
771
  OOM kill, or a crash, where no signal handler runs — so the host reads EOF, shuts down cleanly and removes its private
@@ -729,7 +870,9 @@ What the host does enforce are lifecycle windows, and they only ever return memo
729
870
  - **Empty-host exit**: when the registry holds zero sessions AND no client is connected, continuously for
730
871
  `SENPI_RPC_HOST_EMPTY_EXIT_MS` (default 15 minutes), the host exits through its clean shutdown path (flush, socket
731
872
  removal), for stdio and `--listen` hosts alike. A connected client counts as occupancy even with no session open,
732
- so the host never drops a live socket under itself. Supervised hosts stay clean either way: a supervisor reads a
873
+ so the host never drops a live socket under itself - except a connection whose every request so far was an
874
+ observing read (`"observe": true`, the same rule as the idle-exit window above), which never holds the host open.
875
+ A connection that has not sent its first request yet still counts. Supervised hosts stay clean either way: a supervisor reads a
733
876
  child exit of 0 without a signal as an intentional idle stop and exits 0 with the same cleanup, not as a crash.
734
877
 
735
878
  Values are positive integers; invalid values fall through to the defaults. These lifecycle windows run inside the host process,
@@ -754,7 +897,11 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
754
897
  deadline (a busy host was measured answering after 57 s), and a timeout after it names the
755
898
  queue position instead of a bare deadline. A lost transport still rejects at once.
756
899
  `SENPI_RPC_LOOP_LAG_ERROR_MS` (default 5000) additionally broadcasts a `host_stalled` record
757
- (`{ type, driftMs, sessionId?, tool? }`) to every connection, like the other content-free lifecycle records.
900
+ (`{ type, driftMs, sessionId?, tool?, processCpuMs?, heapDeltaMb? }`) to every connection, like the other content-free lifecycle records.
901
+ `processCpuMs` is the process CPU time spent during the stalled window and `heapDeltaMb` the JS heap change across it, so a
902
+ stall explains itself: CPU close to `driftMs` means the host was busy (a large heap drop in the same window points at a
903
+ collection), and CPU close to zero means the process did not run at all (the machine starved it, or it sat in a blocking
904
+ wait). The stderr line carries the same two numbers as `cpu=<ms> heap=<+/-MB>`.
758
905
  - **Stall attribution**: each routed command is dispatched inside an `AsyncLocalStorage` scope carrying its routing
759
906
  `sessionId`, and an in-process session's tool executions open a span carrying `{ sessionId, tool }` for as long as
760
907
  the tool runs. A stall is blamed on the synchronous work that finished inside the measured window, or on the tool
@@ -766,7 +913,15 @@ REPORT: nothing here aborts a turn, kills a session, or refuses an `open_session
766
913
  minutes, and HALVES the idle-eviction window above while the host stays above the threshold, so idle sessions return
767
914
  their memory sooner. It is released as soon as RSS falls back under the threshold. Memory never refuses an open: the
768
915
  shared host has no resource caps, so every `open_session` is admitted whatever the host holds (#2207). Hosts released
769
- before #2207 refused NEW worker sessions above `SENPI_RPC_HOST_RSS_REFUSE_MB`; that variable is no longer read.
916
+ before #2207 had a second admission watermark; current hosts have no such admission path.
917
+ - **Per-endpoint pressure under sharding**: an agent directory may contain many independent hosts, so each endpoint
918
+ samples and reports its own memory pressure. `host_memory_pressure` describes only the host that emitted it; pressure
919
+ on one `p-*` or `i-*` endpoint does not gate opens on another endpoint. In `host status --all`, `rss_mb` is the
920
+ endpoint generation's whole process tree, `host_rss_mb` is the supervisor plus host processes, and `memory_pressure`
921
+ is whether the answering generation is above the threshold right now. Read them on the endpoint row (and the RSS
922
+ pair on its generation rows) rather than treating an aggregate agent-directory number as an admission
923
+ signal. The warning threshold remains observability-only: every endpoint continues to admit worker `open_session`
924
+ requests while pressured.
770
925
  - **Stall-proof dead-peer detection**: the socket dead-peer budget (30 s, `socket-event-fanout.ts`) counts only time
771
926
  the host loop actually SERVED. The loop-lag watchdog deposits each measured drift into a process-wide ledger
772
927
  (`loop-blocked-time.ts`) and the deadline re-arms for whatever blocked time landed inside its window, so a host that
@@ -798,7 +953,7 @@ absolute: **no blocking primitive, and no unbounded synchronous filesystem read.
798
953
  sleeps synchronously, or reads a large file synchronously inside an event handler freezes every other client's
799
954
  session on that host. Use the async API, and give genuinely CPU-bound work its own worker or child process.
800
955
  - The rule is observable rather than enforced at runtime: the stall watchdog above is what names the offender.
801
- `host_stalled { driftMs, sessionId, tool }` and the matching stderr line are how a blocking call in a session or a
956
+ `host_stalled { driftMs, sessionId, tool, processCpuMs, heapDeltaMb }` and the matching stderr line are how a blocking call in a session or a
802
957
  tool becomes a report instead of an unexplained freeze.
803
958
 
804
959
  ### Worker ownership and flow control
@@ -889,7 +1044,7 @@ containment, or containment of arbitrary native code. They are not an extension
889
1044
 
890
1045
  | Command | Params | Success data | Notes |
891
1046
  | --- | --- | --- | --- |
892
- | `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core } }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context` and `auto_title_per_session` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
1047
+ | `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi", instanceId: string, generation: number, engineVersion: string, engineOrdinal: [y, m, d, n, epoch], launch_profile: { profile_id, core }, memory_pressure?: boolean }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session`, `retain_on_disconnect`, `session_kind`, `session_context` and `auto_title_per_session` plus the negotiated launch capabilities. Those are HOST capabilities (a client never sends them) and are advertised only in multi-session mode, where the host owns the attachment refcount and the per-session launch profile. The identity fields are described under "Host identity" above; compatibility is decided from `protocolVersion`, `capabilities` and `engineOrdinal`, NEVER from `serverVersion`. |
893
1048
  | `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?`, `retain_on_disconnect?`, `kind?`, `context?`, `auto_title?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. `retain_on_disconnect: true` (default false) makes a dropped connection DETACH from this session instead of closing it — see "Retained sessions" below. `kind` (default `interactive`) and the opaque `context` map are described under "Session kind and context" above; both are stored frozen for the session's life and never influence auth, model or resource resolution. |
894
1049
  | `close_session` | `sessionId` | `{}` | Refused with `unknown_session` when the requesting connection never attached to that handle (a close releases the CALLER's attachment, and `list_sessions` publishes every handle). Otherwise aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then quarantines any worker that has not exited without releasing its path reservation; its response is the LAST record tagged with that handle for the first closer — no events after (test-pinned). An admitted concurrent close joins the same teardown and receives its own successful response; output saturation rejects admission with the bounded close-overflow/resync notice described above. |
895
1050
  | `list_sessions` | `include_workers?` (default false) | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status, attachments, kind, context? }] }` | Includes `opening`/`closing` entries. Internally quarantined workers remain externally `closing` until exit. `attachments` is the session's live client attachment count; `0` on an `open` row is a retained session with no client attached. Every row carries `kind`. Rows with `kind: "worker"` are omitted unless `include_workers: true`, and `context` is published ONLY on that listing — a default listing carries no `context` at all. |
@@ -1638,11 +1793,23 @@ Response:
1638
1793
  "tokens": 60000,
1639
1794
  "contextWindow": 200000,
1640
1795
  "percent": 30
1796
+ },
1797
+ "failures": {
1798
+ "requests": 6,
1799
+ "erroredRequests": 1,
1800
+ "abortedRequests": 0,
1801
+ "failureShare": 0.16666666666666666,
1802
+ "failedDurationMs": 4200,
1803
+ "postFailureRequests": 1,
1804
+ "postFailureFullMissRequests": 1,
1805
+ "postFailureFullMissInputTokens": 48000
1641
1806
  }
1642
1807
  }
1643
1808
  }
1644
1809
  ```
1645
1810
 
1811
+ `failures` reports what failed provider requests cost the session. Every assistant response is one request; `erroredRequests` and `abortedRequests` count responses that ended with stop reason `error` or `aborted`, and `failedDurationMs` sums their time from request start to the recorded response. `postFailureRequests` counts the first successful response after one or more failed ones within the same user turn (a user message resets it); `postFailureFullMissRequests` counts those that read nothing from the prompt cache on a prompt of at least 2048 tokens, and `postFailureFullMissInputTokens` sums their uncached prompt tokens (input plus cache writes). The token counts are the provider's usage report; attributing the cache miss to the preceding failure is an inference from the turn's sequence. Hosts older than this field omit it.
1812
+
1646
1813
  `tokens` and `cost` include assistant messages, usage reported by tools, and compaction/branch-summary generation across the full session. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.
1647
1814
 
1648
1815
  `contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.
package/docs/settings.md CHANGED
@@ -254,6 +254,8 @@ See [compaction.md](compaction.md) for trigger and summarization behavior.
254
254
  | `retry.fallbackChains` | `Record<string, string[]>` | `{}` | Ordered exact model-selector to fallback-selector chains |
255
255
  | `retry.fallbackRevertPolicy` | `"cooldown-expiry"` \| `"never"` | `"cooldown-expiry"` | Automatic primary-model restoration policy |
256
256
  | `retry.abortServerSideFallback` | boolean | `true` | Abort a turn when the provider substitutes a different model after a classifier decline |
257
+ | `fallback.circuitCooldownMs` | number | `60000` | First circuit-breaker cooldown for a chain entry that failed out of its chain; doubles on each consecutive failure; `0` disables the breaker |
258
+ | `fallback.circuitMaxCooldownMs` | number | `1800000` | Ceiling for the doubled circuit-breaker cooldown (30 minutes) |
257
259
  | `retry.maxAgentDelayMs` | number | `60000` | Hard ceiling on agent-level retry delay (60s), applied after the retry profile and jitter |
258
260
  | `retry.provider.timeoutMs` | number | `300000` | Provider/SDK request timeout and stream idle timeout in milliseconds |
259
261
  | `retry.provider.streamStartTimeoutMs` | number | `300000` | Maximum wait for the first provider stream event; `0` disables |
@@ -319,6 +321,28 @@ Billing-class failures — Anthropic's 400 *credit balance is too low*, OpenAI's
319
321
 
320
322
  Anthropic streaming refusals are identified from typed `stopDetails`. A configured candidate receives an immediate **pinned** fallback switch with a user-visible fallback notice: Senpi does not retry the refusing model and a pinned fallback never auto-reverts. Set `retry.fallbackRevertPolicy` to `"cooldown-expiry"` (the default) to return an unpinned fallback to its primary after the primary's cooldown expires, or `"never"` to keep the fallback until you change models.
321
323
 
324
+ #### Fallback circuit breaker
325
+
326
+ When a chain entry fails with a provider-health failure, Senpi opens a circuit for that entry: a transient failure (timeout, overload, 429, 5xx, transport drop) once its retry budget is spent, or a billing, credit, quota, or budget exhaustion at once, including on the last entry of the chain. Authentication (401/403) and request-shape rejections do not open a circuit. The circuit is shared by every session in the process that uses the same agent directory - sessions started with `/new`, `/resume`, or `/fork`, and in-process subagents - so they skip the entry without sending it a request or spending its retry budget:
327
+
328
+ - A session whose current model has an open circuit moves to the next chain entry with a closed circuit at the turn boundary (shown as a `transient` model fallback). Mid-turn fallbacks skip open entries the same way.
329
+ - The cooldown starts at `fallback.circuitCooldownMs` and doubles on each consecutive failure, up to `fallback.circuitMaxCooldownMs`. A provider `Retry-After` (seconds or HTTP-date, on 429 and 503 responses) keeps the circuit open until that time when it is longer, and a later failure without a hint never shortens it. An accepted response from the entry closes the circuit and resets the escalation.
330
+ - After the cooldown the circuit is half-open: the first request to use the entry again - a session's turn or a background 429 probe-back - holds its only probe until the probe settles, and every other request keeps skipping it, including the other request lane of the same session. While the circuit tracks an entry, its clock (monotonic, like every cooldown) decides when a fallback returns to it. The probe closes the circuit as soon as the entry streams a response; its first provider-health failure re-opens the circuit with the doubled cooldown and falls back immediately, without same-model retries. A user abort, a request-shaped error, or disposal hands the probe back. A probe that never answers is aborted as a provider failure after the stream-start guard (5 minutes when `retry.provider.streamStartTimeoutMs` is 0).
331
+ - 429 probe-back probes of a demoted primary respect the circuit: none is sent before the provider's retry-after or the cooldown elapses, or while another session holds the probe.
332
+ - The chain never refuses a turn: if every remaining entry is open, the request still goes to the current entry (or the first open candidate) as a probe.
333
+ - Refusals and request-shaped hard errors do not open circuits. Selecting a model yourself closes its circuit.
334
+
335
+ Circuits are process-local like the rest of the fallback state and are never persisted. Set `fallback.circuitCooldownMs` to `0` to turn the breaker off:
336
+
337
+ ```json
338
+ {
339
+ "fallback": {
340
+ "circuitCooldownMs": 60000,
341
+ "circuitMaxCooldownMs": 1800000
342
+ }
343
+ }
344
+ ```
345
+
322
346
  #### Provider-substituted models
323
347
 
324
348
  Anthropic's server-side fallback betas can retry a classifier-declined request on a substitute model *inside the same response*, marking the handoff with a `fallback` content block; a gateway may enable this on your behalf. Honoring that response means paying for a model you did not select, and after the first handoff Anthropic routes later turns of the conversation straight to the substitute with no marker at all — reported only as a `fallback_message` entry in `usage.iterations`.
@@ -331,7 +355,7 @@ Set it to `false` to keep the substituted response instead. If a gateway in fron
331
355
 
332
356
  Fallback decisions are process-local. A `senpi-task` or subagent child process reads its own settings and maintains its own in-memory suppression state; it does not affect its parent process. Disable fallback for one run without changing settings with `--no-model-fallback` or `SENPI_NO_FALLBACK=1`.
333
357
 
334
- For diagnostics, Senpi writes sanitized NDJSON records for candidate skips, cooldowns, switches, reverts, manual clears, and validation warnings to `<agentDir>/logs/fallback.log`. The file is mode `0600` and rotates at 5 MB (`fallback.log.1`).
358
+ For diagnostics, Senpi writes sanitized NDJSON records for candidate skips, cooldowns, circuit opens (`circuit_opened`) and turn-boundary circuit skips (`circuit_open_skip`), switches, reverts, manual clears, and validation warnings to `<agentDir>/logs/fallback.log`. `/session` reports what failed requests cost the session: failed-request counts and share, time spent in failed requests, and retries that succeeded after a failure in the same user turn but read nothing from the prompt cache, with their uncached input tokens. Prompts under 2,048 tokens are never counted as cache misses, because providers do not cache prompts that small; the token counts come from the provider's usage report, while tying the miss to the failure is the report's reading of the turn. The file is mode `0600` and rotates at 5 MB (`fallback.log.1`).
335
359
 
336
360
  ### Message Delivery
337
361
 
package/docs/usage.md CHANGED
@@ -43,13 +43,13 @@ Type `/` in the editor to open command completion. Extensions can register custo
43
43
  | `/login`, `/logout` | Manage OAuth or API-key credentials |
44
44
  | [`/llama`](llama-cpp.md) | Download, load, and unload llama.cpp router models |
45
45
  | `/model` | Switch models; Ctrl+S in the picker saves the startup default |
46
- | `/thinking` | Switch thinking level; Ctrl+S in the picker saves the startup default |
46
+ | `/thinking [level]` | Set the thinking level for this session (`/thinking high`), or open the picker with no argument; Ctrl+S in the picker saves it as the startup default |
47
47
  | `/scoped-models` | Enable/disable models for Ctrl+P cycling |
48
48
  | `/reasoning [on\|off]` | Show or toggle reasoning for the current model |
49
49
  | `/efforts [level]` | Show or set reasoning effort (graded models only) |
50
50
  | `/fast [on\|off]` | Toggle fast mode (ChatGPT Subscription models, persisted per model) |
51
51
  | `/settings` | Theme, message delivery, transport, and other preferences |
52
- | `/resume` | Pick from previous sessions |
52
+ | `/resume`, `/sessions` | Pick from previous sessions (`/sessions` is an alias) |
53
53
  | `/new` | Start a new session |
54
54
  | `/rename [name]` | Rename the current session (`/name` is an alias) |
55
55
  | `/session` | Show session file, ID, messages, tokens, and cost |
@@ -71,6 +71,8 @@ Type `/` in the editor to open command completion. Extensions can register custo
71
71
 
72
72
  ### Reasoning and Fast Mode Commands
73
73
 
74
+ **Changing the thinking level.** `/thinking <level>` sets it for this session and `/thinking` alone opens a picker. Shift+Tab cycles through the levels the model supports, and `/efforts <level>` sets the reasoning effort for graded models; both remember the level for the current model. The footer shows the active level after the model name.
75
+
74
76
  **`/reasoning [on|off]`** shows or toggles reasoning. Behavior adapts to the active model:
75
77
 
76
78
  - Models without reasoning support are told plainly.
@@ -113,6 +115,7 @@ senpi --fork <path|id> # Fork a session into a new session file
113
115
 
114
116
  Useful session commands:
115
117
 
118
+ - `/resume` (or its alias `/sessions`) opens the same picker as `senpi -r` without leaving the TUI.
116
119
  - `/session` shows the current session file and ID.
117
120
  - `/tree` navigates the in-file session tree and can summarize abandoned branches. Ctrl+E on an assistant entry edits that response in place of the original (the original stays in the file on an abandoned branch; tool calls in the edited response are dropped).
118
121
  - `/fork` creates a new session from an earlier user message.
@@ -12,6 +12,18 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.28] - 2026-09-28
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ ### Removed
26
+
15
27
  ## [2026.9.27-4] - 2026-09-27
16
28
 
17
29
  ### Breaking Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.27-4",
3
+ "version": "2026.9.28",
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.27-4",
34
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.28",
35
35
  "typebox": "1.3.34"
36
36
  },
37
37
  "peerDependencies": {
38
- "@code-yeongyu/senpi": "2026.9.27-4"
38
+ "@code-yeongyu/senpi": "2026.9.28"
39
39
  },
40
40
  "devDependencies": {
41
- "@code-yeongyu/senpi": "2026.9.27-4"
41
+ "@code-yeongyu/senpi": "2026.9.28"
42
42
  },
43
43
  "keywords": [
44
44
  "senpi",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-agent-core",
3
- "version": "2026.9.27-4",
3
+ "version": "2026.9.28",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,8 +57,8 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@earendil-works/chord": "0.85.1",
60
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.27-4",
61
- "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.9.27-4",
60
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.28",
61
+ "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.9.28",
62
62
  "diff": "9.0.0",
63
63
  "ignore": "7.0.9",
64
64
  "typebox": "1.3.34",
@@ -1 +1 @@
1
- {"schemaVersion":3,"generatedAt":"2026-09-27T17:38:10.496Z","structureHash":"de13fd32a0f46b302e3edd0d4d50c72e20019642cf25e876b706b4edc97089e5","files":{"alibaba-token-plan.json":"3a9c1be76d47459371e9fa2feeffc31e7edbbfec65fa9cc41236988efc4cf6d9","amazon-bedrock.json":"848e38fd1ade9c6e94c3037c1c6104ac515809a6f190d264eafd9f48ab0ae1ae","ant-ling.json":"4979fe79d99ed97382d7ce40170c6132e932e77906de1eb54464c042e7d63633","anthropic.json":"f9db437d92449fb860a2d457a6019cdfd51c11180c08df2b5f14d75ade773953","azure-openai-responses.json":"d70ff10410e6dd8eacb4a53b9fe887be836562cbb4b74eaee4f28bdbc4ac0732","bai.json":"2b12b15bd43462e60711944dd7283db8676611d9d76c82c2a01f64913e16a67e","baseten.json":"d155c434294b5167244cffb6dea6793f9669e91e8ab65b564f9510d81b330e37","cerebras.json":"3be228c613905c215c9da3d0b3535e10cc594ff2351b05390744c7c34de89b6c","chatgpt-subscription.json":"421c6ad932de4259c8f0c08589da278bed99ec80c042f2490d8ffd2a86f4da99","cloudflare-ai-gateway.json":"b64cc5c7c0df4bfce0c3c2b14fe5290636553914b09718634668ad98276a9bc5","cloudflare-workers-ai.json":"8ee15540a01cf79d0e4063c493ae3c436af2fc1d456f9faf1b066418247183e9","deepseek.json":"b5f9693ec641e9a957dc5f7a22042b3bb928bac3d7268406619b6f18e21c4b83","fireworks.json":"bbb8149854a2658f221afa47ae8e2617e6b58a8e2784aa822faaf54f0c0f18c3","github-copilot.json":"5c9fc5b1b553a26aa7bc843a3e36fa14cf220d528ecd89528c601c5090c88a2d","google-vertex.json":"97d80ac909d256cc1bf19ac54d9d8131c492e7c0c102ad89f68ade528544e0ad","google.json":"68f6cf6283cc0d69b77b0fa79e9c94774e96524f61ebba616319f05d5c9db56c","groq.json":"e4728fd72e358cd8f25e9a518e9b49a521ef44523d3debe38a7db9e0663c4077","huggingface.json":"2f93082144a7b2a1cbf79148244ce7559e0428d6310c5eb0648bca89b15a8b4b","minimax-cn.json":"4f636b47b28a507ad33b5eaba33fc4889e5956504d10f2a0d9c61167ae01ff1d","minimax.json":"9fd1764ffeefebded8579039cf0c48854a43e4c5b724f62361fc2ac54f705aa1","mistral.json":"be522597b22bceeb0bb9a0ba1781647649ffd19637d0c8e67b056d7b0ec33d52","moonshotai-cn.json":"326de79b46f6fdbf0879f005b3040d1b14049e81fa678de407b73ed1bd3bce9c","moonshotai.json":"4b917a375e9c880b903927cf5fe1ae1052c74b64d5027a007bb1e67ad0410755","nvidia.json":"6493e09b5db8b8fe9c8fae5bc15dc287645f37f83a4ed3a1ad1181e5830b5ff7","openai.json":"4c18b28b168f56d621670866a62d2d435da3a8eee55a204bdbabe02c48e6513e","opencode-go.json":"00b2c0154285829e8db1e7b9ad492771b098cb88a61b036e51a6861b2f2f6dc1","opencode.json":"1655e248e76f919fd14ca4b8a6201d1dd89b2190b295028c5d7a4a2a03809946","opengateway.json":"121f544154757cfbccd3bc3d7344986a5822886c12e1c00b39ebf20ef609f239","openrouter.json":"bc12e53c9a86bc315a076a78aa605a520b51cee7214846afe9db77236a703251","qwen-token-plan-cn.json":"4bf520ed38e5c3045680d2ec2816fa8a69079dbe97f7b929767a63172288da6b","qwen-token-plan-individual.json":"eb040e78595853e980011308b1bf9caacae6dc8a91abbac5ffebd402515dd2a6","qwen-token-plan.json":"a5c67213745329ec3ba79f712f31c1c32a781d7ffb8c04f9819f36c27672531f","together.json":"200fa2cc0b4666e3818c260e41b9925b925180e2d0931d7c900e16494b2314b4","venice.json":"d34b566ee6e7a2ed31abd9099a1c0448f1d521fc696382d03d86c90634270cf1","vercel-ai-gateway.json":"95a697cd6f9eba46972dbfa301e80f18f45c701e6068c0326d2f36da10b7ce7e","xai.json":"9b23faa5218e966a17108c05d2498a617ade1eae9b0d0684284c77d67949a3f1","xiaomi-token-plan-ams.json":"7df74a255259fc1d5be8e682c3ffb0908eaaf0af711f6433dedcf345ce3e8393","xiaomi-token-plan-cn.json":"7dc672723d5b37e5b92605752f74f46f44eba10be2b0cb959790260b9e0c2a4e","xiaomi-token-plan-sgp.json":"69b333b158be038459a511a673f4536a5b03d4931c3119339f1ac741bc03d502","xiaomi.json":"333299657f211ad9edac0f3d4298b7e5776626914c9c80e22305a87533f23966","zai-coding-cn.json":"69c572aa75104f16ccbddbaace60c267f4bcc74f86d34891ca43110d312fb1ad","zai.json":"15c8182a88e58bdc1b1c3eb0603c690475342c8a826ceddfe9af1b36b28345ee"}}
1
+ {"schemaVersion":3,"generatedAt":"2026-09-28T01:49:01.161Z","structureHash":"de13fd32a0f46b302e3edd0d4d50c72e20019642cf25e876b706b4edc97089e5","files":{"alibaba-token-plan.json":"3a9c1be76d47459371e9fa2feeffc31e7edbbfec65fa9cc41236988efc4cf6d9","amazon-bedrock.json":"848e38fd1ade9c6e94c3037c1c6104ac515809a6f190d264eafd9f48ab0ae1ae","ant-ling.json":"4979fe79d99ed97382d7ce40170c6132e932e77906de1eb54464c042e7d63633","anthropic.json":"f9db437d92449fb860a2d457a6019cdfd51c11180c08df2b5f14d75ade773953","azure-openai-responses.json":"d70ff10410e6dd8eacb4a53b9fe887be836562cbb4b74eaee4f28bdbc4ac0732","bai.json":"2b12b15bd43462e60711944dd7283db8676611d9d76c82c2a01f64913e16a67e","baseten.json":"d155c434294b5167244cffb6dea6793f9669e91e8ab65b564f9510d81b330e37","cerebras.json":"3be228c613905c215c9da3d0b3535e10cc594ff2351b05390744c7c34de89b6c","chatgpt-subscription.json":"421c6ad932de4259c8f0c08589da278bed99ec80c042f2490d8ffd2a86f4da99","cloudflare-ai-gateway.json":"b64cc5c7c0df4bfce0c3c2b14fe5290636553914b09718634668ad98276a9bc5","cloudflare-workers-ai.json":"8ee15540a01cf79d0e4063c493ae3c436af2fc1d456f9faf1b066418247183e9","deepseek.json":"b5f9693ec641e9a957dc5f7a22042b3bb928bac3d7268406619b6f18e21c4b83","fireworks.json":"bbb8149854a2658f221afa47ae8e2617e6b58a8e2784aa822faaf54f0c0f18c3","github-copilot.json":"5c9fc5b1b553a26aa7bc843a3e36fa14cf220d528ecd89528c601c5090c88a2d","google-vertex.json":"97d80ac909d256cc1bf19ac54d9d8131c492e7c0c102ad89f68ade528544e0ad","google.json":"68f6cf6283cc0d69b77b0fa79e9c94774e96524f61ebba616319f05d5c9db56c","groq.json":"e4728fd72e358cd8f25e9a518e9b49a521ef44523d3debe38a7db9e0663c4077","huggingface.json":"2f93082144a7b2a1cbf79148244ce7559e0428d6310c5eb0648bca89b15a8b4b","minimax-cn.json":"4f636b47b28a507ad33b5eaba33fc4889e5956504d10f2a0d9c61167ae01ff1d","minimax.json":"9fd1764ffeefebded8579039cf0c48854a43e4c5b724f62361fc2ac54f705aa1","mistral.json":"be522597b22bceeb0bb9a0ba1781647649ffd19637d0c8e67b056d7b0ec33d52","moonshotai-cn.json":"326de79b46f6fdbf0879f005b3040d1b14049e81fa678de407b73ed1bd3bce9c","moonshotai.json":"4b917a375e9c880b903927cf5fe1ae1052c74b64d5027a007bb1e67ad0410755","nvidia.json":"6493e09b5db8b8fe9c8fae5bc15dc287645f37f83a4ed3a1ad1181e5830b5ff7","openai.json":"4c18b28b168f56d621670866a62d2d435da3a8eee55a204bdbabe02c48e6513e","opencode-go.json":"00b2c0154285829e8db1e7b9ad492771b098cb88a61b036e51a6861b2f2f6dc1","opencode.json":"1655e248e76f919fd14ca4b8a6201d1dd89b2190b295028c5d7a4a2a03809946","opengateway.json":"121f544154757cfbccd3bc3d7344986a5822886c12e1c00b39ebf20ef609f239","openrouter.json":"ac1c0d449b1fbaa6cd36c55408789ac6e91a861af525045ab0168214f18c8164","qwen-token-plan-cn.json":"4bf520ed38e5c3045680d2ec2816fa8a69079dbe97f7b929767a63172288da6b","qwen-token-plan-individual.json":"eb040e78595853e980011308b1bf9caacae6dc8a91abbac5ffebd402515dd2a6","qwen-token-plan.json":"a5c67213745329ec3ba79f712f31c1c32a781d7ffb8c04f9819f36c27672531f","together.json":"200fa2cc0b4666e3818c260e41b9925b925180e2d0931d7c900e16494b2314b4","venice.json":"d34b566ee6e7a2ed31abd9099a1c0448f1d521fc696382d03d86c90634270cf1","vercel-ai-gateway.json":"95a697cd6f9eba46972dbfa301e80f18f45c701e6068c0326d2f36da10b7ce7e","xai.json":"9b23faa5218e966a17108c05d2498a617ade1eae9b0d0684284c77d67949a3f1","xiaomi-token-plan-ams.json":"7df74a255259fc1d5be8e682c3ffb0908eaaf0af711f6433dedcf345ce3e8393","xiaomi-token-plan-cn.json":"7dc672723d5b37e5b92605752f74f46f44eba10be2b0cb959790260b9e0c2a4e","xiaomi-token-plan-sgp.json":"69b333b158be038459a511a673f4536a5b03d4931c3119339f1ac741bc03d502","xiaomi.json":"333299657f211ad9edac0f3d4298b7e5776626914c9c80e22305a87533f23966","zai-coding-cn.json":"69c572aa75104f16ccbddbaace60c267f4bcc74f86d34891ca43110d312fb1ad","zai.json":"15c8182a88e58bdc1b1c3eb0603c690475342c8a826ceddfe9af1b36b28345ee"}}