@jsonstudio/rcc 0.89.2239 → 0.90.1

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 (237) 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 +1 -0
  5. package/dist/cli/commands/claude.js.map +1 -1
  6. package/dist/cli/commands/codex.js +1 -0
  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/types.d.ts +6 -0
  14. package/dist/cli/commands/launcher-kernel.js +456 -109
  15. package/dist/cli/commands/launcher-kernel.js.map +1 -1
  16. package/dist/cli/commands/port.js +28 -8
  17. package/dist/cli/commands/port.js.map +1 -1
  18. package/dist/cli/commands/restart.d.ts +4 -0
  19. package/dist/cli/commands/restart.js +91 -42
  20. package/dist/cli/commands/restart.js.map +1 -1
  21. package/dist/cli/commands/start-types.d.ts +4 -0
  22. package/dist/cli/commands/start.js +108 -65
  23. package/dist/cli/commands/start.js.map +1 -1
  24. package/dist/cli/commands/stop.d.ts +3 -0
  25. package/dist/cli/commands/stop.js +30 -63
  26. package/dist/cli/commands/stop.js.map +1 -1
  27. package/dist/cli/config/init-provider-catalog.js +8 -3
  28. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  29. package/dist/cli/guardian/client.d.ts +38 -0
  30. package/dist/cli/guardian/client.js +237 -0
  31. package/dist/cli/guardian/client.js.map +1 -0
  32. package/dist/cli/guardian/paths.d.ts +7 -0
  33. package/dist/cli/guardian/paths.js +13 -0
  34. package/dist/cli/guardian/paths.js.map +1 -0
  35. package/dist/cli/guardian/types.d.ts +30 -0
  36. package/dist/cli/guardian/types.js +2 -0
  37. package/dist/cli/guardian/types.js.map +1 -0
  38. package/dist/cli/register/guardian-daemon-command.d.ts +2 -0
  39. package/dist/cli/register/guardian-daemon-command.js +5 -0
  40. package/dist/cli/register/guardian-daemon-command.js.map +1 -0
  41. package/dist/cli/server/port-utils.js +57 -1
  42. package/dist/cli/server/port-utils.js.map +1 -1
  43. package/dist/cli.js +48 -0
  44. package/dist/cli.js.map +1 -1
  45. package/dist/commands/oauth.js +6 -6
  46. package/dist/commands/oauth.js.map +1 -1
  47. package/dist/config/routecodex-config-loader.js +66 -1
  48. package/dist/config/routecodex-config-loader.js.map +1 -1
  49. package/dist/config/virtual-router-builder.js +18 -0
  50. package/dist/config/virtual-router-builder.js.map +1 -1
  51. package/dist/config/virtual-router-types.js +20 -5
  52. package/dist/config/virtual-router-types.js.map +1 -1
  53. package/dist/daemon-admin-ui/assets/index-C8vP_c5E.js +15 -0
  54. package/dist/daemon-admin-ui/assets/index-DjIoHmNv.css +1 -0
  55. package/dist/daemon-admin-ui/index.html +13 -0
  56. package/dist/docs/daemon-admin-ui.html +328 -57
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +268 -10
  59. package/dist/index.js.map +1 -1
  60. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.d.ts +1 -0
  61. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js +36 -0
  62. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js.map +1 -1
  63. package/dist/manager/modules/quota/provider-quota-daemon.events.js +50 -1
  64. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
  65. package/dist/providers/auth/antigravity-user-agent.js +78 -31
  66. package/dist/providers/auth/antigravity-user-agent.js.map +1 -1
  67. package/dist/providers/auth/gemini-cli-userinfo-helper.js +94 -63
  68. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
  69. package/dist/providers/auth/iflow-userinfo-helper.js +1 -1
  70. package/dist/providers/auth/iflow-userinfo-helper.js.map +1 -1
  71. package/dist/providers/auth/oauth-error-message.d.ts +1 -0
  72. package/dist/providers/auth/oauth-error-message.js +44 -0
  73. package/dist/providers/auth/oauth-error-message.js.map +1 -0
  74. package/dist/providers/auth/oauth-lifecycle/error-detection.js +42 -8
  75. package/dist/providers/auth/oauth-lifecycle/error-detection.js.map +1 -1
  76. package/dist/providers/auth/oauth-lifecycle/token-io.d.ts +1 -0
  77. package/dist/providers/auth/oauth-lifecycle/token-io.js +12 -0
  78. package/dist/providers/auth/oauth-lifecycle/token-io.js.map +1 -1
  79. package/dist/providers/auth/oauth-lifecycle.js +502 -87
  80. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  81. package/dist/providers/auth/oauth-repair-cooldown.js +2 -7
  82. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -1
  83. package/dist/providers/auth/oauth-repair-env.js +3 -5
  84. package/dist/providers/auth/oauth-repair-env.js.map +1 -1
  85. package/dist/providers/auth/oauth-utils/error-extraction.js +42 -8
  86. package/dist/providers/auth/oauth-utils/error-extraction.js.map +1 -1
  87. package/dist/providers/core/config/camoufox-actions.d.ts +31 -0
  88. package/dist/providers/core/config/camoufox-actions.js +461 -0
  89. package/dist/providers/core/config/camoufox-actions.js.map +1 -0
  90. package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
  91. package/dist/providers/core/config/camoufox-launcher.js +518 -160
  92. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  93. package/dist/providers/core/config/oauth-flows.js +6 -44
  94. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  95. package/dist/providers/core/config/provider-oauth-configs.js +51 -7
  96. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  97. package/dist/providers/core/runtime/provider-error-classifier.js +32 -15
  98. package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
  99. package/dist/providers/core/runtime/provider-family-profile-utils.js +1 -1
  100. package/dist/providers/core/runtime/provider-family-profile-utils.js.map +1 -1
  101. package/dist/providers/core/runtime/provider-response-postprocessor.js +61 -14
  102. package/dist/providers/core/runtime/provider-response-postprocessor.js.map +1 -1
  103. package/dist/providers/core/strategies/oauth-auth-code-flow.d.ts +1 -0
  104. package/dist/providers/core/strategies/oauth-auth-code-flow.js +124 -19
  105. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  106. package/dist/providers/core/strategies/oauth-device-flow.js +6 -3
  107. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  108. package/dist/providers/profile/families/iflow-profile.js +83 -10
  109. package/dist/providers/profile/families/iflow-profile.js.map +1 -1
  110. package/dist/scripts/camoufox/launch-auth.mjs +112 -5
  111. package/dist/server/handlers/config-admin-handler.js +9 -2
  112. package/dist/server/handlers/config-admin-handler.js.map +1 -1
  113. package/dist/server/handlers/handler-utils.js +3 -12
  114. package/dist/server/handlers/handler-utils.js.map +1 -1
  115. package/dist/server/handlers/logging.js +3 -4
  116. package/dist/server/handlers/logging.js.map +1 -1
  117. package/dist/server/runtime/http-server/clock-client-reaper.js +3 -26
  118. package/dist/server/runtime/http-server/clock-client-reaper.js.map +1 -1
  119. package/dist/server/runtime/http-server/clock-client-registry-utils.d.ts +4 -0
  120. package/dist/server/runtime/http-server/clock-client-registry-utils.js +74 -16
  121. package/dist/server/runtime/http-server/clock-client-registry-utils.js.map +1 -1
  122. package/dist/server/runtime/http-server/clock-client-registry.d.ts +15 -0
  123. package/dist/server/runtime/http-server/clock-client-registry.js +300 -6
  124. package/dist/server/runtime/http-server/clock-client-registry.js.map +1 -1
  125. package/dist/server/runtime/http-server/clock-client-routes.js +49 -19
  126. package/dist/server/runtime/http-server/clock-client-routes.js.map +1 -1
  127. package/dist/server/runtime/http-server/clock-daemon-log-throttle.d.ts +16 -0
  128. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js +49 -0
  129. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js.map +1 -1
  130. package/dist/server/runtime/http-server/clock-scope-resolution.d.ts +14 -0
  131. package/dist/server/runtime/http-server/clock-scope-resolution.js +212 -0
  132. package/dist/server/runtime/http-server/clock-scope-resolution.js.map +1 -0
  133. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +5 -3
  134. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
  135. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +104 -15
  136. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
  137. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +2 -2
  138. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  139. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.d.ts +24 -0
  140. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js +316 -70
  141. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js.map +1 -1
  142. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +190 -1
  143. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  144. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +18 -29
  145. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -1
  146. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +2 -0
  147. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
  148. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +8 -1
  149. package/dist/server/runtime/http-server/daemon-admin-routes.js +30 -0
  150. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  151. package/dist/server/runtime/http-server/executor/client-injection-flow.d.ts +14 -0
  152. package/dist/server/runtime/http-server/executor/client-injection-flow.js +287 -0
  153. package/dist/server/runtime/http-server/executor/client-injection-flow.js.map +1 -0
  154. package/dist/server/runtime/http-server/executor/index.d.ts +1 -1
  155. package/dist/server/runtime/http-server/executor/index.js +1 -1
  156. package/dist/server/runtime/http-server/executor/index.js.map +1 -1
  157. package/dist/server/runtime/http-server/executor/provider-response-converter.js +236 -62
  158. package/dist/server/runtime/http-server/executor/provider-response-converter.js.map +1 -1
  159. package/dist/server/runtime/http-server/executor/request-executor-core-utils.d.ts +1 -0
  160. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js +12 -0
  161. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js.map +1 -1
  162. package/dist/server/runtime/http-server/executor/request-retry-helpers.js +16 -12
  163. package/dist/server/runtime/http-server/executor/request-retry-helpers.js.map +1 -1
  164. package/dist/server/runtime/http-server/executor/sse-error-handler.d.ts +1 -0
  165. package/dist/server/runtime/http-server/executor/sse-error-handler.js +13 -2
  166. package/dist/server/runtime/http-server/executor/sse-error-handler.js.map +1 -1
  167. package/dist/server/runtime/http-server/executor/usage-aggregator.d.ts +0 -12
  168. package/dist/server/runtime/http-server/executor/usage-aggregator.js +84 -88
  169. package/dist/server/runtime/http-server/executor/usage-aggregator.js.map +1 -1
  170. package/dist/server/runtime/http-server/executor-metadata.js +328 -7
  171. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  172. package/dist/server/runtime/http-server/executor-response.d.ts +1 -0
  173. package/dist/server/runtime/http-server/executor-response.js +52 -58
  174. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  175. package/dist/server/runtime/http-server/http-server-bootstrap.js +50 -6
  176. package/dist/server/runtime/http-server/http-server-bootstrap.js.map +1 -1
  177. package/dist/server/runtime/http-server/http-server-clock-daemon.d.ts +1 -0
  178. package/dist/server/runtime/http-server/http-server-clock-daemon.js +186 -44
  179. package/dist/server/runtime/http-server/http-server-clock-daemon.js.map +1 -1
  180. package/dist/server/runtime/http-server/http-server-lifecycle.js +4 -4
  181. package/dist/server/runtime/http-server/http-server-lifecycle.js.map +1 -1
  182. package/dist/server/runtime/http-server/hub-shadow-compare.js +1 -1
  183. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  184. package/dist/server/runtime/http-server/index.d.ts +1 -0
  185. package/dist/server/runtime/http-server/index.js +1 -0
  186. package/dist/server/runtime/http-server/index.js.map +1 -1
  187. package/dist/server/runtime/http-server/middleware.js +82 -4
  188. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  189. package/dist/server/runtime/http-server/request-executor.js +6 -5
  190. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  191. package/dist/server/runtime/http-server/routes.d.ts +2 -1
  192. package/dist/server/runtime/http-server/routes.js +4 -2
  193. package/dist/server/runtime/http-server/routes.js.map +1 -1
  194. package/dist/server/runtime/http-server/session-dir.js +12 -1
  195. package/dist/server/runtime/http-server/session-dir.js.map +1 -1
  196. package/dist/server/runtime/http-server/stats-manager.d.ts +35 -0
  197. package/dist/server/runtime/http-server/stats-manager.js +269 -21
  198. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  199. package/dist/server/runtime/http-server/stopmessage-scope-rebind.d.ts +13 -0
  200. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js +168 -0
  201. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js.map +1 -0
  202. package/dist/server/runtime/http-server/tmux-session-probe.d.ts +10 -0
  203. package/dist/server/runtime/http-server/tmux-session-probe.js +97 -0
  204. package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -1
  205. package/dist/server-lifecycle/port-utils.d.ts +2 -1
  206. package/dist/server-lifecycle/port-utils.js +84 -4
  207. package/dist/server-lifecycle/port-utils.js.map +1 -1
  208. package/dist/token-daemon/index.d.ts +1 -0
  209. package/dist/token-daemon/index.js +17 -12
  210. package/dist/token-daemon/index.js.map +1 -1
  211. package/dist/utils/clock-client-token.d.ts +2 -1
  212. package/dist/utils/clock-client-token.js +52 -8
  213. package/dist/utils/clock-client-token.js.map +1 -1
  214. package/dist/utils/clock-scope-trace.d.ts +11 -0
  215. package/dist/utils/clock-scope-trace.js +41 -0
  216. package/dist/utils/clock-scope-trace.js.map +1 -0
  217. package/dist/utils/llms-engine-shadow.js +1 -1
  218. package/dist/utils/llms-engine-shadow.js.map +1 -1
  219. package/docs/DAEMON_CONTROL_PLANE.md +1 -0
  220. package/docs/ROUTING_POLICY_SCHEMA.md +4 -2
  221. package/docs/daemon-admin-ui.html +328 -57
  222. package/docs/design/servertool-stopmessage-lifecycle.md +109 -0
  223. package/docs/exec-command-guard-policy.example.v1.json +7 -1
  224. package/docs/providers/antigravity-gemini-provider-compat.md +2 -2
  225. package/package.json +21 -5
  226. package/scripts/build-core.mjs +12 -0
  227. package/scripts/camoufox/launch-auth.mjs +112 -5
  228. package/scripts/ci/repo-sanity.mjs +1 -0
  229. package/scripts/install-global.sh +6 -0
  230. package/scripts/install-verify.mjs +33 -16
  231. package/scripts/run-bg.sh +226 -43
  232. package/scripts/run-fg-gtimeout.sh +158 -14
  233. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +3 -3
  234. package/scripts/tests/ci-jest.mjs +9 -1
  235. package/scripts/triage-errorsamples.mjs +216 -0
  236. package/scripts/verify-codex-error-samples.mjs +92 -15
  237. package/scripts/verify-install-e2e.mjs +57 -27
