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