@jsonstudio/rcc 0.89.2202 → 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.
- package/README.md +27 -0
- package/dist/build-info.js +2 -2
- package/dist/build-info.js.map +1 -1
- package/dist/cli/commands/claude.js +1 -0
- package/dist/cli/commands/claude.js.map +1 -1
- package/dist/cli/commands/codex.js +1 -0
- package/dist/cli/commands/codex.js.map +1 -1
- package/dist/cli/commands/guardian-daemon.d.ts +2 -0
- package/dist/cli/commands/guardian-daemon.js +299 -0
- package/dist/cli/commands/guardian-daemon.js.map +1 -0
- package/dist/cli/commands/init/camoufox.js +1 -1
- package/dist/cli/commands/init/camoufox.js.map +1 -1
- package/dist/cli/commands/init.js +15 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/launcher/types.d.ts +6 -0
- package/dist/cli/commands/launcher-kernel.js +456 -109
- package/dist/cli/commands/launcher-kernel.js.map +1 -1
- package/dist/cli/commands/port.js +28 -8
- package/dist/cli/commands/port.js.map +1 -1
- package/dist/cli/commands/restart.d.ts +4 -0
- package/dist/cli/commands/restart.js +91 -42
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/start-types.d.ts +4 -0
- package/dist/cli/commands/start.js +112 -68
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/stop.d.ts +3 -0
- package/dist/cli/commands/stop.js +30 -63
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/cli/config/init-config.js +15 -1
- package/dist/cli/config/init-config.js.map +1 -1
- package/dist/cli/config/init-provider-catalog.js +13 -5
- package/dist/cli/config/init-provider-catalog.js.map +1 -1
- package/dist/cli/guardian/client.d.ts +38 -0
- package/dist/cli/guardian/client.js +237 -0
- package/dist/cli/guardian/client.js.map +1 -0
- package/dist/cli/guardian/paths.d.ts +7 -0
- package/dist/cli/guardian/paths.js +13 -0
- package/dist/cli/guardian/paths.js.map +1 -0
- package/dist/cli/guardian/types.d.ts +30 -0
- package/dist/cli/guardian/types.js +2 -0
- package/dist/cli/guardian/types.js.map +1 -0
- package/dist/cli/register/guardian-daemon-command.d.ts +2 -0
- package/dist/cli/register/guardian-daemon-command.js +5 -0
- package/dist/cli/register/guardian-daemon-command.js.map +1 -0
- package/dist/cli/server/port-utils.js +57 -1
- package/dist/cli/server/port-utils.js.map +1 -1
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/oauth.js +6 -6
- package/dist/commands/oauth.js.map +1 -1
- package/dist/commands/provider-update.js +12 -0
- package/dist/commands/provider-update.js.map +1 -1
- package/dist/config/routecodex-config-loader.js +66 -1
- package/dist/config/routecodex-config-loader.js.map +1 -1
- package/dist/config/virtual-router-builder.js +18 -0
- package/dist/config/virtual-router-builder.js.map +1 -1
- package/dist/config/virtual-router-types.js +20 -5
- package/dist/config/virtual-router-types.js.map +1 -1
- package/dist/daemon-admin-ui/assets/index-C8vP_c5E.js +15 -0
- package/dist/daemon-admin-ui/assets/index-DjIoHmNv.css +1 -0
- package/dist/daemon-admin-ui/index.html +13 -0
- package/dist/docs/daemon-admin-ui.html +328 -57
- package/dist/index.d.ts +9 -0
- package/dist/index.js +268 -10
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js +36 -0
- package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.events.js +50 -1
- package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
- package/dist/providers/auth/antigravity-user-agent.js +78 -31
- package/dist/providers/auth/antigravity-user-agent.js.map +1 -1
- package/dist/providers/auth/gemini-cli-userinfo-helper.js +94 -63
- package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/iflow-userinfo-helper.js +1 -1
- package/dist/providers/auth/iflow-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/oauth-error-message.d.ts +1 -0
- package/dist/providers/auth/oauth-error-message.js +44 -0
- package/dist/providers/auth/oauth-error-message.js.map +1 -0
- package/dist/providers/auth/oauth-lifecycle/error-detection.js +42 -8
- package/dist/providers/auth/oauth-lifecycle/error-detection.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle/token-io.d.ts +1 -0
- package/dist/providers/auth/oauth-lifecycle/token-io.js +12 -0
- package/dist/providers/auth/oauth-lifecycle/token-io.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.js +502 -87
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/auth/oauth-repair-cooldown.js +2 -7
- package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -1
- package/dist/providers/auth/oauth-repair-env.js +3 -5
- package/dist/providers/auth/oauth-repair-env.js.map +1 -1
- package/dist/providers/auth/oauth-utils/error-extraction.js +42 -8
- package/dist/providers/auth/oauth-utils/error-extraction.js.map +1 -1
- package/dist/providers/auth/qwen-userinfo-helper.js +18 -3
- package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/tokenfile-auth.d.ts +1 -0
- package/dist/providers/auth/tokenfile-auth.js +15 -9
- package/dist/providers/auth/tokenfile-auth.js.map +1 -1
- package/dist/providers/core/config/camoufox-actions.d.ts +31 -0
- package/dist/providers/core/config/camoufox-actions.js +461 -0
- package/dist/providers/core/config/camoufox-actions.js.map +1 -0
- package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
- package/dist/providers/core/config/camoufox-launcher.js +518 -160
- package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
- package/dist/providers/core/config/oauth-flows.js +6 -44
- package/dist/providers/core/config/oauth-flows.js.map +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js +51 -7
- package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
- package/dist/providers/core/config/service-profiles.js +13 -4
- package/dist/providers/core/config/service-profiles.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.d.ts +1 -0
- package/dist/providers/core/runtime/http-transport-provider.js +60 -1
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/provider-error-classifier.js +32 -15
- package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
- package/dist/providers/core/runtime/provider-family-profile-utils.js +1 -1
- package/dist/providers/core/runtime/provider-family-profile-utils.js.map +1 -1
- package/dist/providers/core/runtime/provider-response-postprocessor.js +61 -14
- package/dist/providers/core/runtime/provider-response-postprocessor.js.map +1 -1
- package/dist/providers/core/strategies/oauth-auth-code-flow.d.ts +1 -0
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +124 -19
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-device-flow.js +6 -3
- package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
- package/dist/providers/profile/families/iflow-profile.js +83 -10
- package/dist/providers/profile/families/iflow-profile.js.map +1 -1
- package/dist/providers/profile/families/qwen-profile.js +203 -0
- package/dist/providers/profile/families/qwen-profile.js.map +1 -1
- package/dist/scripts/camoufox/launch-auth.mjs +112 -5
- package/dist/server/handlers/config-admin-handler.js +9 -2
- package/dist/server/handlers/config-admin-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.js +3 -14
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/logging.js +3 -4
- package/dist/server/handlers/logging.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-reaper.d.ts +1 -0
- package/dist/server/runtime/http-server/clock-client-reaper.js +21 -15
- package/dist/server/runtime/http-server/clock-client-reaper.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-registry-utils.d.ts +4 -0
- package/dist/server/runtime/http-server/clock-client-registry-utils.js +74 -16
- package/dist/server/runtime/http-server/clock-client-registry-utils.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-registry.d.ts +15 -0
- package/dist/server/runtime/http-server/clock-client-registry.js +300 -6
- package/dist/server/runtime/http-server/clock-client-registry.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-routes.js +49 -19
- package/dist/server/runtime/http-server/clock-client-routes.js.map +1 -1
- package/dist/server/runtime/http-server/clock-daemon-log-throttle.d.ts +16 -0
- package/dist/server/runtime/http-server/clock-daemon-log-throttle.js +49 -0
- package/dist/server/runtime/http-server/clock-daemon-log-throttle.js.map +1 -1
- package/dist/server/runtime/http-server/clock-scope-resolution.d.ts +14 -0
- package/dist/server/runtime/http-server/clock-scope-resolution.js +212 -0
- package/dist/server/runtime/http-server/clock-scope-resolution.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +5 -3
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js +104 -15
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +2 -2
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.d.ts +24 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js +316 -70
- package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +190 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +18 -29
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +2 -0
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +8 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.js +30 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor/client-injection-flow.d.ts +14 -0
- package/dist/server/runtime/http-server/executor/client-injection-flow.js +287 -0
- package/dist/server/runtime/http-server/executor/client-injection-flow.js.map +1 -0
- package/dist/server/runtime/http-server/executor/index.d.ts +1 -1
- package/dist/server/runtime/http-server/executor/index.js +1 -1
- package/dist/server/runtime/http-server/executor/index.js.map +1 -1
- package/dist/server/runtime/http-server/executor/provider-response-converter.js +236 -62
- package/dist/server/runtime/http-server/executor/provider-response-converter.js.map +1 -1
- package/dist/server/runtime/http-server/executor/provider-response-utils.js +5 -0
- package/dist/server/runtime/http-server/executor/provider-response-utils.js.map +1 -1
- package/dist/server/runtime/http-server/executor/request-executor-core-utils.d.ts +2 -0
- package/dist/server/runtime/http-server/executor/request-executor-core-utils.js +60 -0
- package/dist/server/runtime/http-server/executor/request-executor-core-utils.js.map +1 -1
- package/dist/server/runtime/http-server/executor/request-retry-helpers.js +20 -8
- package/dist/server/runtime/http-server/executor/request-retry-helpers.js.map +1 -1
- package/dist/server/runtime/http-server/executor/sse-error-handler.d.ts +1 -0
- package/dist/server/runtime/http-server/executor/sse-error-handler.js +13 -2
- package/dist/server/runtime/http-server/executor/sse-error-handler.js.map +1 -1
- package/dist/server/runtime/http-server/executor/usage-aggregator.d.ts +0 -12
- package/dist/server/runtime/http-server/executor/usage-aggregator.js +84 -88
- package/dist/server/runtime/http-server/executor/usage-aggregator.js.map +1 -1
- package/dist/server/runtime/http-server/executor-metadata.js +328 -7
- package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.d.ts +1 -0
- package/dist/server/runtime/http-server/executor-response.js +52 -50
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/http-server-bootstrap.js +55 -6
- package/dist/server/runtime/http-server/http-server-bootstrap.js.map +1 -1
- package/dist/server/runtime/http-server/http-server-clock-daemon.d.ts +1 -0
- package/dist/server/runtime/http-server/http-server-clock-daemon.js +199 -44
- package/dist/server/runtime/http-server/http-server-clock-daemon.js.map +1 -1
- package/dist/server/runtime/http-server/http-server-lifecycle.js +4 -4
- package/dist/server/runtime/http-server/http-server-lifecycle.js.map +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +1 -0
- package/dist/server/runtime/http-server/index.js +1 -0
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.js +82 -4
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +26 -7
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +2 -1
- package/dist/server/runtime/http-server/routes.js +4 -2
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/session-dir.js +12 -1
- package/dist/server/runtime/http-server/session-dir.js.map +1 -1
- package/dist/server/runtime/http-server/stats-manager.d.ts +35 -0
- package/dist/server/runtime/http-server/stats-manager.js +269 -21
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/stopmessage-scope-rebind.d.ts +13 -0
- package/dist/server/runtime/http-server/stopmessage-scope-rebind.js +168 -0
- package/dist/server/runtime/http-server/stopmessage-scope-rebind.js.map +1 -0
- package/dist/server/runtime/http-server/tmux-session-probe.d.ts +10 -0
- package/dist/server/runtime/http-server/tmux-session-probe.js +97 -0
- package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -1
- package/dist/server-lifecycle/port-utils.d.ts +2 -1
- package/dist/server-lifecycle/port-utils.js +84 -4
- package/dist/server-lifecycle/port-utils.js.map +1 -1
- package/dist/token-daemon/index.d.ts +1 -0
- package/dist/token-daemon/index.js +17 -12
- package/dist/token-daemon/index.js.map +1 -1
- package/dist/utils/clock-client-token.d.ts +2 -1
- package/dist/utils/clock-client-token.js +52 -8
- package/dist/utils/clock-client-token.js.map +1 -1
- package/dist/utils/clock-scope-trace.d.ts +11 -0
- package/dist/utils/clock-scope-trace.js +41 -0
- package/dist/utils/clock-scope-trace.js.map +1 -0
- package/dist/utils/llms-engine-shadow.js +1 -1
- package/dist/utils/llms-engine-shadow.js.map +1 -1
- package/docs/DAEMON_CONTROL_PLANE.md +1 -0
- package/docs/ROUTING_POLICY_SCHEMA.md +4 -2
- package/docs/daemon-admin-ui.html +328 -57
- package/docs/design/servertool-stopmessage-lifecycle.md +109 -0
- package/docs/exec-command-guard-policy.example.v1.json +7 -1
- package/docs/providers/antigravity-gemini-provider-compat.md +2 -2
- package/package.json +23 -6
- package/scripts/build-core.mjs +12 -0
- package/scripts/camoufox/launch-auth.mjs +112 -5
- package/scripts/ci/repo-sanity.mjs +1 -0
- package/scripts/cleanup-stale-server-pids.mjs +142 -0
- package/scripts/install-global.sh +8 -0
- package/scripts/install-verify.mjs +33 -16
- package/scripts/run-bg.sh +226 -43
- package/scripts/run-fg-gtimeout.sh +158 -14
- package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +3 -3
- package/scripts/tests/ci-jest.mjs +9 -1
- package/scripts/triage-errorsamples.mjs +216 -0
- package/scripts/verify-codex-error-samples.mjs +92 -15
- package/scripts/verify-e2e-toolcall.mjs +12 -1
- package/scripts/verify-install-e2e.mjs +69 -28
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
|
573
|
-
'if [ "$__rcc_try" -ge "$__rcc_max" ]; then
|
|
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
|
|
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
|
-
|
|
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
|
|
1034
|
-
|
|
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
|
-
...(
|
|
1307
|
+
...(inferredTmuxSessionId
|
|
1048
1308
|
? {
|
|
1049
|
-
RCC_CLOCK_CLIENT_SESSION_ID:
|
|
1050
|
-
RCC_CLOCK_CLIENT_TMUX_SESSION_ID:
|
|
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
|
-
|
|
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
|
-
|
|
1396
|
+
await clockClientService?.stop();
|
|
1113
1397
|
}
|
|
1114
1398
|
catch {
|
|
1115
1399
|
// ignore
|
|
1116
1400
|
}
|
|
1117
1401
|
try {
|
|
1118
|
-
|
|
1402
|
+
if (managedTmuxSession && shouldStopManagedTmuxOnToolExit(ctx.env)) {
|
|
1403
|
+
managedTmuxSession.stop();
|
|
1404
|
+
}
|
|
1119
1405
|
}
|
|
1120
1406
|
catch {
|
|
1121
1407
|
// ignore
|
|
1122
1408
|
}
|
|
1123
1409
|
try {
|
|
1124
|
-
|
|
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
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1512
|
+
await finalizeToolTermination({ forceExitCode: 1 });
|
|
1159
1513
|
})();
|
|
1160
1514
|
});
|
|
1161
1515
|
toolProcess.on('exit', (code, signal) => {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
}
|