@@ -4,9 +4,97 @@ 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 { encodeClockClientApiKey, extractClockClientDaemonIdFromApiKey, extractClockClientTmuxSessionIdFromApiKey } from '../../utils/clock-client-token.js';
8
+ import { isClockScopeTraceEnabled, isClockScopeTraceVerbose } from '../../utils/clock-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
+ function shouldStopManagedTmuxOnShutdown(signal, env) {
12
+ if (signal === 'SIGINT') {
13
+ return true;
14
+ }
15
+ if (signal !== 'SIGTERM') {
16
+ return true;
17
+ }
18
+ return resolveBoolFromEnv(env.ROUTECODEX_LAUNCHER_STOP_MANAGED_TMUX_ON_SIGTERM
19
+ ?? env.RCC_LAUNCHER_STOP_MANAGED_TMUX_ON_SIGTERM, true);
20
+ }
21
+ function shouldStopManagedTmuxOnToolExit(env) {
22
+ return resolveBoolFromEnv(env.ROUTECODEX_LAUNCHER_STOP_MANAGED_TMUX_ON_TOOL_EXIT
23
+ ?? env.RCC_LAUNCHER_STOP_MANAGED_TMUX_ON_TOOL_EXIT, true);
24
+ }
25
+ function shouldLogClientExitSummary(commandName) {
26
+ const normalized = String(commandName || '').trim().toLowerCase();
27
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'routecodex';
28
+ }
29
+ function readProcessPpidAndCommand(pid) {
30
+ if (process.platform === 'win32') {
31
+ return { ppid: null, command: '' };
32
+ }
33
+ try {
34
+ const out = spawnSync('ps', ['-p', String(pid), '-o', 'ppid=,command='], { encoding: 'utf8' });
35
+ if (out.error || Number(out.status ?? 0) !== 0) {
36
+ return { ppid: null, command: '' };
37
+ }
38
+ const line = String(out.stdout || '')
39
+ .split(/\r?\n/)
40
+ .map((item) => item.trim())
41
+ .find(Boolean);
42
+ if (!line) {
43
+ return { ppid: null, command: '' };
44
+ }
45
+ const match = line.match(/^(\d+)\s+(.+)$/);
46
+ if (!match) {
47
+ return { ppid: null, command: line };
48
+ }
49
+ const ppid = Number.parseInt(match[1], 10);
50
+ return {
51
+ ppid: Number.isFinite(ppid) && ppid > 0 ? ppid : null,
52
+ command: match[2] || ''
53
+ };
54
+ }
55
+ catch {
56
+ return { ppid: null, command: '' };
57
+ }
58
+ }
59
+ function commandLikelyMatchesHint(command, commandHint) {
60
+ const normalizedCommand = String(command || '').toLowerCase();
61
+ const normalizedHint = String(commandHint || '').toLowerCase().trim();
62
+ if (!normalizedHint) {
63
+ return true;
64
+ }
65
+ const hintBase = path.basename(normalizedHint);
66
+ if (hintBase && normalizedCommand.includes(hintBase)) {
67
+ return true;
68
+ }
69
+ const tokens = normalizedHint
70
+ .split(/[\\/\s]+/)
71
+ .map((token) => token.trim())
72
+ .filter((token) => token.length >= 3);
73
+ return tokens.some((token) => normalizedCommand.includes(token));
74
+ }
75
+ function canSignalOwnedToolProcess(args) {
76
+ const strictGuard = resolveBoolFromEnv(args.env.ROUTECODEX_LAUNCHER_STRICT_SIGNAL_GUARD ?? args.env.RCC_LAUNCHER_STRICT_SIGNAL_GUARD, true);
77
+ if (!strictGuard) {
78
+ return { ok: true, reason: 'strict_guard_disabled' };
79
+ }
80
+ if (!args.pid || !Number.isFinite(args.pid) || args.pid <= 1) {
81
+ return { ok: false, reason: 'invalid_pid' };
82
+ }
83
+ if (process.platform === 'win32') {
84
+ return { ok: true, reason: 'unsupported_platform' };
85
+ }
86
+ const snapshot = readProcessPpidAndCommand(args.pid);
87
+ if (!snapshot.ppid) {
88
+ return { ok: false, reason: 'ppid_unavailable' };
89
+ }
90
+ if (snapshot.ppid !== args.expectedParentPid) {
91
+ return { ok: false, reason: 'ppid_mismatch' };
92
+ }
93
+ if (!commandLikelyMatchesHint(snapshot.command, args.commandHint)) {
94
+ return { ok: false, reason: 'command_mismatch' };
95
+ }
96
+ return { ok: true, reason: 'owned_child' };
97
+ }
10
98
  function resolveServerConnection(ctx, fsImpl, pathImpl, options) {
11
99
  let configPath = typeof options.config === 'string' && options.config.trim() ? options.config.trim() : '';
12
100
  if (!configPath) {
@@ -69,43 +157,82 @@ function resolveServerConnection(ctx, fsImpl, pathImpl, options) {
69
157
  };
70
158
  }
71
159
  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);
