@jsonstudio/rcc 0.89.2239 → 0.90.89

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 (338) hide show
  1. package/README.md +27 -0
  2. package/dist/build-info.js +2 -2
  3. package/dist/build-info.js.map +1 -1
  4. package/dist/cli/commands/claude.js +4 -8
  5. package/dist/cli/commands/claude.js.map +1 -1
  6. package/dist/cli/commands/codex.js +6 -3
  7. package/dist/cli/commands/codex.js.map +1 -1
  8. package/dist/cli/commands/guardian-daemon.d.ts +2 -0
  9. package/dist/cli/commands/guardian-daemon.js +299 -0
  10. package/dist/cli/commands/guardian-daemon.js.map +1 -0
  11. package/dist/cli/commands/init/camoufox.js +1 -1
  12. package/dist/cli/commands/init/camoufox.js.map +1 -1
  13. package/dist/cli/commands/launcher/index.d.ts +1 -1
  14. package/dist/cli/commands/launcher/types.d.ts +7 -1
  15. package/dist/cli/commands/launcher/utils.d.ts +4 -1
  16. package/dist/cli/commands/launcher/utils.js +18 -8
  17. package/dist/cli/commands/launcher/utils.js.map +1 -1
  18. package/dist/cli/commands/launcher-kernel.d.ts +1 -1
  19. package/dist/cli/commands/launcher-kernel.js +608 -249
  20. package/dist/cli/commands/launcher-kernel.js.map +1 -1
  21. package/dist/cli/commands/port.js +28 -8
  22. package/dist/cli/commands/port.js.map +1 -1
  23. package/dist/cli/commands/restart.d.ts +4 -0
  24. package/dist/cli/commands/restart.js +91 -42
  25. package/dist/cli/commands/restart.js.map +1 -1
  26. package/dist/cli/commands/{clock-admin.d.ts → session-admin.d.ts} +2 -2
  27. package/dist/cli/commands/{clock-admin.js → session-admin.js} +17 -17
  28. package/dist/cli/commands/session-admin.js.map +1 -0
  29. package/dist/cli/commands/{tmux-inject.d.ts → session-inject.d.ts} +2 -2
  30. package/dist/cli/commands/{tmux-inject.js → session-inject.js} +12 -12
  31. package/dist/cli/commands/session-inject.js.map +1 -0
  32. package/dist/cli/commands/start-types.d.ts +4 -0
  33. package/dist/cli/commands/start-utils.d.ts +1 -0
  34. package/dist/cli/commands/start-utils.js +3 -0
  35. package/dist/cli/commands/start-utils.js.map +1 -1
  36. package/dist/cli/commands/start.js +122 -72
  37. package/dist/cli/commands/start.js.map +1 -1
  38. package/dist/cli/commands/stop.d.ts +3 -0
  39. package/dist/cli/commands/stop.js +30 -63
  40. package/dist/cli/commands/stop.js.map +1 -1
  41. package/dist/cli/config/init-provider-catalog.js +8 -3
  42. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  43. package/dist/cli/guardian/client.d.ts +38 -0
  44. package/dist/cli/guardian/client.js +237 -0
  45. package/dist/cli/guardian/client.js.map +1 -0
  46. package/dist/cli/guardian/paths.d.ts +7 -0
  47. package/dist/cli/guardian/paths.js +13 -0
  48. package/dist/cli/guardian/paths.js.map +1 -0
  49. package/dist/cli/guardian/types.d.ts +30 -0
  50. package/dist/cli/guardian/types.js +2 -0
  51. package/dist/cli/guardian/types.js.map +1 -0
  52. package/dist/cli/register/guardian-daemon-command.d.ts +2 -0
  53. package/dist/cli/register/guardian-daemon-command.js +5 -0
  54. package/dist/cli/register/guardian-daemon-command.js.map +1 -0
  55. package/dist/cli/register/session-admin-command.d.ts +3 -0
  56. package/dist/cli/register/session-admin-command.js +5 -0
  57. package/dist/cli/register/session-admin-command.js.map +1 -0
  58. package/dist/cli/register/session-inject-command.d.ts +3 -0
  59. package/dist/cli/register/session-inject-command.js +5 -0
  60. package/dist/cli/register/session-inject-command.js.map +1 -0
  61. package/dist/cli/server/port-utils.js +57 -1
  62. package/dist/cli/server/port-utils.js.map +1 -1
  63. package/dist/cli.js +52 -4
  64. package/dist/cli.js.map +1 -1
  65. package/dist/commands/oauth.js +6 -6
  66. package/dist/commands/oauth.js.map +1 -1
  67. package/dist/config/provider-v2-loader.js +18 -3
  68. package/dist/config/provider-v2-loader.js.map +1 -1
  69. package/dist/config/routecodex-config-loader.js +184 -9
  70. package/dist/config/routecodex-config-loader.js.map +1 -1
  71. package/dist/config/unified-config-paths.js +22 -0
  72. package/dist/config/unified-config-paths.js.map +1 -1
  73. package/dist/config/virtual-router-builder.js +18 -5
  74. package/dist/config/virtual-router-builder.js.map +1 -1
  75. package/dist/config/virtual-router-types.js +20 -5
  76. package/dist/config/virtual-router-types.js.map +1 -1
  77. package/dist/daemon-admin-ui/assets/index-C8vP_c5E.js +15 -0
  78. package/dist/daemon-admin-ui/assets/index-DjIoHmNv.css +1 -0
  79. package/dist/daemon-admin-ui/index.html +13 -0
  80. package/dist/docs/daemon-admin-ui.html +334 -63
  81. package/dist/index.d.ts +9 -0
  82. package/dist/index.js +268 -10
  83. package/dist/index.js.map +1 -1
  84. package/dist/manager/modules/quota/provider-key-normalization.js +1 -10
  85. package/dist/manager/modules/quota/provider-key-normalization.js.map +1 -1
  86. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.d.ts +1 -0
  87. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js +36 -0
  88. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js.map +1 -1
  89. package/dist/manager/modules/quota/provider-quota-daemon.events.js +89 -49
  90. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
  91. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +2 -16
  92. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
  93. package/dist/manager/modules/token/index.d.ts +1 -0
  94. package/dist/manager/modules/token/index.js +6 -1
  95. package/dist/manager/modules/token/index.js.map +1 -1
  96. package/dist/manager/types.d.ts +1 -0
  97. package/dist/modules/llmswitch/bridge/state-integrations.js +1 -1
  98. package/dist/modules/llmswitch/bridge/state-integrations.js.map +1 -1
  99. package/dist/providers/auth/antigravity-user-agent.js +78 -31
  100. package/dist/providers/auth/antigravity-user-agent.js.map +1 -1
  101. package/dist/providers/auth/gemini-cli-userinfo-helper.js +94 -63
  102. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
  103. package/dist/providers/auth/iflow-userinfo-helper.js +1 -1
  104. package/dist/providers/auth/iflow-userinfo-helper.js.map +1 -1
  105. package/dist/providers/auth/oauth-error-message.d.ts +1 -0
  106. package/dist/providers/auth/oauth-error-message.js +44 -0
  107. package/dist/providers/auth/oauth-error-message.js.map +1 -0
  108. package/dist/providers/auth/oauth-lifecycle/error-detection.js +42 -8
  109. package/dist/providers/auth/oauth-lifecycle/error-detection.js.map +1 -1
  110. package/dist/providers/auth/oauth-lifecycle/token-io.d.ts +1 -0
  111. package/dist/providers/auth/oauth-lifecycle/token-io.js +12 -0
  112. package/dist/providers/auth/oauth-lifecycle/token-io.js.map +1 -1
  113. package/dist/providers/auth/oauth-lifecycle.js +502 -87
  114. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  115. package/dist/providers/auth/oauth-repair-env.js +3 -5
  116. package/dist/providers/auth/oauth-repair-env.js.map +1 -1
  117. package/dist/providers/auth/oauth-utils/error-extraction.js +42 -8
  118. package/dist/providers/auth/oauth-utils/error-extraction.js.map +1 -1
  119. package/dist/providers/core/config/camoufox-actions.d.ts +31 -0
  120. package/dist/providers/core/config/camoufox-actions.js +470 -0
  121. package/dist/providers/core/config/camoufox-actions.js.map +1 -0
  122. package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
  123. package/dist/providers/core/config/camoufox-launcher.js +553 -159
  124. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  125. package/dist/providers/core/config/oauth-flows.js +6 -44
  126. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  127. package/dist/providers/core/config/provider-oauth-configs.js +51 -7
  128. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  129. package/dist/providers/core/config/service-profiles.js +2 -2
  130. package/dist/providers/core/config/service-profiles.js.map +1 -1
  131. package/dist/providers/core/runtime/base-provider-runtime-helpers.js +15 -2
  132. package/dist/providers/core/runtime/base-provider-runtime-helpers.js.map +1 -1
  133. package/dist/providers/core/runtime/provider-error-classifier.js +32 -15
  134. package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
  135. package/dist/providers/core/runtime/provider-family-profile-utils.js +1 -1
  136. package/dist/providers/core/runtime/provider-family-profile-utils.js.map +1 -1
  137. package/dist/providers/core/runtime/provider-response-postprocessor.js +61 -14
  138. package/dist/providers/core/runtime/provider-response-postprocessor.js.map +1 -1
  139. package/dist/providers/core/strategies/oauth-auth-code-flow.d.ts +1 -0
  140. package/dist/providers/core/strategies/oauth-auth-code-flow.js +124 -19
  141. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  142. package/dist/providers/core/strategies/oauth-device-flow.js +32 -6
  143. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  144. package/dist/providers/core/utils/provider-error-reporter.js +51 -0
  145. package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
  146. package/dist/providers/profile/families/iflow-profile.js +83 -10
  147. package/dist/providers/profile/families/iflow-profile.js.map +1 -1
  148. package/dist/scripts/camoufox/launch-auth.mjs +112 -5
  149. package/dist/server/handlers/config-admin-handler.js +9 -2
  150. package/dist/server/handlers/config-admin-handler.js.map +1 -1
  151. package/dist/server/handlers/handler-response-utils.js +3 -6
  152. package/dist/server/handlers/handler-response-utils.js.map +1 -1
  153. package/dist/server/handlers/handler-utils.js +14 -17
  154. package/dist/server/handlers/handler-utils.js.map +1 -1
  155. package/dist/server/handlers/logging.js +3 -4
  156. package/dist/server/handlers/logging.js.map +1 -1
  157. package/dist/server/handlers/responses-handler.js +5 -3
  158. package/dist/server/handlers/responses-handler.js.map +1 -1
  159. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +5 -3
  160. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
  161. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +104 -15
  162. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
  163. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +2 -2
  164. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  165. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.d.ts +24 -0
  166. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js +316 -70
  167. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js.map +1 -1
  168. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +190 -1
  169. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  170. package/dist/server/runtime/http-server/daemon-admin/routing-policy.d.ts +1 -1
  171. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +21 -32
  172. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -1
  173. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +2 -0
  174. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
  175. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +8 -1
  176. package/dist/server/runtime/http-server/daemon-admin-routes.js +30 -0
  177. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  178. package/dist/server/runtime/http-server/executor/client-injection-flow.d.ts +14 -0
  179. package/dist/server/runtime/http-server/executor/client-injection-flow.js +297 -0
  180. package/dist/server/runtime/http-server/executor/client-injection-flow.js.map +1 -0
  181. package/dist/server/runtime/http-server/executor/index.d.ts +1 -1
  182. package/dist/server/runtime/http-server/executor/index.js +1 -1
  183. package/dist/server/runtime/http-server/executor/index.js.map +1 -1
  184. package/dist/server/runtime/http-server/executor/provider-response-converter.js +281 -70
  185. package/dist/server/runtime/http-server/executor/provider-response-converter.js.map +1 -1
  186. package/dist/server/runtime/http-server/executor/provider-runtime-resolver.js +8 -6
  187. package/dist/server/runtime/http-server/executor/provider-runtime-resolver.js.map +1 -1
  188. package/dist/server/runtime/http-server/executor/request-executor-core-utils.d.ts +1 -0
  189. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js +12 -0
  190. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js.map +1 -1
  191. package/dist/server/runtime/http-server/executor/request-retry-helpers.d.ts +1 -1
  192. package/dist/server/runtime/http-server/executor/request-retry-helpers.js +23 -19
  193. package/dist/server/runtime/http-server/executor/request-retry-helpers.js.map +1 -1
  194. package/dist/server/runtime/http-server/executor/retry-engine.d.ts +2 -2
  195. package/dist/server/runtime/http-server/executor/retry-engine.js +2 -2
  196. package/dist/server/runtime/http-server/executor/retry-engine.js.map +1 -1
  197. package/dist/server/runtime/http-server/executor/sse-error-handler.d.ts +1 -0
  198. package/dist/server/runtime/http-server/executor/sse-error-handler.js +13 -2
  199. package/dist/server/runtime/http-server/executor/sse-error-handler.js.map +1 -1
  200. package/dist/server/runtime/http-server/executor/usage-aggregator.d.ts +0 -12
  201. package/dist/server/runtime/http-server/executor/usage-aggregator.js +89 -90
  202. package/dist/server/runtime/http-server/executor/usage-aggregator.js.map +1 -1
  203. package/dist/server/runtime/http-server/executor-metadata.js +318 -17
  204. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  205. package/dist/server/runtime/http-server/executor-provider.d.ts +1 -0
  206. package/dist/server/runtime/http-server/executor-provider.js +5 -1
  207. package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
  208. package/dist/server/runtime/http-server/executor-response.d.ts +1 -0
  209. package/dist/server/runtime/http-server/executor-response.js +52 -58
  210. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  211. package/dist/server/runtime/http-server/http-server-bootstrap.js +50 -6
  212. package/dist/server/runtime/http-server/http-server-bootstrap.js.map +1 -1
  213. package/dist/server/runtime/http-server/http-server-lifecycle.js +6 -5
  214. package/dist/server/runtime/http-server/http-server-lifecycle.js.map +1 -1
  215. package/dist/server/runtime/http-server/http-server-runtime-setup.js +1 -1
  216. package/dist/server/runtime/http-server/http-server-runtime-setup.js.map +1 -1
  217. package/dist/server/runtime/http-server/http-server-session-daemon.d.ts +6 -0
  218. package/dist/server/runtime/http-server/http-server-session-daemon.js +404 -0
  219. package/dist/server/runtime/http-server/http-server-session-daemon.js.map +1 -0
  220. package/dist/server/runtime/http-server/hub-shadow-compare.js +1 -1
  221. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  222. package/dist/server/runtime/http-server/index.d.ts +11 -10
  223. package/dist/server/runtime/http-server/index.js +21 -20
  224. package/dist/server/runtime/http-server/index.js.map +1 -1
  225. package/dist/server/runtime/http-server/managed-process-probe.js +1 -1
  226. package/dist/server/runtime/http-server/managed-process-probe.js.map +1 -1
  227. package/dist/server/runtime/http-server/middleware.js +91 -5
  228. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  229. package/dist/server/runtime/http-server/request-executor.js +19 -9
  230. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  231. package/dist/server/runtime/http-server/routes.d.ts +2 -1
  232. package/dist/server/runtime/http-server/routes.js +7 -5
  233. package/dist/server/runtime/http-server/routes.js.map +1 -1
  234. package/dist/server/runtime/http-server/{clock-client-reaper.d.ts → session-client-reaper.d.ts} +6 -6
  235. package/dist/server/runtime/http-server/{clock-client-reaper.js → session-client-reaper.js} +26 -49
  236. package/dist/server/runtime/http-server/session-client-reaper.js.map +1 -0
  237. package/dist/server/runtime/http-server/{clock-client-registry-utils.d.ts → session-client-registry-utils.d.ts} +14 -10
  238. package/dist/server/runtime/http-server/{clock-client-registry-utils.js → session-client-registry-utils.js} +77 -19
  239. package/dist/server/runtime/http-server/session-client-registry-utils.js.map +1 -0
  240. package/dist/server/runtime/http-server/{clock-client-registry.d.ts → session-client-registry.d.ts} +26 -11
  241. package/dist/server/runtime/http-server/{clock-client-registry.js → session-client-registry.js} +305 -11
  242. package/dist/server/runtime/http-server/session-client-registry.js.map +1 -0
  243. package/dist/server/runtime/http-server/{clock-client-route-utils.d.ts → session-client-route-utils.d.ts} +1 -1
  244. package/dist/server/runtime/http-server/{clock-client-route-utils.js → session-client-route-utils.js} +4 -4
  245. package/dist/server/runtime/http-server/session-client-route-utils.js.map +1 -0
  246. package/dist/server/runtime/http-server/session-client-routes.d.ts +2 -0
  247. package/dist/server/runtime/http-server/{clock-client-routes.js → session-client-routes.js} +107 -59
  248. package/dist/server/runtime/http-server/session-client-routes.js.map +1 -0
  249. package/dist/server/runtime/http-server/session-daemon-inject-config.d.ts +1 -0
  250. package/dist/server/runtime/http-server/{clock-daemon-inject-config.js → session-daemon-inject-config.js} +2 -2
  251. package/dist/server/runtime/http-server/session-daemon-inject-config.js.map +1 -0
  252. package/dist/server/runtime/http-server/session-daemon-log-throttle.d.ts +28 -0
  253. package/dist/server/runtime/http-server/session-daemon-log-throttle.js +105 -0
  254. package/dist/server/runtime/http-server/session-daemon-log-throttle.js.map +1 -0
  255. package/dist/server/runtime/http-server/session-dir.js +12 -1
  256. package/dist/server/runtime/http-server/session-dir.js.map +1 -1
  257. package/dist/server/runtime/http-server/session-scope-resolution.d.ts +14 -0
  258. package/dist/server/runtime/http-server/session-scope-resolution.js +208 -0
  259. package/dist/server/runtime/http-server/session-scope-resolution.js.map +1 -0
  260. package/dist/server/runtime/http-server/stats-manager.d.ts +35 -0
  261. package/dist/server/runtime/http-server/stats-manager.js +269 -21
  262. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  263. package/dist/server/runtime/http-server/stopmessage-scope-rebind.d.ts +21 -0
  264. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js +197 -0
  265. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js.map +1 -0
  266. package/dist/server/runtime/http-server/tmux-session-probe.d.ts +10 -0
  267. package/dist/server/runtime/http-server/tmux-session-probe.js +98 -1
  268. package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -1
  269. package/dist/server-lifecycle/port-utils.d.ts +2 -1
  270. package/dist/server-lifecycle/port-utils.js +84 -4
  271. package/dist/server-lifecycle/port-utils.js.map +1 -1
  272. package/dist/token-daemon/index.d.ts +1 -0
  273. package/dist/token-daemon/index.js +17 -12
  274. package/dist/token-daemon/index.js.map +1 -1
  275. package/dist/token-daemon/token-daemon.d.ts +2 -0
  276. package/dist/token-daemon/token-daemon.js +18 -10
  277. package/dist/token-daemon/token-daemon.js.map +1 -1
  278. package/dist/utils/llms-engine-shadow.js +1 -1
  279. package/dist/utils/llms-engine-shadow.js.map +1 -1
  280. package/dist/utils/log-helpers.js +46 -0
  281. package/dist/utils/log-helpers.js.map +1 -1
  282. package/dist/utils/session-client-token.d.ts +4 -0
  283. package/dist/utils/session-client-token.js +93 -0
  284. package/dist/utils/session-client-token.js.map +1 -0
  285. package/dist/utils/session-scope-trace.d.ts +11 -0
  286. package/dist/utils/session-scope-trace.js +41 -0
  287. package/dist/utils/session-scope-trace.js.map +1 -0
  288. package/docs/CLOCK.md +0 -1
  289. package/docs/DAEMON_CONTROL_PLANE.md +1 -0
  290. package/docs/PORTS.md +2 -2
  291. package/docs/ROUTING_POLICY_SCHEMA.md +5 -3
  292. package/docs/antigravity-routing-contract.md +2 -2
  293. package/docs/daemon-admin-ui.html +334 -63
  294. package/docs/design/servertool-stopmessage-lifecycle.md +109 -0
  295. package/docs/exec-command-guard-policy.example.v1.json +7 -1
  296. package/docs/providers/antigravity-gemini-provider-compat.md +2 -2
  297. package/docs/{clock-client-daemon-design.md → session-client-daemon-design.md} +34 -34
  298. package/package.json +23 -7
  299. package/scripts/build-core.mjs +12 -0
  300. package/scripts/camoufox/launch-auth.mjs +112 -5
  301. package/scripts/ci/repo-sanity.mjs +1 -0
  302. package/scripts/compare-responses-sse.mjs +267 -0
  303. package/scripts/install-global.sh +6 -0
  304. package/scripts/install-verify.mjs +33 -16
  305. package/scripts/replay-codex-sample.mjs +52 -6
  306. package/scripts/run-bg.sh +226 -43
  307. package/scripts/run-fg-gtimeout.sh +158 -14
  308. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +3 -3
  309. package/scripts/tests/ci-jest.mjs +9 -1
  310. package/scripts/triage-errorsamples.mjs +216 -0
  311. package/scripts/verify-codex-error-samples.mjs +92 -15
  312. package/scripts/verify-install-e2e.mjs +57 -27
  313. package/scripts/virtual-router-dryrun.mjs +7 -1
  314. package/dist/cli/commands/clock-admin.js.map +0 -1
  315. package/dist/cli/commands/tmux-inject.js.map +0 -1
  316. package/dist/cli/register/clock-admin-command.d.ts +0 -3
  317. package/dist/cli/register/clock-admin-command.js +0 -5
  318. package/dist/cli/register/clock-admin-command.js.map +0 -1
  319. package/dist/cli/register/tmux-inject-command.d.ts +0 -3
  320. package/dist/cli/register/tmux-inject-command.js +0 -5
  321. package/dist/cli/register/tmux-inject-command.js.map +0 -1
  322. package/dist/server/runtime/http-server/clock-client-reaper.js.map +0 -1
  323. package/dist/server/runtime/http-server/clock-client-registry-utils.js.map +0 -1
  324. package/dist/server/runtime/http-server/clock-client-registry.js.map +0 -1
  325. package/dist/server/runtime/http-server/clock-client-route-utils.js.map +0 -1
  326. package/dist/server/runtime/http-server/clock-client-routes.d.ts +0 -2
  327. package/dist/server/runtime/http-server/clock-client-routes.js.map +0 -1
  328. package/dist/server/runtime/http-server/clock-daemon-inject-config.d.ts +0 -1
  329. package/dist/server/runtime/http-server/clock-daemon-inject-config.js.map +0 -1
  330. package/dist/server/runtime/http-server/clock-daemon-log-throttle.d.ts +0 -12
  331. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js +0 -56
  332. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js.map +0 -1
  333. package/dist/server/runtime/http-server/http-server-clock-daemon.d.ts +0 -5
  334. package/dist/server/runtime/http-server/http-server-clock-daemon.js +0 -255
  335. package/dist/server/runtime/http-server/http-server-clock-daemon.js.map +0 -1
  336. package/dist/utils/clock-client-token.d.ts +0 -3
  337. package/dist/utils/clock-client-token.js +0 -54
  338. package/dist/utils/clock-client-token.js.map +0 -1