160
+ const headers = apiKey ? { 'x-api-key': apiKey } : undefined;
161
+ const probeTargets = resolveServerProbeTargets(serverUrl);
162
+ for (let attempt = 0; attempt < 2; attempt += 1) {
163
+ for (const target of probeTargets) {
77
164
  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 };
165
+ const healthProbe = await probeServerState(ctx, `${target}/health`, headers, timeoutMs);
166
+ if (healthProbe.ok) {
167
+ const status = typeof healthProbe.body?.status === 'string' ? healthProbe.body.status.toLowerCase() : '';
168
+ if (status === 'ok' ||
169
+ status === 'ready' ||
170
+ healthProbe.body?.ready === true ||
171
+ healthProbe.body?.pipelineReady === true ||
172
+ healthProbe.body === null) {
173
+ return true;
174
+ }
175
+ }
176
+ const readyProbe = await probeServerState(ctx, `${target}/ready`, headers, timeoutMs);
177
+ if (readyProbe.ok) {
178
+ const status = typeof readyProbe.body?.status === 'string' ? readyProbe.body.status.toLowerCase() : '';
179
+ if (status === 'ready' || readyProbe.body?.ready === true || readyProbe.body === null) {
180
+ return true;
181
+ }
85
182
  }
86
- const body = await response.json().catch(() => null);
87
- return { ok: true, body };
88
- }
89
- finally {
90
- clearTimeout(timeoutId);
91
183
  }
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;
184
+ catch {
185
+ // try next target
98
186
  }
99
187
  }
100
- const healthProbe = await probe('/health');
101
- if (!healthProbe.ok) {
102
- return false;
188
+ if (attempt < 1) {
189
+ await new Promise((resolve) => setTimeout(resolve, 150));
190
+ }
191
+ }
192
+ return false;
193
+ }
194
+ function resolveServerProbeTargets(serverUrl) {
195
+ const out = [];
196
+ const seen = new Set();
197
+ const pushTarget = (value) => {
198
+ const normalized = value.trim().replace(/\/+$/, '');
199
+ if (!normalized || seen.has(normalized)) {
200
+ return;
201
+ }
202
+ seen.add(normalized);
203
+ out.push(normalized);
204
+ };
205
+ pushTarget(serverUrl);
206
+ try {
207
+ const parsed = new URL(serverUrl);
208
+ if (parsed.hostname === '0.0.0.0' || parsed.hostname === '::' || parsed.hostname === '::1' || parsed.hostname === 'localhost') {
209
+ const loopback = new URL(serverUrl);
210
+ loopback.hostname = '127.0.0.1';
211
+ pushTarget(loopback.toString());
103
212
  }
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
213
  }
107
214
  catch {
108
- return false;
215
+ // ignore invalid URL parse; keep original
216
+ }
217
+ return out;
218
+ }
219
+ async function probeServerState(ctx, url, headers, timeoutMs) {
220
+ const controller = new AbortController();
221
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
222
+ try {
223
+ const response = await ctx.fetch(url, {
224
+ signal: controller.signal,
225
+ method: 'GET',
226
+ headers
227
+ }).catch(() => null);
228
+ if (!response || !response.ok) {
229
+ return { ok: false, body: null };
230
+ }
231
+ const body = await response.json().catch(() => null);
232
+ return { ok: true, body };
233
+ }
234
+ finally {
235
+ clearTimeout(timeoutId);
109
236
  }
110
237
  }