@@ -4,13 +4,105 @@ import { spawnSync } from 'node:child_process';
4
4
  import { createServer } from 'node:http';
5
5
  import crypto from 'node:crypto';
6
6
  import { LOCAL_HOSTS } from '../../constants/index.js';
7
- import { encodeClockClientApiKey } from '../../utils/clock-client-token.js';
7
+ import { encodeSessionClientApiKey, extractSessionClientDaemonIdFromApiKey, extractSessionClientScopeIdFromApiKey } from '../../utils/session-client-token.js';
8
+ import { isSessionScopeTraceEnabled, isSessionScopeTraceVerbose } from '../../utils/session-scope-trace.js';
8
9
  import { logProcessLifecycle } from '../../utils/process-lifecycle-logger.js';
9
- import { resolveBinary, parseServerUrl, resolveTmuxSelfHealPolicy, readConfigApiKey, normalizeConnectHost, toIntegerPort, tryReadConfigHostPort, resolveIntFromEnv } from './launcher/utils.js';
10
+ import { resolveBinary, parseServerUrl, resolveBoolFromEnv, resolveTmuxSelfHealPolicy, readConfigApiKey, normalizeConnectHost, toIntegerPort, tryReadConfigHostPort, resolveIntFromEnv } from './launcher/utils.js';
11
+ import { resolveRouteCodexConfigPath } from '../../config/config-paths.js';
12
+ function shouldStopManagedTmuxOnShutdown(signal, env) {
13
+ if (signal === 'SIGINT') {
14
+ return true;
15
+ }
16
+ if (signal !== 'SIGTERM') {
17
+ return true;
18
+ }
19
+ return resolveBoolFromEnv(env.ROUTECODEX_LAUNCHER_STOP_MANAGED_TMUX_ON_SIGTERM
20
+ ?? env.RCC_LAUNCHER_STOP_MANAGED_TMUX_ON_SIGTERM, true);
21
+ }
22
+ function shouldStopManagedTmuxOnToolExit(env) {
23
+ return resolveBoolFromEnv(env.ROUTECODEX_LAUNCHER_STOP_MANAGED_TMUX_ON_TOOL_EXIT
24
+ ?? env.RCC_LAUNCHER_STOP_MANAGED_TMUX_ON_TOOL_EXIT, true);
25
+ }
26
+ function shouldLogClientExitSummary(commandName) {
27
+ const normalized = String(commandName || '').trim().toLowerCase();
28
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'routecodex';
29
+ }
30
+ function readProcessPpidAndCommand(pid) {
31
+ if (process.platform === 'win32') {
32
+ return { ppid: null, command: '' };
33
+ }
34
+ try {
35
+ const out = spawnSync('ps', ['-p', String(pid), '-o', 'ppid=,command='], { encoding: 'utf8' });
36
+ if (out.error || Number(out.status ?? 0) !== 0) {
37
+ return { ppid: null, command: '' };
38
+ }
39
+ const line = String(out.stdout || '')
40
+ .split(/\r?\n/)
41
+ .map((item) => item.trim())
42
+ .find(Boolean);
43
+ if (!line) {
44
+ return { ppid: null, command: '' };
45
+ }
46
+ const match = line.match(/^(\d+)\s+(.+)$/);
47
+ if (!match) {
48
+ return { ppid: null, command: line };
49
+ }
50
+ const ppid = Number.parseInt(match[1], 10);
51
+ return {
52
+ ppid: Number.isFinite(ppid) && ppid > 0 ? ppid : null,
53
+ command: match[2] || ''
54
+ };
55
+ }
56
+ catch {
57
+ return { ppid: null, command: '' };
58
+ }
59
+ }
60
+ function commandLikelyMatchesHint(command, commandHint) {
61
+ const normalizedCommand = String(command || '').toLowerCase();
62
+ const normalizedHint = String(commandHint || '').toLowerCase().trim();
63
+ if (!normalizedHint) {
64
+ return true;
65
+ }
66
+ const hintBase = path.basename(normalizedHint);
67
+ if (hintBase && normalizedCommand.includes(hintBase)) {
68
+ return true;
69
+ }
70
+ const tokens = normalizedHint
71
+ .split(/[\\/\s]+/)
72
+ .map((token) => token.trim())
73
+ .filter((token) => token.length >= 3);
74
+ return tokens.some((token) => normalizedCommand.includes(token));
75
+ }
76
+ function canSignalOwnedToolProcess(args) {
77
+ const strictGuard = resolveBoolFromEnv(args.env.ROUTECODEX_LAUNCHER_STRICT_SIGNAL_GUARD ?? args.env.RCC_LAUNCHER_STRICT_SIGNAL_GUARD, true);
78
+ if (!strictGuard) {
79
+ return { ok: true, reason: 'strict_guard_disabled' };
80
+ }
81
+ if (!args.pid || !Number.isFinite(args.pid) || args.pid <= 1) {
82
+ return { ok: false, reason: 'invalid_pid' };
83
+ }
84
+ if (process.platform === 'win32') {
85
+ return { ok: true, reason: 'unsupported_platform' };
86
+ }
87
+ const snapshot = readProcessPpidAndCommand(args.pid);
88
+ if (!snapshot.ppid) {
89
+ return { ok: false, reason: 'ppid_unavailable' };
90
+ }
91
+ if (snapshot.ppid !== args.expectedParentPid) {
92
+ return { ok: false, reason: 'ppid_mismatch' };
93
+ }
94
+ if (!commandLikelyMatchesHint(snapshot.command, args.commandHint)) {
95
+ return { ok: false, reason: 'command_mismatch' };
96
+ }
97
+ return { ok: true, reason: 'owned_child' };
98
+ }
10
99
  function resolveServerConnection(ctx, fsImpl, pathImpl, options) {
11
100
  let configPath = typeof options.config === 'string' && options.config.trim() ? options.config.trim() : '';
12
101
  if (!configPath) {
13
- configPath = pathImpl.join(ctx.homedir(), '.routecodex', 'config.json');
102
+ const resolved = resolveRouteCodexConfigPath();
103
+ configPath = resolved && resolved.trim()
104
+ ? resolved
105
+ : pathImpl.join(ctx.homedir(), '.routecodex', 'config.json');
14
106
  }
15
107
  let actualProtocol = 'http';
16
108
  let actualPort = toIntegerPort(options.port);
@@ -48,10 +140,9 @@ function resolveServerConnection(ctx, fsImpl, pathImpl, options) {
48
140
  throw new Error('Invalid or missing port configuration for RouteCodex server');
49
141
  }
50
142
  const configuredApiKey = (typeof options.apikey === 'string' && options.apikey.trim() ? options.apikey.trim() : null) ??
51
- (typeof ctx.env.ROUTECODEX_APIKEY === 'string' && ctx.env.ROUTECODEX_APIKEY.trim()
52
- ? ctx.env.ROUTECODEX_APIKEY.trim()
143
+ (typeof ctx.env.ROUTECODEX_HTTP_APIKEY === 'string' && ctx.env.ROUTECODEX_HTTP_APIKEY.trim()
144
+ ? ctx.env.ROUTECODEX_HTTP_APIKEY.trim()
53
145
  : null) ??
54
- (typeof ctx.env.RCC_APIKEY === 'string' && ctx.env.RCC_APIKEY.trim() ? ctx.env.RCC_APIKEY.trim() : null) ??
55
146
  readConfigApiKey(fsImpl, configPath);
56
147
  const connectHost = normalizeConnectHost(actualHost);
57
148
  const portPart = actualPort ? `:${actualPort}` : '';
@@ -69,43 +160,82 @@ function resolveServerConnection(ctx, fsImpl, pathImpl, options) {
69
160
  };
70
161
  }
71
162
  async function checkServerReady(ctx, serverUrl, apiKey, timeoutMs = 2500) {
72
- try {
73
- const headers = apiKey ? { 'x-api-key': apiKey } : undefined;
74
- const probe = async (pathSuffix) => {
75
- const controller = new AbortController();
76
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
163
+ const headers = apiKey ? { 'x-api-key': apiKey } : undefined;
164
+ const probeTargets = resolveServerProbeTargets(serverUrl);
165
+ for (let attempt = 0; attempt < 2; attempt += 1) {
166
+ for (const target of probeTargets) {
77
167
  try {
78
- const response = await ctx.fetch(`${serverUrl}${pathSuffix}`, {
79
- signal: controller.signal,
80
- method: 'GET',
81
- headers
82
- }).catch(() => null);
83
- if (!response || !response.ok) {
84
- return { ok: false, body: null };
168
+ const healthProbe = await probeServerState(ctx, `${target}/health`, headers, timeoutMs);
169
+ if (healthProbe.ok) {
170
+ const status = typeof healthProbe.body?.status === 'string' ? healthProbe.body.status.toLowerCase() : '';
171
+ if (status === 'ok' ||
172
+ status === 'ready' ||
173
+ healthProbe.body?.ready === true ||
174
+ healthProbe.body?.pipelineReady === true ||
175
+ healthProbe.body === null) {
176
+ return true;
177
+ }
178
+ }
179
+ const readyProbe = await probeServerState(ctx, `${target}/ready`, headers, timeoutMs);
180
+ if (readyProbe.ok) {
181
+ const status = typeof readyProbe.body?.status === 'string' ? readyProbe.body.status.toLowerCase() : '';
182
+ if (status === 'ready' || readyProbe.body?.ready === true || readyProbe.body === null) {
183
+ return true;
184
+ }
85
185
  }
86
- const body = await response.json().catch(() => null);
87
- return { ok: true, body };
88
- }
89
- finally {
90
- clearTimeout(timeoutId);
91
186
  }
92
- };
93
- const readyProbe = await probe('/ready');
94
- if (readyProbe.ok) {
95
- const status = typeof readyProbe.body?.status === 'string' ? readyProbe.body.status : '';
96
- if (status.toLowerCase() === 'ready' || readyProbe.body?.ready === true) {
97
- return true;
187
+ catch {
188
+ // try next target
98
189
  }
99
190
  }
100
- const healthProbe = await probe('/health');
101
- if (!healthProbe.ok) {
102
- return false;
191
+ if (attempt < 1) {
192
+ await new Promise((resolve) => setTimeout(resolve, 150));
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+ function resolveServerProbeTargets(serverUrl) {
198
+ const out = [];
199
+ const seen = new Set();
200
+ const pushTarget = (value) => {
201
+ const normalized = value.trim().replace(/\/+$/, '');
202
+ if (!normalized || seen.has(normalized)) {
203
+ return;
204
+ }
205
+ seen.add(normalized);
206
+ out.push(normalized);
207
+ };
208
+ pushTarget(serverUrl);
209
+ try {
210
+ const parsed = new URL(serverUrl);
211
+ if (parsed.hostname === '0.0.0.0' || parsed.hostname === '::' || parsed.hostname === '::1' || parsed.hostname === 'localhost') {
212
+ const loopback = new URL(serverUrl);
213
+ loopback.hostname = '127.0.0.1';
214
+ pushTarget(loopback.toString());
103
215
  }
104
- const status = typeof healthProbe.body?.status === 'string' ? healthProbe.body.status.toLowerCase() : '';
105
- return status === 'ok' || status === 'ready' || healthProbe.body?.ready === true || healthProbe.body?.pipelineReady === true;
106
216
  }
107
217
  catch {
108
- return false;
218
+ // ignore invalid URL parse; keep original
219
+ }
220
+ return out;
221
+ }
222
+ async function probeServerState(ctx, url, headers, timeoutMs) {
223
+ const controller = new AbortController();
224
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
225
+ try {
226
+ const response = await ctx.fetch(url, {
227
+ signal: controller.signal,
228
+ method: 'GET',
229
+ headers
230
+ }).catch(() => null);
231
+ if (!response || !response.ok) {
232
+ return { ok: false, body: null };
233
+ }
234
+ const body = await response.json().catch(() => null);
235
+ return { ok: true, body };
236
+ }
237
+ finally {
238
+ clearTimeout(timeoutId);
109
239
  }
110
240
  }
111
241
  function rotateLogFile(fsImpl, filePath, maxBytes = 8 * 1024 * 1024, maxBackups = 3) {
@@ -154,10 +284,13 @@ function ensureServerLogPath(ctx, fsImpl, pathImpl, port) {
154
284
  rotateLogFile(fsImpl, logPath);
155
285
  return logPath;
156
286
  }
157
- async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved) {
287
+ async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved, allowAutoStartServer) {
158
288
  const alreadyReady = await checkServerReady(ctx, resolved.serverUrl, resolved.configuredApiKey);
159
289
  if (alreadyReady) {
160
- return { started: false };
290
+ return { started: false, ready: true };
291
+ }
292
+ if (!allowAutoStartServer) {
293
+ return { started: false, ready: false };
161
294
  }
162
295
  const hasExplicitUrl = typeof options.url === 'string' && options.url.trim().length > 0;
163
296
  if (hasExplicitUrl) {
@@ -166,12 +299,24 @@ async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolv
166
299
  spinner.info('RouteCodex server is not running, starting it in background...');
167
300
  const logPath = ensureServerLogPath(ctx, fsImpl, pathImpl, resolved.port);
168
301
  const logFd = fsImpl.openSync(logPath, 'a');
302
+ // Launcher auto-started server follows launcher lifecycle by default.
303
+ // This is intentionally different from `routecodex start`, which is persistent by default.
304
+ const bindServerToParent = resolveBoolFromEnv(ctx.env.ROUTECODEX_LAUNCHER_SERVER_PARENT_GUARD
305
+ ?? ctx.env.RCC_LAUNCHER_SERVER_PARENT_GUARD
306
+ ?? ctx.env.ROUTECODEX_SERVER_PARENT_GUARD
307
+ ?? ctx.env.RCC_SERVER_PARENT_GUARD, true);
169
308
  const env = {
170
309
  ...ctx.env,
171
310
  ROUTECODEX_CONFIG: resolved.configPath,
172
311
  ROUTECODEX_CONFIG_PATH: resolved.configPath,
173
312
  ROUTECODEX_PORT: String(resolved.port),
174
- RCC_PORT: String(resolved.port)
313
+ RCC_PORT: String(resolved.port),
314
+ ...(bindServerToParent
315
+ ? {
316
+ ROUTECODEX_EXPECT_PARENT_PID: String(process.pid),
317
+ RCC_EXPECT_PARENT_PID: String(process.pid)
318
+ }
319
+ : {})
175
320
  };
176
321
  logProcessLifecycle({
177
322
  event: 'detached_spawn',
@@ -264,7 +409,7 @@ async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolv
264
409
  await ctx.sleep(1000);
265
410
  const ready = await checkServerReady(ctx, resolved.serverUrl, resolved.configuredApiKey, 1500);
266
411
  if (ready) {
267
- return { started: true, logPath };
412
+ return { started: true, ready: true, logPath };
268
413
  }
269
414
  }
270
415
  logProcessLifecycle({
@@ -326,6 +471,18 @@ function resolveCurrentTmuxTarget(env, spawnSyncImpl = spawnSync) {
326
471
  return null;
327
472
  }
328
473
  }
474
+ function inferTmuxSessionIdFromTarget(tmuxTarget) {
475
+ const normalized = String(tmuxTarget || '').trim();
476
+ if (!normalized) {
477
+ return null;
478
+ }
479
+ const index = normalized.indexOf(':');
480
+ if (index <= 0) {
481
+ return null;
482
+ }
483
+ const sessionName = normalized.slice(0, index).trim();
484
+ return sessionName || null;
485
+ }
329
486
  function isReusableTmuxPaneTarget(spawnSyncImpl, tmuxTarget, cwd) {
330
487
  const normalizedTarget = String(tmuxTarget || '').trim();
331
488
  if (!normalizedTarget) {
@@ -448,129 +605,104 @@ function normalizePathForComparison(candidate) {
448
605
  return raw;
449
606
  }
450
607
  }
451
- function findReusableManagedTmuxSession(spawnSyncImpl, cwd, commandName) {
452
- const expectedCwd = normalizePathForComparison(cwd);
453
- const expectedSessionPrefix = `rcc_${normalizeSessionToken(commandName)}_`;
608
+ function formatHmms(value) {
609
+ const pad = (n) => String(n).padStart(2, '0');
610
+ return `${pad(value.getHours())}${pad(value.getMinutes())}${pad(value.getSeconds())}`;
611
+ }
612
+ function tmuxSessionExists(spawnSyncImpl, sessionName) {
454
613
  try {
455
- const listResult = spawnSyncImpl('tmux', ['list-sessions', '-F', '#S #{session_attached}'], { encoding: 'utf8' });
456
- if (listResult.status !== 0) {
457
- return null;
458
- }
459
- const lines = String(listResult.stdout || '')
460
- .split(/\r?\n/)
461
- .map((line) => line.trim())
462
- .filter(Boolean);
463
- for (const line of lines) {
464
- const [sessionName, attachedFlag] = line.split(' ');
465
- const normalizedName = String(sessionName || '').trim();
466
- if (!normalizedName.startsWith(expectedSessionPrefix)) {
467
- continue;
468
- }
469
- if (String(attachedFlag || '').trim() === '1') {
470
- continue;
471
- }
472
- const panesResult = spawnSyncImpl('tmux', [
473
- 'list-panes',
474
- '-t',
475
- normalizedName,
476
- '-F',
477
- '#{session_name}:#{window_index}.#{pane_index} #{pane_current_command} #{pane_current_path}'
478
- ], { encoding: 'utf8' });
479
- if (panesResult.status !== 0) {
480
- continue;
481
- }
482
- const panes = String(panesResult.stdout || '')
483
- .split(/\r?\n/)
484
- .map((paneLine) => paneLine.trim())
485
- .filter(Boolean);
486
- for (const pane of panes) {
487
- const [target, command, panePath] = pane.split(' ');
488
- const tmuxTarget = String(target || '').trim();
489
- const currentCommand = String(command || '').trim().toLowerCase();
490
- const normalizedPanePath = normalizePathForComparison(String(panePath || '').trim());
491
- if (!tmuxTarget) {
492
- continue;
493
- }
494
- if (!isReusableIdlePaneCommand(currentCommand)) {
495
- continue;
496
- }
497
- if (!normalizedPanePath || normalizedPanePath !== expectedCwd) {
498
- continue;
499
- }
500
- return { sessionName: normalizedName, tmuxTarget };
501
- }
502
- }
503
- return null;
614
+ const result = spawnSyncImpl('tmux', ['has-session', '-t', sessionName], { encoding: 'utf8' });
615
+ return result.status === 0;
504
616
  }
505
617
  catch {
506
- return null;
618
+ return false;
507
619
  }
508
620
  }
509
- function createManagedTmuxSession(args) {
510
- const { spawnSyncImpl, cwd, commandName } = args;
511
- const reusable = findReusableManagedTmuxSession(spawnSyncImpl, cwd, commandName);
512
- if (reusable) {
513
- return {
514
- sessionName: reusable.sessionName,
515
- tmuxTarget: reusable.tmuxTarget,
516
- reused: true,
517
- stop: () => {
518
- try {
519
- spawnSyncImpl('tmux', ['kill-session', '-t', reusable.sessionName], { encoding: 'utf8' });
520
- }
521
- catch {
522
- // ignore
523
- }
524
- }
525
- };
621
+ function buildManagedTmuxSessionName(nowMs, attempt) {
622
+ const stamp = formatHmms(new Date(nowMs + attempt * 1000));
623
+ return `rcc-tmux-${stamp}`;
624
+ }
625
+ function requestManagedTmuxSessionExit(spawnSyncImpl, sessionName) {
626
+ const target = String(sessionName || '').trim();
627
+ if (!target) {
628
+ return;
526
629
  }
527
- const sessionName = (() => {
528
- const token = normalizeSessionToken(commandName);
529
- return `rcc_${token}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
530
- })();
531
630
  try {
532
- const result = spawnSyncImpl('tmux', ['new-session', '-d', '-s', sessionName, '-c', cwd], { encoding: 'utf8' });
533
- if (result.status !== 0) {
534
- return null;
535
- }
631
+ spawnSyncImpl('tmux', ['send-keys', '-t', target, '-X', 'cancel'], { encoding: 'utf8' });
536
632
  }
537
633
  catch {
538
- return null;
634
+ // ignore
539
635
  }
540
- const tmuxTarget = `${sessionName}:0.0`;
541
- return {
542
- sessionName,
543
- tmuxTarget,
544
- reused: false,
545
- stop: () => {
546
- try {
547
- spawnSyncImpl('tmux', ['kill-session', '-t', sessionName], { encoding: 'utf8' });
548
- }
549
- catch {
550
- // ignore
636
+ try {
637
+ spawnSyncImpl('tmux', ['send-keys', '-t', target, 'C-c'], { encoding: 'utf8' });
638
+ }
639
+ catch {
640
+ // ignore
641
+ }
642
+ try {
643
+ spawnSyncImpl('tmux', ['send-keys', '-t', target, '-l', '--', 'exit'], { encoding: 'utf8' });
644
+ }
645
+ catch {
646
+ // ignore
647
+ }
648
+ try {
649
+ sendTmuxSubmitKey(spawnSyncImpl, target);
650
+ }
651
+ catch {
652
+ // ignore
653
+ }
654
+ }
655
+ function createManagedTmuxSession(args) {
656
+ const { spawnSyncImpl, cwd } = args;
657
+ const baseNow = Date.now();
658
+ for (let attempt = 0; attempt < 6; attempt += 1) {
659
+ const sessionName = buildManagedTmuxSessionName(baseNow, attempt);
660
+ if (tmuxSessionExists(spawnSyncImpl, sessionName)) {
661
+ continue;
662
+ }
663
+ try {
664
+ const result = spawnSyncImpl('tmux', ['new-session', '-d', '-s', sessionName, '-c', cwd], { encoding: 'utf8' });
665
+ if (result.status !== 0) {
666
+ continue;
551
667
  }
552
668
  }
553
- };
669
+ catch {
670
+ continue;
671
+ }
672
+ const tmuxTarget = `${sessionName}:0.0`;
673
+ return {
674
+ sessionName,
675
+ tmuxTarget,
676
+ reused: false,
677
+ stop: () => {
678
+ requestManagedTmuxSessionExit(spawnSyncImpl, sessionName);
679
+ }
680
+ };
681
+ }
682
+ return null;
554
683
  }
555
684
  function launchCommandInTmuxPane(args) {
556
685
  const { spawnSyncImpl, tmuxTarget, cwd, command, commandName, commandArgs, envOverrides, selfHealPolicy } = args;
686
+ const tmuxSessionName = (() => {
687
+ const idx = String(tmuxTarget || '').indexOf(':');
688
+ const name = idx >= 0 ? String(tmuxTarget).slice(0, idx) : String(tmuxTarget || '');
689
+ return name.trim();
690
+ })();
557
691
  const envTokens = [
558
692
  ...envOverrides.unset.flatMap((key) => ['-u', key]),
559
693
  ...envOverrides.set.map(([key, value]) => `${key}=${value}`)
560
694
  ];
561
695
  const baseCommand = buildShellCommand(['env', ...envTokens, command, ...commandArgs]);
562
- // Keep the managed tmux session alive when the client process exits.
563
- // Session cleanup is handled by managed heartbeat/reaper logic, not by inline shell self-kill.
564
- const shellCommand = (() => {
696
+ const commandBody = (() => {
565
697
  if (!selfHealPolicy.enabled || selfHealPolicy.maxRetries <= 0) {
566
- return `cd -- ${shellQuote(cwd)} && ${baseCommand}`;
698
+ return `cd -- ${shellQuote(cwd)} || exit 1; ${baseCommand}; __rcc_exit=$?`;
567
699
  }
568
700
  const safeCommandName = shellQuote(commandName || command || 'client');
569
701
  const loopBody = [
570
702
  `${baseCommand}`,
571
703
  '__rcc_exit=$?',
572
- 'if [ "$__rcc_exit" -eq 0 ] || [ "$__rcc_exit" -eq 130 ] || [ "$__rcc_exit" -eq 143 ]; then exit "$__rcc_exit"; fi',
573
- 'if [ "$__rcc_try" -ge "$__rcc_max" ]; then exit "$__rcc_exit"; fi',
704
+ 'if [ "$__rcc_exit" -eq 0 ] || [ "$__rcc_exit" -eq 130 ] || [ "$__rcc_exit" -eq 143 ]; then break; fi',
705
+ 'if [ "$__rcc_try" -ge "$__rcc_max" ]; then break; fi',
574
706
  '__rcc_try=$((__rcc_try + 1))',
575
707
  `echo "[routecodex][self-heal] ${safeCommandName} exited with code $__rcc_exit; retry $__rcc_try/$__rcc_max in $__rcc_delay s" >&2`,
576
708
  'sleep "$__rcc_delay"'
@@ -583,6 +715,14 @@ function launchCommandInTmuxPane(args) {
583
715
  `while true; do ${loopBody}; done`
584
716
  ].join('; ');
585
717
  })();
718
+ // Client lifecycle owns managed tmux lifecycle: once command exits, destroy session.
719
+ const shellCommand = [
720
+ commandBody,
721
+ tmuxSessionName
722
+ ? `tmux kill-session -t ${shellQuote(tmuxSessionName)} >/dev/null 2>&1 || true`
723
+ : ':',
724
+ 'exit "$__rcc_exit"'
725
+ ].join('; ');
586
726
  try {
587
727
  // Prefer respawn-pane for deterministic execution in managed sessions.
588
728
  // This avoids flaky "typed but not submitted" behavior from send-keys on some terminals.
@@ -639,21 +779,32 @@ function sendJson(res, status, payload) {
639
779
  res.setHeader('content-type', 'application/json');
640
780
  res.end(body);
641
781
  }
642
- async function startClockClientService(args) {
782
+ function normalizeTmuxInjectedText(raw) {
783
+ return raw
784
+ .replace(/\r\n?/g, '\n')
785
+ .split('\n')
786
+ .map((line) => line.trim())
787
+ .filter((line) => line.length > 0)
788
+ .join(' ')
789
+ .trim();
790
+ }
791
+ async function startSessionClientService(args) {
643
792
  const { ctx, resolved, workdir, tmuxTarget, spawnSyncImpl, clientType, managedTmuxSession, getManagedProcessState } = args;
644
793
  const daemonId = (() => {
645
794
  try {
646
- return `clockd_${crypto.randomUUID()}`;
795
+ return `sessiond_${crypto.randomUUID()}`;
647
796
  }
648
797
  catch {
649
- return `clockd_${Date.now()}_${Math.random().toString(16).slice(2)}`;
798
+ return `sessiond_${Date.now()}_${Math.random().toString(16).slice(2)}`;
650
799
  }
651
800
  })();
652
801
  const normalizedTmuxTarget = String(tmuxTarget || '').trim();
802
+ if (!normalizedTmuxTarget) {
803
+ // No tmux target means no reliable stdin injection path.
804
+ // Do not register a session-client daemon with a synthetic session id.
805
+ return null;
806
+ }
653
807
  const tmuxSessionId = (() => {
654
- if (!normalizedTmuxTarget) {
655
- return daemonId;
656
- }
657
808
  const idx = normalizedTmuxTarget.indexOf(':');
658
809
  const candidate = (idx >= 0 ? normalizedTmuxTarget.slice(0, idx) : normalizedTmuxTarget).trim();
659
810
  return candidate || daemonId;
@@ -667,12 +818,14 @@ async function startClockClientService(args) {
667
818
  return;
668
819
  }
669
820
  const body = await readJsonBody(req);
670
- const text = typeof body.text === 'string' ? body.text.trim() : '';
821
+ const text = typeof body.text === 'string' ? normalizeTmuxInjectedText(body.text) : '';
671
822
  if (!text) {
672
823
  sendJson(res, 400, { ok: false, message: 'text is required' });
673
824
  return;
674
825
  }
675
826
  try {
827
+ // Ensure pane is not stuck in copy-mode before literal injection + submit.
828
+ spawnSyncImpl('tmux', ['send-keys', '-t', normalizedTmuxTarget, '-X', 'cancel'], { encoding: 'utf8' });
676
829
  const literal = spawnSyncImpl('tmux', ['send-keys', '-t', normalizedTmuxTarget, '-l', '--', text], { encoding: 'utf8' });
677
830
  if (literal.status !== 0) {
678
831
  sendJson(res, 500, {
@@ -701,7 +854,7 @@ async function startClockClientService(args) {
701
854
  server?.listen(0, '127.0.0.1', () => {
702
855
  const address = server?.address();
703
856
  if (!address || typeof address === 'string') {
704
- reject(new Error('failed to resolve clock daemon callback address'));
857
+ reject(new Error('failed to resolve session daemon callback address'));
705
858
  return;
706
859
  }
707
860
  resolve(address.port);
@@ -719,6 +872,7 @@ async function startClockClientService(args) {
719
872
  callbackUrl = `http://127.0.0.1:${port}/inject`;
720
873
  }
721
874
  const controlUrl = `${resolved.protocol}://127.0.0.1:${resolved.port}${resolved.basePath}`;
875
+ const controlRequestTimeoutMs = resolveIntFromEnv(ctx.env.ROUTECODEX_SESSION_CLIENT_CONTROL_TIMEOUT_MS ?? ctx.env.RCC_SESSION_CLIENT_CONTROL_TIMEOUT_MS, 1500, 200, 30_000);
722
876
  const normalizeManagedProcessPayload = () => {
723
877
  const state = typeof getManagedProcessState === 'function' ? getManagedProcessState() : undefined;
724
878
  const managedClientProcess = state?.managedClientProcess === true;
@@ -735,19 +889,39 @@ async function startClockClientService(args) {
735
889
  };
736
890
  };
737
891
  const post = async (pathSuffix, payload) => {
892
+ const abortController = typeof AbortController !== 'undefined' ? new AbortController() : null;
893
+ const timeoutHandle = abortController
894
+ ? setTimeout(() => {
895
+ try {
896
+ abortController.abort();
897
+ }
898
+ catch {
899
+ // ignore abort failures
900
+ }
901
+ }, controlRequestTimeoutMs)
902
+ : null;
903
+ if (timeoutHandle && typeof timeoutHandle.unref === 'function') {
904
+ timeoutHandle.unref();
905
+ }
738
906
  try {
739
907
  const response = await ctx.fetch(`${controlUrl}${pathSuffix}`, {
740
908
  method: 'POST',
741
909
  headers: { 'content-type': 'application/json' },
742
- body: JSON.stringify(payload)
910
+ body: JSON.stringify(payload),
911
+ ...(abortController ? { signal: abortController.signal } : {})
743
912
  });
744
913
  return { ok: response.ok, status: response.status };
745
914
  }
746
915
  catch {
747
916
  return { ok: false, status: 0 };
748
917
  }
918
+ finally {
919
+ if (timeoutHandle) {
920
+ clearTimeout(timeoutHandle);
921
+ }
922
+ }
749
923
  };
750
- const reRegisterBackoffMs = resolveIntFromEnv(ctx.env.ROUTECODEX_CLOCK_CLIENT_REREGISTER_BACKOFF_MS ?? ctx.env.RCC_CLOCK_CLIENT_REREGISTER_BACKOFF_MS, 1500, 200, 60_000);
924
+ const reRegisterBackoffMs = resolveIntFromEnv(ctx.env.ROUTECODEX_SESSION_CLIENT_REREGISTER_BACKOFF_MS ?? ctx.env.RCC_SESSION_CLIENT_REREGISTER_BACKOFF_MS, 1500, 200, 60_000);
751
925
  let registerInFlight = null;
752
926
  let lastRegisterAttemptAtMs = 0;
753
927
  const registerDaemon = async () => {
@@ -756,7 +930,7 @@ async function startClockClientService(args) {
756
930
  }
757
931
  registerInFlight = (async () => {
758
932
  lastRegisterAttemptAtMs = Date.now();
759
- const result = await post('/daemon/clock-client/register', {
933
+ const result = await post('/daemon/session-client/register', {
760
934
  daemonId,
761
935
  tmuxSessionId,
762
936
  sessionId: tmuxSessionId,
@@ -777,7 +951,7 @@ async function startClockClientService(args) {
777
951
  }
778
952
  };
779
953
  const syncHeartbeat = async () => {
780
- const heartbeat = await post('/daemon/clock-client/heartbeat', {
954
+ const heartbeat = await post('/daemon/session-client/heartbeat', {
781
955
  daemonId,
782
956
  tmuxSessionId,
783
957
  sessionId: tmuxSessionId,
@@ -824,7 +998,7 @@ async function startClockClientService(args) {
824
998
  syncHeartbeat,
825
999
  stop: async () => {
826
1000
  clearInterval(heartbeat);
827
- await post('/daemon/clock-client/unregister', { daemonId });
1001
+ await post('/daemon/session-client/unregister', { daemonId });
828
1002
  if (!server) {
829
1003
  return;
830
1004
  }
@@ -912,10 +1086,24 @@ export function createLauncherCommand(program, ctx, spec) {
912
1086
  command.action(async (extraArgs = [], options) => {
913
1087
  const spinner = await ctx.createSpinner(`Preparing ${spec.displayName} with RouteCodex...`);
914
1088
  try {
915
- const resolved = resolveServerConnection(ctx, fsImpl, pathImpl, options);
916
- const ensureResult = await ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved);
1089
+ const tmuxOnly = spec.commandName === 'codex';
1090
+ const resolved = tmuxOnly ? undefined : resolveServerConnection(ctx, fsImpl, pathImpl, options);
1091
+ const requireResolved = () => {
1092
+ if (!resolved) {
1093
+ throw new Error('RouteCodex server connection is not available for this launcher');
1094
+ }
1095
+ return resolved;
1096
+ };
1097
+ let ensureResult = null;
1098
+ if (!tmuxOnly) {
1099
+ const server = requireResolved();
1100
+ await ctx.ensureGuardianDaemon?.();
1101
+ ensureResult = await ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, server, spec.allowAutoStartServer === true);
1102
+ if (!ensureResult.ready) {
1103
+ spinner.info('RouteCodex server is not running; launcher will continue and wait for your next requests.');
1104
+ }
1105
+ }
917
1106
  spinner.text = `Launching ${spec.displayName}...`;
918
- const baseUrl = `${resolved.protocol}://${resolved.connectHost}${resolved.portPart}${resolved.basePath}`;
919
1107
  const currentCwd = resolveWorkingDirectory(ctx, fsImpl, pathImpl, options.cwd);
920
1108
  const spawnSyncImpl = ctx.spawnSyncImpl ?? spawnSync;
921
1109
  const tmuxSelfHealPolicy = resolveTmuxSelfHealPolicy(ctx.env);
@@ -975,87 +1163,170 @@ export function createLauncherCommand(program, ctx, spec) {
975
1163
  let managedTmuxSession = null;
976
1164
  const tmuxEnabled = isTmuxAvailable(spawnSyncImpl);
977
1165
  if (!tmuxEnabled) {
978
- ctx.logger.warning('[clock-advanced] tmux not found; advanced clock client service disabled (launcher will continue).');
1166
+ ctx.logger.warning('[session-advanced] tmux not found; advanced session client service disabled (launcher will continue).');
979
1167
  }
980
- let tmuxTarget = tmuxEnabled ? resolveCurrentTmuxTarget(ctx.env, spawnSyncImpl) : null;
981
- if (tmuxTarget && !isReusableTmuxPaneTarget(spawnSyncImpl, tmuxTarget, currentCwd)) {
982
- tmuxTarget = null;
1168
+ let tmuxTarget = null;
1169
+ if (tmuxEnabled && !tmuxOnly) {
1170
+ tmuxTarget = resolveCurrentTmuxTarget(ctx.env, spawnSyncImpl);
983
1171
  }
984
- if (tmuxEnabled && !tmuxTarget) {
1172
+ if (tmuxEnabled && (tmuxOnly || !tmuxTarget)) {
985
1173
  managedTmuxSession = createManagedTmuxSession({
986
1174
  spawnSyncImpl,
987
- cwd: currentCwd,
988
- commandName: spec.commandName
1175
+ cwd: currentCwd
989
1176
  });
990
1177
  if (managedTmuxSession) {
991
1178
  tmuxTarget = managedTmuxSession.tmuxTarget;
992
- if (managedTmuxSession.reused) {
993
- ctx.logger.info('[clock-advanced] reused existing managed tmux session and rebound launcher automatically.');
994
- }
995
- else {
996
- ctx.logger.info('[clock-advanced] started managed tmux session automatically; no manual tmux setup needed.');
997
- }
1179
+ ctx.logger.info('[session-advanced] started managed tmux session automatically; no manual tmux setup needed.');
998
1180
  }
999
1181
  else {
1000
- ctx.logger.warning('[clock-advanced] failed to start managed tmux session; launcher continues without advanced mode.');
1182
+ ctx.logger.warning('[session-advanced] failed to start managed tmux session; launcher continues without advanced mode.');
1001
1183
  }
1002
1184
  }
1003
1185
  const managedClientProcessEnabled = !managedTmuxSession;
1004
1186
  let managedClientPid = null;
1005
1187
  const managedClientCommandHint = managedClientProcessEnabled ? resolvedBinary : undefined;
1006
- const reclaimRequiredRaw = String(ctx.env.ROUTECODEX_CLOCK_RECLAIM_REQUIRED
1007
- ?? ctx.env.RCC_CLOCK_RECLAIM_REQUIRED
1188
+ const reclaimRequiredRaw = String(ctx.env.ROUTECODEX_SESSION_RECLAIM_REQUIRED
1189
+ ?? ctx.env.RCC_SESSION_RECLAIM_REQUIRED
1008
1190
  ?? '1')
1009
1191
  .trim()
1010
1192
  .toLowerCase();
1011
1193
  const reclaimRequired = reclaimRequiredRaw !== '0' && reclaimRequiredRaw !== 'false' && reclaimRequiredRaw !== 'no';
1012
- const clockClientService = await startClockClientService({
1013
- ctx,
1014
- resolved,
1015
- workdir: currentCwd,
1016
- tmuxTarget,
1017
- spawnSyncImpl,
1018
- clientType: spec.commandName,
1019
- managedTmuxSession: Boolean(managedTmuxSession),
1020
- getManagedProcessState: () => ({
1021
- managedClientProcess: managedClientProcessEnabled,
1022
- managedClientPid,
1023
- managedClientCommandHint
1024
- })
1025
- });
1026
- if (managedClientProcessEnabled && reclaimRequired && !clockClientService) {
1027
- throw new Error('clock client registration failed for managed child process; aborting launch to avoid orphan process');
1194
+ let sessionClientService = null;
1195
+ let sessionAdvancedEnabled = false;
1196
+ let inferredTmuxSessionId;
1197
+ let inferredDaemonId;
1198
+ let sessionClientApiKey;
1199
+ let tmuxOnlySessionId;
1200
+ if (!tmuxOnly) {
1201
+ const server = requireResolved();
1202
+ sessionClientService = await startSessionClientService({
1203
+ ctx,
1204
+ resolved: server,
1205
+ workdir: currentCwd,
1206
+ tmuxTarget,
1207
+ spawnSyncImpl,
1208
+ clientType: spec.commandName,
1209
+ managedTmuxSession: Boolean(managedTmuxSession),
1210
+ getManagedProcessState: () => ({
1211
+ managedClientProcess: managedClientProcessEnabled,
1212
+ managedClientPid,
1213
+ managedClientCommandHint
1214
+ })
1215
+ });
1216
+ if (managedClientProcessEnabled && reclaimRequired && tmuxTarget && !sessionClientService) {
1217
+ throw new Error('session client registration failed for managed child process; aborting launch to avoid orphan process');
1218
+ }
1219
+ if (tmuxTarget && !sessionClientService) {
1220
+ ctx.logger.warning('[session-advanced] failed to start session client daemon service; launcher continues without advanced mode.');
1221
+ }
1222
+ sessionAdvancedEnabled = Boolean(sessionClientService && tmuxTarget);
1223
+ inferredTmuxSessionId =
1224
+ sessionClientService?.tmuxSessionId ||
1225
+ inferTmuxSessionIdFromTarget(tmuxTarget) ||
1226
+ undefined;
1227
+ inferredDaemonId =
1228
+ sessionClientService?.daemonId ||
1229
+ (inferredTmuxSessionId ? `sessiond_unbound_${process.pid}` : undefined);
1230
+ sessionClientApiKey =
1231
+ inferredTmuxSessionId && inferredDaemonId
1232
+ ? encodeSessionClientApiKey(server.configuredApiKey || 'rcc-proxy-key', inferredDaemonId, inferredTmuxSessionId)
1233
+ : (server.configuredApiKey || 'rcc-proxy-key');
1234
+ if (isSessionScopeTraceEnabled()) {
1235
+ try {
1236
+ const parsedDaemonId = extractSessionClientDaemonIdFromApiKey(sessionClientApiKey) || 'none';
1237
+ const parsedTmuxSessionId = extractSessionClientScopeIdFromApiKey(sessionClientApiKey) || 'none';
1238
+ const verbose = isSessionScopeTraceVerbose();
1239
+ ctx.logger.info(`[session-scope][launch] command=${spec.commandName} advanced=${sessionAdvancedEnabled ? 'on' : 'off'} ` +
1240
+ `daemon=${parsedDaemonId} tmux=${parsedTmuxSessionId} tmuxTarget=${tmuxTarget || 'none'}` +
1241
+ (verbose ? ` managedTmux=${managedTmuxSession ? 'yes' : 'no'} serverStarted=${ensureResult?.started ? 'yes' : 'no'}` : ''));
1242
+ }
1243
+ catch {
1244
+ // best-effort diagnostics only
1245
+ }
1246
+ }
1247
+ await ctx.registerGuardianProcess?.({
1248
+ source: spec.commandName,
1249
+ pid: process.pid,
1250
+ ppid: process.ppid,
1251
+ port: server.port,
1252
+ tmuxSessionId: sessionClientService?.tmuxSessionId || inferTmuxSessionIdFromTarget(tmuxTarget) || undefined,
1253
+ tmuxTarget: tmuxTarget || undefined,
1254
+ metadata: {
1255
+ workingDirectory: currentCwd,
1256
+ binary: resolvedBinary,
1257
+ managedTmuxSession: Boolean(managedTmuxSession),
1258
+ autoStartedServer: ensureResult?.started === true
1259
+ }
1260
+ });
1028
1261
  }
1029
- if (tmuxTarget && !clockClientService) {
1030
- ctx.logger.warning('[clock-advanced] failed to start clock client daemon service; launcher continues without advanced mode.');
1262
+ const applyLifecycleOrThrow = async (args) => {
1263
+ if (tmuxOnly || !resolved) {
1264
+ return;
1265
+ }
1266
+ const server = requireResolved();
1267
+ const accepted = await ctx.reportGuardianLifecycle?.({
1268
+ action: args.action,
1269
+ source: `cli.launcher.${spec.commandName}`,
1270
+ actorPid: process.pid,
1271
+ targetPid: args.targetPid && args.targetPid > 0 ? args.targetPid : undefined,
1272
+ signal: args.signal,
1273
+ metadata: {
1274
+ port: server.port,
1275
+ serverUrl: server.serverUrl
1276
+ }
1277
+ });
1278
+ if (ctx.reportGuardianLifecycle && accepted !== true) {
1279
+ throw new Error(`guardian lifecycle apply rejected (${args.action})`);
1280
+ }
1281
+ };
1282
+ if (tmuxOnly) {
1283
+ const tmuxSessionId = inferTmuxSessionIdFromTarget(tmuxTarget) || '';
1284
+ tmuxOnlySessionId = tmuxSessionId || undefined;
1031
1285
  }
1032
- const clockAdvancedEnabled = Boolean(clockClientService && tmuxTarget);
1033
- const clockClientApiKey = clockAdvancedEnabled && clockClientService
1034
- ? encodeClockClientApiKey(resolved.configuredApiKey || 'rcc-proxy-key', clockClientService.daemonId)
1035
- : (resolved.configuredApiKey || 'rcc-proxy-key');
1036
- const toolEnv = spec.buildEnv({
1037
- env: {
1038
- ...ctx.env,
1039
- PWD: currentCwd,
1040
- RCC_WORKDIR: currentCwd,
1041
- ROUTECODEX_WORKDIR: currentCwd,
1042
- OPENAI_BASE_URL: normalizeOpenAiBaseUrl(baseUrl),
1043
- OPENAI_API_BASE: normalizeOpenAiBaseUrl(baseUrl),
1044
- OPENAI_API_BASE_URL: normalizeOpenAiBaseUrl(baseUrl),
1045
- OPENAI_API_KEY: clockClientApiKey,
1046
- RCC_CLOCK_ADVANCED_ENABLED: clockAdvancedEnabled ? '1' : '0',
1047
- ...(clockAdvancedEnabled && clockClientService
1048
- ? {
1049
- RCC_CLOCK_CLIENT_SESSION_ID: clockClientService.tmuxSessionId,
1050
- RCC_CLOCK_CLIENT_TMUX_SESSION_ID: clockClientService.tmuxSessionId,
1051
- RCC_CLOCK_CLIENT_DAEMON_ID: clockClientService.daemonId
1286
+ const toolEnv = (() => {
1287
+ if (tmuxOnly) {
1288
+ const env = { ...ctx.env };
1289
+ if (tmuxOnlySessionId) {
1290
+ const baseKey = (typeof env.ROUTECODEX_HTTP_APIKEY === 'string' && env.ROUTECODEX_HTTP_APIKEY.trim())
1291
+ || '';
1292
+ if (!baseKey) {
1293
+ ctx.logger.warning('[session-scope] ROUTECODEX_HTTP_APIKEY is empty; tmux scope not injected.');
1294
+ return env;
1052
1295
  }
1053
- : {})
1054
- },
1055
- baseUrl,
1056
- configuredApiKey: resolved.configuredApiKey,
1057
- cwd: currentCwd
1058
- });
1296
+ const scopedKey = encodeSessionClientApiKey(baseKey, '', tmuxOnlySessionId);
1297
+ env.ROUTECODEX_HTTP_APIKEY = scopedKey;
1298
+ env.OPENAI_API_KEY = scopedKey;
1299
+ env.ANTHROPIC_AUTH_TOKEN = scopedKey;
1300
+ }
1301
+ return env;
1302
+ }
1303
+ const server = requireResolved();
1304
+ return spec.buildEnv({
1305
+ env: {
1306
+ ...ctx.env,
1307
+ PWD: currentCwd,
1308
+ RCC_WORKDIR: currentCwd,
1309
+ ROUTECODEX_WORKDIR: currentCwd,
1310
+ OPENAI_BASE_URL: normalizeOpenAiBaseUrl(`${server.protocol}://${server.connectHost}${server.portPart}${server.basePath}`),
1311
+ OPENAI_API_BASE: normalizeOpenAiBaseUrl(`${server.protocol}://${server.connectHost}${server.portPart}${server.basePath}`),
1312
+ OPENAI_API_BASE_URL: normalizeOpenAiBaseUrl(`${server.protocol}://${server.connectHost}${server.portPart}${server.basePath}`),
1313
+ OPENAI_API_KEY: sessionClientApiKey,
1314
+ RCC_SESSION_ADVANCED_ENABLED: sessionAdvancedEnabled ? '1' : '0',
1315
+ ...(inferredTmuxSessionId
1316
+ ? {
1317
+ RCC_SESSION_CLIENT_SESSION_ID: inferredTmuxSessionId,
1318
+ RCC_SESSION_CLIENT_TMUX_SESSION_ID: inferredTmuxSessionId
1319
+ }
1320
+ : {}),
1321
+ ...(inferredDaemonId
1322
+ ? { RCC_SESSION_CLIENT_DAEMON_ID: inferredDaemonId }
1323
+ : {})
1324
+ },
1325
+ baseUrl: `${server.protocol}://${server.connectHost}${server.portPart}${server.basePath}`,
1326
+ configuredApiKey: server.configuredApiKey,
1327
+ cwd: currentCwd
1328
+ });
1329
+ })();
1059
1330
  const shouldUseShell = ctx.isWindows &&
1060
1331
  !pathImpl.extname(resolvedBinary) &&
1061
1332
  !resolvedBinary.includes('/') &&
@@ -1094,39 +1365,137 @@ export function createLauncherCommand(program, ctx, spec) {
1094
1365
  managedClientPid = typeof toolProcess.pid === 'number' && Number.isFinite(toolProcess.pid)
1095
1366
  ? Math.floor(toolProcess.pid)
1096
1367
  : null;
1097
- if (clockClientService && managedClientProcessEnabled && managedClientPid) {
1098
- void clockClientService.syncHeartbeat();
1368
+ if (sessionClientService && managedClientProcessEnabled && managedClientPid) {
1369
+ void sessionClientService.syncHeartbeat();
1099
1370
  }
1100
1371
  spinner.succeed(`${spec.displayName} launched with RouteCodex proxy`);
1101
1372
  if (!managedTmuxSession) {
1102
- ctx.logger.info(`Using RouteCodex server at: ${baseUrl}`);
1373
+ if (!tmuxOnly) {
1374
+ const server = requireResolved();
1375
+ ctx.logger.info(`Using RouteCodex server at: ${server.protocol}://${server.connectHost}${server.portPart}${server.basePath}`);
1376
+ }
1103
1377
  ctx.logger.info(`${spec.displayName} binary: ${resolvedBinary}`);
1104
- if (ensureResult.started && ensureResult.logPath) {
1378
+ if (!tmuxOnly && ensureResult?.started && ensureResult.logPath) {
1105
1379
  ctx.logger.info(`RouteCodex auto-start logs: ${ensureResult.logPath}`);
1106
1380
  }
1107
1381
  ctx.logger.info(`Working directory for ${spec.displayName}: ${currentCwd}`);
1108
1382
  ctx.logger.info(`Press Ctrl+C to exit ${spec.displayName}`);
1109
1383
  }
1110
- const shutdown = async (signal) => {
1384
+ let shutdownTriggered = false;
1385
+ let toolProcessClosing = false;
1386
+ let observedToolExitCode;
1387
+ let observedToolExitSignal = null;
1388
+ let requestedShutdownSignal = null;
1389
+ let clientExitSummaryLogged = false;
1390
+ const logClientExitSummary = () => {
1391
+ if (clientExitSummaryLogged || !shouldLogClientExitSummary(spec.commandName)) {
1392
+ return;
1393
+ }
1394
+ clientExitSummaryLogged = true;
1395
+ const codeLabel = typeof observedToolExitCode === 'number' && Number.isFinite(observedToolExitCode)
1396
+ ? String(observedToolExitCode)
1397
+ : 'n/a';
1398
+ const signalLabel = observedToolExitSignal || 'none';
1399
+ ctx.logger.info(`[client-exit] ${spec.displayName} exited (code=${codeLabel}, signal=${signalLabel})`);
1400
+ };
1401
+ const finalizeToolTermination = async (options) => {
1402
+ if (toolProcessClosing) {
1403
+ return;
1404
+ }
1405
+ toolProcessClosing = true;
1406
+ logClientExitSummary();
1111
1407
  try {
1112
- toolProcess.kill(signal);
1408
+ await sessionClientService?.stop();
1113
1409
  }
1114
1410
  catch {
1115
1411
  // ignore
1116
1412
  }
1117
1413
  try {
1118
- await clockClientService?.stop();
1414
+ if (managedTmuxSession && shouldStopManagedTmuxOnToolExit(ctx.env)) {
1415
+ managedTmuxSession.stop();
1416
+ }
1119
1417
  }
1120
1418
  catch {
1121
1419
  // ignore
1122
1420
  }
1123
1421
  try {
1124
- managedTmuxSession?.stop();
1422
+ await applyLifecycleOrThrow({
1423
+ action: 'launcher_tool_exit',
1424
+ signal: observedToolExitSignal ? String(observedToolExitSignal) : undefined,
1425
+ targetPid: toolProcess.pid ?? null
1426
+ });
1427
+ }
1428
+ catch {
1429
+ // ignore lifecycle logging errors in exit path
1430
+ }
1431
+ const forcedExitCode = options?.forceExitCode;
1432
+ if (typeof forcedExitCode === 'number' && Number.isFinite(forcedExitCode)) {
1433
+ ctx.exit(Math.max(0, Math.floor(forcedExitCode)));
1434
+ return;
1435
+ }
1436
+ if (requestedShutdownSignal || observedToolExitSignal) {
1437
+ ctx.exit(0);
1438
+ return;
1439
+ }
1440
+ ctx.exit(observedToolExitCode ?? 0);
1441
+ };
1442
+ const shutdown = async (signal) => {
1443
+ if (shutdownTriggered) {
1444
+ return;
1445
+ }
1446
+ shutdownTriggered = true;
1447
+ requestedShutdownSignal = signal;
1448
+ const targetGuard = canSignalOwnedToolProcess({
1449
+ env: ctx.env,
1450
+ pid: toolProcess.pid ?? null,
1451
+ expectedParentPid: process.pid,
1452
+ commandHint: resolvedBinary
1453
+ });
1454
+ logProcessLifecycle({
1455
+ event: 'launcher_signal_guard',
1456
+ source: 'cli.launcher.shutdown',
1457
+ details: {
1458
+ commandName: spec.commandName,
1459
+ signal,
1460
+ targetPid: toolProcess.pid ?? null,
1461
+ result: targetGuard.ok ? 'allowed' : 'blocked',
1462
+ reason: targetGuard.reason
1463
+ }
1464
+ });
1465
+ logProcessLifecycle({
1466
+ event: 'launcher_signal_forward',
1467
+ source: 'cli.launcher.shutdown',
1468
+ details: {
1469
+ commandName: spec.commandName,
1470
+ signal,
1471
+ forwarded: false,
1472
+ targetPid: toolProcess.pid ?? null,
1473
+ reason: 'disabled_no_forward'
1474
+ }
1475
+ });
1476
+ try {
1477
+ await applyLifecycleOrThrow({
1478
+ action: 'launcher_exit_signal',
1479
+ signal,
1480
+ targetPid: toolProcess.pid ?? null
1481
+ });
1482
+ }
1483
+ catch (error) {
1484
+ try {
1485
+ ctx.logger.error(error instanceof Error ? error.message : String(error));
1486
+ }
1487
+ catch {
1488
+ // ignore
1489
+ }
1490
+ }
1491
+ try {
1492
+ if (managedTmuxSession && shouldStopManagedTmuxOnShutdown(signal, ctx.env)) {
1493
+ managedTmuxSession.stop();
1494
+ }
1125
1495
  }
1126
1496
  catch {
1127
1497
  // ignore
1128
1498
  }
1129
- ctx.exit(0);
1130
1499
  };
1131
1500
  const onSignal = ctx.onSignal ?? ((signal, cb) => process.on(signal, cb));
1132
1501
  onSignal('SIGINT', () => {
@@ -1144,40 +1513,30 @@ export function createLauncherCommand(program, ctx, spec) {
1144
1513
  // ignore
1145
1514
  }
1146
1515
  try {
1147
- await clockClientService?.stop();
1516
+ await applyLifecycleOrThrow({
1517
+ action: 'launcher_tool_error_exit',
1518
+ targetPid: toolProcess.pid ?? null
1519
+ });
1148
1520
  }
1149
1521
  catch {
1150
- // ignore
1151
- }
1152
- try {
1153
- managedTmuxSession?.stop();
1522
+ // ignore lifecycle logging errors for terminal error path
1154
1523
  }
1155
- catch {
1156
- // ignore
1157
- }
1158
- ctx.exit(1);
1524
+ await finalizeToolTermination({ forceExitCode: 1 });
1159
1525
  })();
1160
1526
  });
1161
1527
  toolProcess.on('exit', (code, signal) => {
1162
- void (async () => {
1163
- try {
1164
- await clockClientService?.stop();
1165
- }
1166
- catch {
1167
- // ignore
1168
- }
1169
- try {
1170
- managedTmuxSession?.stop();
1171
- }
1172
- catch {
1173
- // ignore
1174
- }
1175
- if (signal) {
1176
- ctx.exit(0);
1177
- return;
1178
- }
1179
- ctx.exit(code ?? 0);
1180
- })();
1528
+ observedToolExitCode = code;
1529
+ observedToolExitSignal = signal ?? null;
1530
+ });
1531
+ toolProcess.on('close', (code, signal) => {
1532
+ if (observedToolExitCode === undefined) {
1533
+ observedToolExitCode = code;
1534
+ }
1535
+ if (!observedToolExitSignal) {
1536
+ observedToolExitSignal = signal ?? null;
1537
+ }
1538
+ logClientExitSummary();
1539
+ void finalizeToolTermination();
1181
1540
  });
1182
1541
  await ctx.waitForever();
1183
1542
  }