111
238
  function rotateLogFile(fsImpl, filePath, maxBytes = 8 * 1024 * 1024, maxBackups = 3) {
@@ -154,10 +281,13 @@ function ensureServerLogPath(ctx, fsImpl, pathImpl, port) {
154
281
  rotateLogFile(fsImpl, logPath);
155
282
  return logPath;
156
283
  }
157
- async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved) {
284
+ async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved, allowAutoStartServer) {
158
285
  const alreadyReady = await checkServerReady(ctx, resolved.serverUrl, resolved.configuredApiKey);
159
286
  if (alreadyReady) {
160
- return { started: false };
287
+ return { started: false, ready: true };
288
+ }
289
+ if (!allowAutoStartServer) {
290
+ return { started: false, ready: false };
161
291
  }
162
292
  const hasExplicitUrl = typeof options.url === 'string' && options.url.trim().length > 0;
163
293
  if (hasExplicitUrl) {
@@ -166,12 +296,24 @@ async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolv
166
296
  spinner.info('RouteCodex server is not running, starting it in background...');
167
297
  const logPath = ensureServerLogPath(ctx, fsImpl, pathImpl, resolved.port);
168
298
  const logFd = fsImpl.openSync(logPath, 'a');
299
+ // Launcher auto-started server follows launcher lifecycle by default.
300
+ // This is intentionally different from `routecodex start`, which is persistent by default.
301
+ const bindServerToParent = resolveBoolFromEnv(ctx.env.ROUTECODEX_LAUNCHER_SERVER_PARENT_GUARD
302
+ ?? ctx.env.RCC_LAUNCHER_SERVER_PARENT_GUARD
303
+ ?? ctx.env.ROUTECODEX_SERVER_PARENT_GUARD
304
+ ?? ctx.env.RCC_SERVER_PARENT_GUARD, true);
169
305
  const env = {
170
306
  ...ctx.env,
171
307
  ROUTECODEX_CONFIG: resolved.configPath,
172
308
  ROUTECODEX_CONFIG_PATH: resolved.configPath,
173
309
  ROUTECODEX_PORT: String(resolved.port),
174
- RCC_PORT: String(resolved.port)
310
+ RCC_PORT: String(resolved.port),
311
+ ...(bindServerToParent
312
+ ? {
313
+ ROUTECODEX_EXPECT_PARENT_PID: String(process.pid),
314
+ RCC_EXPECT_PARENT_PID: String(process.pid)
315
+ }
316
+ : {})
175
317
  };
176
318
  logProcessLifecycle({
177
319
  event: 'detached_spawn',
@@ -264,7 +406,7 @@ async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolv
264
406
  await ctx.sleep(1000);
265
407
  const ready = await checkServerReady(ctx, resolved.serverUrl, resolved.configuredApiKey, 1500);
266
408
  if (ready) {
267
- return { started: true, logPath };
409
+ return { started: true, ready: true, logPath };
268
410
  }
269
411
  }
270
412
  logProcessLifecycle({
@@ -326,6 +468,18 @@ function resolveCurrentTmuxTarget(env, spawnSyncImpl = spawnSync) {
326
468
  return null;
327
469
  }
328
470
  }
471
+ function inferTmuxSessionIdFromTarget(tmuxTarget) {
472
+ const normalized = String(tmuxTarget || '').trim();
473
+ if (!normalized) {
474
+ return null;
475
+ }
476
+ const index = normalized.indexOf(':');
477
+ if (index <= 0) {
478
+ return null;
479
+ }
480
+ const sessionName = normalized.slice(0, index).trim();
481
+ return sessionName || null;
482
+ }
329
483
  function isReusableTmuxPaneTarget(spawnSyncImpl, tmuxTarget, cwd) {
330
484
  const normalizedTarget = String(tmuxTarget || '').trim();
331
485
  if (!normalizedTarget) {
@@ -506,24 +660,38 @@ function findReusableManagedTmuxSession(spawnSyncImpl, cwd, commandName) {
506
660
  return null;
507
661
  }
508
662
  }
663
+ function requestManagedTmuxSessionExit(spawnSyncImpl, sessionName) {
664
+ const target = String(sessionName || '').trim();
665
+ if (!target) {
666
+ return;
667
+ }
668
+ try {
669
+ spawnSyncImpl('tmux', ['send-keys', '-t', target, '-X', 'cancel'], { encoding: 'utf8' });
670
+ }
671
+ catch {
672
+ // ignore
673
+ }
674
+ try {
675
+ spawnSyncImpl('tmux', ['send-keys', '-t', target, 'C-c'], { encoding: 'utf8' });
676
+ }
677
+ catch {
678
+ // ignore
679
+ }
680
+ try {
681
+ spawnSyncImpl('tmux', ['send-keys', '-t', target, '-l', '--', 'exit'], { encoding: 'utf8' });
682
+ }
683
+ catch {
684
+ // ignore
685
+ }
686
+ try {
687
+ sendTmuxSubmitKey(spawnSyncImpl, target);
688
+ }
689
+ catch {
690
+ // ignore
691
+ }
692
+ }
509
693
  function createManagedTmuxSession(args) {
510
694
  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
- };
526
- }
527
695
  const sessionName = (() => {
528
696
  const token = normalizeSessionToken(commandName);
529
697
  return `rcc_${token}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
@@ -543,34 +711,32 @@ function createManagedTmuxSession(args) {
543
711
  tmuxTarget,
544
712
  reused: false,
545
713
  stop: () => {
546
- try {
547
- spawnSyncImpl('tmux', ['kill-session', '-t', sessionName], { encoding: 'utf8' });
548
- }
549
- catch {
550
- // ignore
551
- }
714
+ requestManagedTmuxSessionExit(spawnSyncImpl, sessionName);
552
715
  }
553
716
  };
554
717
  }
555
718
  function launchCommandInTmuxPane(args) {
556
719
  const { spawnSyncImpl, tmuxTarget, cwd, command, commandName, commandArgs, envOverrides, selfHealPolicy } = args;
720
+ const tmuxSessionName = (() => {
721
+ const idx = String(tmuxTarget || '').indexOf(':');
722
+ const name = idx >= 0 ? String(tmuxTarget).slice(0, idx) : String(tmuxTarget || '');
723
+ return name.trim();
724
+ })();
557
725
  const envTokens = [
558
726
  ...envOverrides.unset.flatMap((key) => ['-u', key]),
559
727
  ...envOverrides.set.map(([key, value]) => `${key}=${value}`)
560
728
  ];
561
729
  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 = (() => {
730
+ const commandBody = (() => {
565
731
  if (!selfHealPolicy.enabled || selfHealPolicy.maxRetries <= 0) {
566
- return `cd -- ${shellQuote(cwd)} && ${baseCommand}`;
732
+ return `cd -- ${shellQuote(cwd)} || exit 1; ${baseCommand}; __rcc_exit=$?`;
567
733
  }
568
734
  const safeCommandName = shellQuote(commandName || command || 'client');
569
735
  const loopBody = [
570
736
  `${baseCommand}`,
571
737
  '__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',
738
+ 'if [ "$__rcc_exit" -eq 0 ] || [ "$__rcc_exit" -eq 130 ] || [ "$__rcc_exit" -eq 143 ]; then break; fi',
739
+ 'if [ "$__rcc_try" -ge "$__rcc_max" ]; then break; fi',
574
740
  '__rcc_try=$((__rcc_try + 1))',
575
741
  `echo "[routecodex][self-heal] ${safeCommandName} exited with code $__rcc_exit; retry $__rcc_try/$__rcc_max in $__rcc_delay s" >&2`,
576
742
  'sleep "$__rcc_delay"'
@@ -583,6 +749,14 @@ function launchCommandInTmuxPane(args) {
583
749
  `while true; do ${loopBody}; done`
584
750
  ].join('; ');
585
751
  })();
752
+ // Client lifecycle owns managed tmux lifecycle: once command exits, destroy session.
753
+ const shellCommand = [
754
+ commandBody,
755
+ tmuxSessionName
756
+ ? `tmux kill-session -t ${shellQuote(tmuxSessionName)} >/dev/null 2>&1 || true`
757
+ : ':',
758
+ 'exit "$__rcc_exit"'
759
+ ].join('; ');
586
760
  try {
587
761
  // Prefer respawn-pane for deterministic execution in managed sessions.
588
762
  // This avoids flaky "typed but not submitted" behavior from send-keys on some terminals.
@@ -639,6 +813,15 @@ function sendJson(res, status, payload) {
639
813
  res.setHeader('content-type', 'application/json');
640
814
  res.end(body);
641
815
  }
816
+ function normalizeTmuxInjectedText(raw) {
817
+ return raw
818
+ .replace(/\r\n?/g, '\n')
819
+ .split('\n')
820
+ .map((line) => line.trim())
821
+ .filter((line) => line.length > 0)
822
+ .join(' ')
823
+ .trim();
824
+ }
642
825
  async function startClockClientService(args) {
643
826
  const { ctx, resolved, workdir, tmuxTarget, spawnSyncImpl, clientType, managedTmuxSession, getManagedProcessState } = args;
644
827
  const daemonId = (() => {
@@ -650,10 +833,12 @@ async function startClockClientService(args) {
650
833
  }
651
834
  })();
652
835
  const normalizedTmuxTarget = String(tmuxTarget || '').trim();
836
+ if (!normalizedTmuxTarget) {
837
+ // No tmux target means no reliable stdin injection path.
838
+ // Do not register a clock-client daemon with a synthetic session id.
839
+ return null;
840
+ }
653
841
  const tmuxSessionId = (() => {
654
- if (!normalizedTmuxTarget) {
655
- return daemonId;
656
- }
657
842
  const idx = normalizedTmuxTarget.indexOf(':');
658
843
  const candidate = (idx >= 0 ? normalizedTmuxTarget.slice(0, idx) : normalizedTmuxTarget).trim();
659
844
  return candidate || daemonId;
@@ -667,12 +852,14 @@ async function startClockClientService(args) {
667
852
  return;
668
853
  }
669
854
  const body = await readJsonBody(req);
670
- const text = typeof body.text === 'string' ? body.text.trim() : '';
855
+ const text = typeof body.text === 'string' ? normalizeTmuxInjectedText(body.text) : '';
671
856
  if (!text) {
672
857
  sendJson(res, 400, { ok: false, message: 'text is required' });
673
858
  return;
674
859
  }
675
860
  try {
861
+ // Ensure pane is not stuck in copy-mode before literal injection + submit.
862
+ spawnSyncImpl('tmux', ['send-keys', '-t', normalizedTmuxTarget, '-X', 'cancel'], { encoding: 'utf8' });
676
863
  const literal = spawnSyncImpl('tmux', ['send-keys', '-t', normalizedTmuxTarget, '-l', '--', text], { encoding: 'utf8' });
677
864
  if (literal.status !== 0) {
678
865
  sendJson(res, 500, {
@@ -719,6 +906,7 @@ async function startClockClientService(args) {
719
906
  callbackUrl = `http://127.0.0.1:${port}/inject`;
720
907
  }
721
908
  const controlUrl = `${resolved.protocol}://127.0.0.1:${resolved.port}${resolved.basePath}`;
909
+ const controlRequestTimeoutMs = resolveIntFromEnv(ctx.env.ROUTECODEX_CLOCK_CLIENT_CONTROL_TIMEOUT_MS ?? ctx.env.RCC_CLOCK_CLIENT_CONTROL_TIMEOUT_MS, 1500, 200, 30_000);
722
910
  const normalizeManagedProcessPayload = () => {
723
911
  const state = typeof getManagedProcessState === 'function' ? getManagedProcessState() : undefined;
724
912
  const managedClientProcess = state?.managedClientProcess === true;
@@ -735,17 +923,37 @@ async function startClockClientService(args) {
735
923
  };
736
924
  };
737
925
  const post = async (pathSuffix, payload) => {
926
+ const abortController = typeof AbortController !== 'undefined' ? new AbortController() : null;
927
+ const timeoutHandle = abortController
928
+ ? setTimeout(() => {
929
+ try {
930
+ abortController.abort();
931
+ }
932
+ catch {
933
+ // ignore abort failures
934
+ }
935
+ }, controlRequestTimeoutMs)
936
+ : null;
937
+ if (timeoutHandle && typeof timeoutHandle.unref === 'function') {
938
+ timeoutHandle.unref();
939
+ }
738
940
  try {
739
941
  const response = await ctx.fetch(`${controlUrl}${pathSuffix}`, {
740
942
  method: 'POST',
741
943
  headers: { 'content-type': 'application/json' },
742
- body: JSON.stringify(payload)
944
+ body: JSON.stringify(payload),
945
+ ...(abortController ? { signal: abortController.signal } : {})
743
946
  });
744
947
  return { ok: response.ok, status: response.status };
745
948
  }
746
949
  catch {
747
950
  return { ok: false, status: 0 };
748
951
  }
952
+ finally {
953
+ if (timeoutHandle) {
954
+ clearTimeout(timeoutHandle);
955
+ }
956
+ }
749
957
  };
750
958
  const reRegisterBackoffMs = resolveIntFromEnv(ctx.env.ROUTECODEX_CLOCK_CLIENT_REREGISTER_BACKOFF_MS ?? ctx.env.RCC_CLOCK_CLIENT_REREGISTER_BACKOFF_MS, 1500, 200, 60_000);
751
959
  let registerInFlight = null;
@@ -913,7 +1121,11 @@ export function createLauncherCommand(program, ctx, spec) {
913
1121
  const spinner = await ctx.createSpinner(`Preparing ${spec.displayName} with RouteCodex...`);
914
1122
  try {
915
1123
  const resolved = resolveServerConnection(ctx, fsImpl, pathImpl, options);
916
- const ensureResult = await ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved);
1124
+ await ctx.ensureGuardianDaemon?.();
1125
+ const ensureResult = await ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved, spec.allowAutoStartServer === true);
1126
+ if (!ensureResult.ready) {
1127
+ spinner.info('RouteCodex server is not running; launcher will continue and wait for your next requests.');
1128
+ }
917
1129
  spinner.text = `Launching ${spec.displayName}...`;
918
1130
  const baseUrl = `${resolved.protocol}://${resolved.connectHost}${resolved.portPart}${resolved.basePath}`;
919
1131
  const currentCwd = resolveWorkingDirectory(ctx, fsImpl, pathImpl, options.cwd);
@@ -1023,16 +1235,64 @@ export function createLauncherCommand(program, ctx, spec) {
1023
1235
  managedClientCommandHint
1024
1236
  })
1025
1237
  });
1026
- if (managedClientProcessEnabled && reclaimRequired && !clockClientService) {
1238
+ if (managedClientProcessEnabled && reclaimRequired && tmuxTarget && !clockClientService) {
1027
1239
  throw new Error('clock client registration failed for managed child process; aborting launch to avoid orphan process');
1028
1240
  }
1029
1241
  if (tmuxTarget && !clockClientService) {
1030
1242
  ctx.logger.warning('[clock-advanced] failed to start clock client daemon service; launcher continues without advanced mode.');
1031
1243
  }
1032
1244
  const clockAdvancedEnabled = Boolean(clockClientService && tmuxTarget);
1033
- const clockClientApiKey = clockAdvancedEnabled && clockClientService
1034
- ? encodeClockClientApiKey(resolved.configuredApiKey || 'rcc-proxy-key', clockClientService.daemonId)
1245
+ const inferredTmuxSessionId = clockClientService?.tmuxSessionId ||
1246
+ inferTmuxSessionIdFromTarget(tmuxTarget) ||
1247
+ undefined;
1248
+ const inferredDaemonId = clockClientService?.daemonId ||
1249
+ (inferredTmuxSessionId ? `clockd_unbound_${process.pid}` : undefined);
1250
+ const clockClientApiKey = inferredTmuxSessionId && inferredDaemonId
1251
+ ? encodeClockClientApiKey(resolved.configuredApiKey || 'rcc-proxy-key', inferredDaemonId, inferredTmuxSessionId)
1035
1252
  : (resolved.configuredApiKey || 'rcc-proxy-key');
1253
+ if (isClockScopeTraceEnabled()) {
1254
+ try {
1255
+ const parsedDaemonId = extractClockClientDaemonIdFromApiKey(clockClientApiKey) || 'none';
1256
+ const parsedTmuxSessionId = extractClockClientTmuxSessionIdFromApiKey(clockClientApiKey) || 'none';
1257
+ const verbose = isClockScopeTraceVerbose();
1258
+ ctx.logger.info(`[clock-scope][launch] command=${spec.commandName} advanced=${clockAdvancedEnabled ? 'on' : 'off'} ` +
1259
+ `daemon=${parsedDaemonId} tmux=${parsedTmuxSessionId} tmuxTarget=${tmuxTarget || 'none'}` +
1260
+ (verbose ? ` managedTmux=${managedTmuxSession ? 'yes' : 'no'} serverStarted=${ensureResult.started ? 'yes' : 'no'}` : ''));
1261
+ }
1262
+ catch {
1263
+ // best-effort diagnostics only
1264
+ }
1265
+ }
1266
+ await ctx.registerGuardianProcess?.({
1267
+ source: spec.commandName,
1268
+ pid: process.pid,
1269
+ ppid: process.ppid,
1270
+ port: resolved.port,
1271
+ tmuxSessionId: clockClientService?.tmuxSessionId || inferTmuxSessionIdFromTarget(tmuxTarget) || undefined,
1272
+ tmuxTarget: tmuxTarget || undefined,
1273
+ metadata: {
1274
+ workingDirectory: currentCwd,
1275
+ binary: resolvedBinary,
1276
+ managedTmuxSession: Boolean(managedTmuxSession),
1277
+ autoStartedServer: ensureResult.started === true
1278
+ }
1279
+ });
1280
+ const applyLifecycleOrThrow = async (args) => {
1281
+ const accepted = await ctx.reportGuardianLifecycle?.({
1282
+ action: args.action,
1283
+ source: `cli.launcher.${spec.commandName}`,
1284
+ actorPid: process.pid,
1285
+ targetPid: args.targetPid && args.targetPid > 0 ? args.targetPid : undefined,
1286
+ signal: args.signal,
1287
+ metadata: {
1288
+ port: resolved.port,
1289
+ serverUrl: resolved.serverUrl
1290
+ }
1291
+ });
1292
+ if (ctx.reportGuardianLifecycle && accepted !== true) {
1293
+ throw new Error(`guardian lifecycle apply rejected (${args.action})`);
1294
+ }
1295
+ };
1036
1296
  const toolEnv = spec.buildEnv({
1037
1297
  env: {
1038
1298
  ...ctx.env,
@@ -1044,12 +1304,14 @@ export function createLauncherCommand(program, ctx, spec) {
1044
1304
  OPENAI_API_BASE_URL: normalizeOpenAiBaseUrl(baseUrl),
1045
1305
  OPENAI_API_KEY: clockClientApiKey,
1046
1306
  RCC_CLOCK_ADVANCED_ENABLED: clockAdvancedEnabled ? '1' : '0',
1047
- ...(clockAdvancedEnabled && clockClientService
1307
+ ...(inferredTmuxSessionId
1048
1308
  ? {
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
1309
+ RCC_CLOCK_CLIENT_SESSION_ID: inferredTmuxSessionId,
1310
+ RCC_CLOCK_CLIENT_TMUX_SESSION_ID: inferredTmuxSessionId
1052
1311
  }
1312
+ : {}),
1313
+ ...(inferredDaemonId
1314
+ ? { RCC_CLOCK_CLIENT_DAEMON_ID: inferredDaemonId }
1053
1315
  : {})
1054
1316
  },
1055
1317
  baseUrl,
@@ -1107,26 +1369,121 @@ export function createLauncherCommand(program, ctx, spec) {
1107
1369
  ctx.logger.info(`Working directory for ${spec.displayName}: ${currentCwd}`);
1108
1370
  ctx.logger.info(`Press Ctrl+C to exit ${spec.displayName}`);
1109
1371
  }
1110
- const shutdown = async (signal) => {
1372
+ let shutdownTriggered = false;
1373
+ let toolProcessClosing = false;
1374
+ let observedToolExitCode;
1375
+ let observedToolExitSignal = null;
1376
+ let requestedShutdownSignal = null;
1377
+ let clientExitSummaryLogged = false;
1378
+ const logClientExitSummary = () => {
1379
+ if (clientExitSummaryLogged || !shouldLogClientExitSummary(spec.commandName)) {
1380
+ return;
1381
+ }
1382
+ clientExitSummaryLogged = true;
1383
+ const codeLabel = typeof observedToolExitCode === 'number' && Number.isFinite(observedToolExitCode)
1384
+ ? String(observedToolExitCode)
1385
+ : 'n/a';
1386
+ const signalLabel = observedToolExitSignal || 'none';
1387
+ ctx.logger.info(`[client-exit] ${spec.displayName} exited (code=${codeLabel}, signal=${signalLabel})`);
1388
+ };
1389
+ const finalizeToolTermination = async (options) => {
1390
+ if (toolProcessClosing) {
1391
+ return;
1392
+ }
1393
+ toolProcessClosing = true;
1394
+ logClientExitSummary();
1111
1395
  try {
1112
- toolProcess.kill(signal);
1396
+ await clockClientService?.stop();
1113
1397
  }
1114
1398
  catch {
1115
1399
  // ignore
1116
1400
  }
1117
1401
  try {
1118
- await clockClientService?.stop();
1402
+ if (managedTmuxSession && shouldStopManagedTmuxOnToolExit(ctx.env)) {
1403
+ managedTmuxSession.stop();
1404
+ }
1119
1405
  }
1120
1406
  catch {
1121
1407
  // ignore
1122
1408
  }
1123
1409
  try {
1124
- managedTmuxSession?.stop();
1410
+ await applyLifecycleOrThrow({
1411
+ action: 'launcher_tool_exit',
1412
+ signal: observedToolExitSignal ? String(observedToolExitSignal) : undefined,
1413
+ targetPid: toolProcess.pid ?? null
1414
+ });
1415
+ }
1416
+ catch {
1417
+ // ignore lifecycle logging errors in exit path
1418
+ }
1419
+ const forcedExitCode = options?.forceExitCode;
1420
+ if (typeof forcedExitCode === 'number' && Number.isFinite(forcedExitCode)) {
1421
+ ctx.exit(Math.max(0, Math.floor(forcedExitCode)));
1422
+ return;
1423
+ }
1424
+ if (requestedShutdownSignal || observedToolExitSignal) {
1425
+ ctx.exit(0);
1426
+ return;
1427
+ }
1428
+ ctx.exit(observedToolExitCode ?? 0);
1429
+ };
1430
+ const shutdown = async (signal) => {
1431
+ if (shutdownTriggered) {
1432
+ return;
1433
+ }
1434
+ shutdownTriggered = true;
1435
+ requestedShutdownSignal = signal;
1436
+ const targetGuard = canSignalOwnedToolProcess({
1437
+ env: ctx.env,
1438
+ pid: toolProcess.pid ?? null,
1439
+ expectedParentPid: process.pid,
1440
+ commandHint: resolvedBinary
1441
+ });
1442
+ logProcessLifecycle({
1443
+ event: 'launcher_signal_guard',
1444
+ source: 'cli.launcher.shutdown',
1445
+ details: {
1446
+ commandName: spec.commandName,
1447
+ signal,
1448
+ targetPid: toolProcess.pid ?? null,
1449
+ result: targetGuard.ok ? 'allowed' : 'blocked',
1450
+ reason: targetGuard.reason
1451
+ }
1452
+ });
1453
+ logProcessLifecycle({
1454
+ event: 'launcher_signal_forward',
1455
+ source: 'cli.launcher.shutdown',
1456
+ details: {
1457
+ commandName: spec.commandName,
1458
+ signal,
1459
+ forwarded: false,
1460
+ targetPid: toolProcess.pid ?? null,
1461
+ reason: 'disabled_no_forward'
1462
+ }
1463
+ });
1464
+ try {
1465
+ await applyLifecycleOrThrow({
1466
+ action: 'launcher_exit_signal',
1467
+ signal,
1468
+ targetPid: toolProcess.pid ?? null
1469
+ });
1470
+ }
1471
+ catch (error) {
1472
+ try {
1473
+ ctx.logger.error(error instanceof Error ? error.message : String(error));
1474
+ }
1475
+ catch {
1476
+ // ignore
1477
+ }
1478
+ }
1479
+ try {
1480
+ if (managedTmuxSession && shouldStopManagedTmuxOnShutdown(signal, ctx.env)) {
1481
+ managedTmuxSession.stop();
1482
+ }
1125
1483
  }
1126
1484
  catch {
1127
1485
  // ignore
1128
1486
  }
1129
- ctx.exit(0);
1130
1487
  };
1131
1488
  const onSignal = ctx.onSignal ?? ((signal, cb) => process.on(signal, cb));
1132
1489
  onSignal('SIGINT', () => {
@@ -1144,40 +1501,30 @@ export function createLauncherCommand(program, ctx, spec) {
1144
1501
  // ignore
1145
1502
  }
1146
1503
  try {
1147
- await clockClientService?.stop();
1148
- }
1149
- catch {
1150
- // ignore
1151
- }
1152
- try {
1153
- managedTmuxSession?.stop();
1504
+ await applyLifecycleOrThrow({
1505
+ action: 'launcher_tool_error_exit',
1506
+ targetPid: toolProcess.pid ?? null
1507
+ });
1154
1508
  }
1155
1509
  catch {
1156
- // ignore
1510
+ // ignore lifecycle logging errors for terminal error path
1157
1511
  }
1158
- ctx.exit(1);
1512
+ await finalizeToolTermination({ forceExitCode: 1 });
1159
1513
  })();
1160
1514
  });
1161
1515
  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
- })();
1516
+ observedToolExitCode = code;
1517
+ observedToolExitSignal = signal ?? null;
1518
+ });
1519
+ toolProcess.on('close', (code, signal) => {
1520
+ if (observedToolExitCode === undefined) {
1521
+ observedToolExitCode = code;
1522
+ }
1523
+ if (!observedToolExitSignal) {
1524
+ observedToolExitSignal = signal ?? null;
1525
+ }
1526
+ logClientExitSummary();
1527
+ void finalizeToolTermination();
1181
1528
  });
1182
1529
  await ctx.waitForever();
1183
1530
  }