borgmcp 1.1.14 → 2.0.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/CONTRIBUTING.md +15 -0
- package/LICENSE +201 -33
- package/NOTICE +2 -0
- package/README.md +86 -37
- package/SECURITY.md +7 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +63 -1
- package/dist/agent-runtime.js.map +1 -0
- package/dist/assimilate-cmd.d.ts +96 -17
- package/dist/assimilate-cmd.d.ts.map +1 -0
- package/dist/assimilate-cmd.js +1396 -48
- package/dist/assimilate-cmd.js.map +1 -0
- package/dist/assimilate-deps.d.ts.map +1 -0
- package/dist/assimilate-deps.js +384 -3
- package/dist/assimilate-deps.js.map +1 -0
- package/dist/assimilate-guard.d.ts +5 -7
- package/dist/assimilate-guard.d.ts.map +1 -0
- package/dist/assimilate-guard.js +60 -2
- package/dist/assimilate-guard.js.map +1 -0
- package/dist/assimilate-welcome.d.ts +1 -1
- package/dist/assimilate-welcome.d.ts.map +1 -0
- package/dist/assimilate-welcome.js +32 -2
- package/dist/assimilate-welcome.js.map +1 -0
- package/dist/auth-env.d.ts +7 -50
- package/dist/auth-env.d.ts.map +1 -0
- package/dist/auth-env.js +22 -1
- package/dist/auth-env.js.map +1 -0
- package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
- package/dist/backends/launch-all-pastelist.js +11 -4
- package/dist/backends/launch-all-pastelist.js.map +1 -0
- package/dist/backends/launch-all-tmux.d.ts.map +1 -0
- package/dist/backends/launch-all-tmux.js +49 -1
- package/dist/backends/launch-all-tmux.js.map +1 -0
- package/dist/backends/launch-all-windows.d.ts.map +1 -0
- package/dist/backends/launch-all-windows.js +86 -12
- package/dist/backends/launch-all-windows.js.map +1 -0
- package/dist/bare-launch-menu.d.ts.map +1 -0
- package/dist/bare-launch-menu.js +83 -6
- package/dist/bare-launch-menu.js.map +1 -0
- package/dist/claude-launch-args.d.ts.map +1 -0
- package/dist/claude-launch-args.js +34 -1
- package/dist/claude-launch-args.js.map +1 -0
- package/dist/claude.d.ts.map +1 -0
- package/dist/claude.js +486 -14
- package/dist/claude.js.map +1 -0
- package/dist/cleanup-cmd.d.ts +5 -5
- package/dist/cleanup-cmd.d.ts.map +1 -0
- package/dist/cleanup-cmd.js +453 -14
- package/dist/cleanup-cmd.js.map +1 -0
- package/dist/clear-rewake-core.d.ts.map +1 -0
- package/dist/clear-rewake-core.js +23 -2
- package/dist/clear-rewake-core.js.map +1 -0
- package/dist/clear-rewake.d.ts.map +1 -0
- package/dist/clear-rewake.js +19 -1
- package/dist/clear-rewake.js.map +1 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +104 -56
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-platform.d.ts.map +1 -0
- package/dist/cli-platform.js +106 -4
- package/dist/cli-platform.js.map +1 -0
- package/dist/cli-tool-approval.d.ts +84 -0
- package/dist/cli-tool-approval.d.ts.map +1 -0
- package/dist/cli-tool-approval.js +583 -0
- package/dist/cli-tool-approval.js.map +1 -0
- package/dist/codex-app-server.d.ts.map +1 -0
- package/dist/codex-app-server.js +228 -4
- package/dist/codex-app-server.js.map +1 -0
- package/dist/codex-app-wake.d.ts.map +1 -0
- package/dist/codex-app-wake.js +432 -2
- package/dist/codex-app-wake.js.map +1 -0
- package/dist/codex-launch.d.ts.map +1 -0
- package/dist/codex-launch.js +102 -2
- package/dist/codex-launch.js.map +1 -0
- package/dist/codex-remote.d.ts +6 -1
- package/dist/codex-remote.d.ts.map +1 -0
- package/dist/codex-remote.js +394 -1
- package/dist/codex-remote.js.map +1 -0
- package/dist/codex-wake-resolve.d.ts.map +1 -0
- package/dist/codex-wake-resolve.js +127 -1
- package/dist/codex-wake-resolve.js.map +1 -0
- package/dist/config-utils.d.ts +2 -2
- package/dist/config-utils.d.ts.map +1 -0
- package/dist/config-utils.js +766 -3
- package/dist/config-utils.js.map +1 -0
- package/dist/config.d.ts +98 -61
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +426 -1
- package/dist/config.js.map +1 -0
- package/dist/console-prefix.d.ts.map +1 -0
- package/dist/console-prefix.js +96 -1
- package/dist/console-prefix.js.map +1 -0
- package/dist/cube-name.d.ts.map +1 -0
- package/dist/cube-name.js +67 -1
- package/dist/cube-name.js.map +1 -0
- package/dist/cubes.d.ts +126 -18
- package/dist/cubes.d.ts.map +1 -0
- package/dist/cubes.js +412 -4
- package/dist/cubes.js.map +1 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +71 -1
- package/dist/debug.js.map +1 -0
- package/dist/direct-log.d.ts.map +1 -0
- package/dist/direct-log.js +11 -1
- package/dist/direct-log.js.map +1 -0
- package/dist/docs-sections.d.ts +11 -10
- package/dist/docs-sections.d.ts.map +1 -0
- package/dist/docs-sections.js +113 -3
- package/dist/docs-sections.js.map +1 -0
- package/dist/drone-lifecycle.d.ts +5 -21
- package/dist/drone-lifecycle.d.ts.map +1 -0
- package/dist/drone-lifecycle.js +57 -8
- package/dist/drone-lifecycle.js.map +1 -0
- package/dist/drone-management.d.ts +29 -0
- package/dist/drone-management.d.ts.map +1 -0
- package/dist/drone-management.js +109 -0
- package/dist/drone-management.js.map +1 -0
- package/dist/ensure-mcp-config.d.ts.map +1 -0
- package/dist/ensure-mcp-config.js +36 -1
- package/dist/ensure-mcp-config.js.map +1 -0
- package/dist/evict-drone.d.ts +10 -2
- package/dist/evict-drone.d.ts.map +1 -0
- package/dist/evict-drone.js +60 -1
- package/dist/evict-drone.js.map +1 -0
- package/dist/gc-orphan-inboxes.d.ts.map +1 -0
- package/dist/gc-orphan-inboxes.js +177 -1
- package/dist/gc-orphan-inboxes.js.map +1 -0
- package/dist/inbox-monitor.d.ts +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -0
- package/dist/inbox-monitor.js +856 -3
- package/dist/inbox-monitor.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1034 -37
- package/dist/index.js.map +1 -0
- package/dist/launch-all-cmd.d.ts.map +1 -0
- package/dist/launch-all-cmd.js +377 -37
- package/dist/launch-all-cmd.js.map +1 -0
- package/dist/launch-all-command.d.ts.map +1 -0
- package/dist/launch-all-command.js +24 -1
- package/dist/launch-all-command.js.map +1 -0
- package/dist/launch-all-deps.d.ts +3 -8
- package/dist/launch-all-deps.d.ts.map +1 -0
- package/dist/launch-all-deps.js +99 -1
- package/dist/launch-all-deps.js.map +1 -0
- package/dist/launch-all-discovery.d.ts +1 -0
- package/dist/launch-all-discovery.d.ts.map +1 -0
- package/dist/launch-all-discovery.js +90 -4
- package/dist/launch-all-discovery.js.map +1 -0
- package/dist/launch-all-locks.d.ts.map +1 -0
- package/dist/launch-all-locks.js +54 -1
- package/dist/launch-all-locks.js.map +1 -0
- package/dist/launch-gate.d.ts +2 -2
- package/dist/launch-gate.d.ts.map +1 -0
- package/dist/launch-gate.js +73 -1
- package/dist/launch-gate.js.map +1 -0
- package/dist/lifecycle-log-guard.d.ts.map +1 -0
- package/dist/lifecycle-log-guard.js +93 -2
- package/dist/lifecycle-log-guard.js.map +1 -0
- package/dist/list-roles-render.d.ts.map +1 -0
- package/dist/list-roles-render.js +40 -6
- package/dist/list-roles-render.js.map +1 -0
- package/dist/local-log-routing.d.ts +19 -0
- package/dist/local-log-routing.d.ts.map +1 -0
- package/dist/local-log-routing.js +67 -0
- package/dist/local-log-routing.js.map +1 -0
- package/dist/local-manage-tool-result.d.ts +8 -0
- package/dist/local-manage-tool-result.d.ts.map +1 -0
- package/dist/local-manage-tool-result.js +10 -0
- package/dist/local-manage-tool-result.js.map +1 -0
- package/dist/local-server-cursor.d.ts +34 -0
- package/dist/local-server-cursor.d.ts.map +1 -0
- package/dist/local-server-cursor.js +151 -0
- package/dist/local-server-cursor.js.map +1 -0
- package/dist/log-audit.d.ts.map +1 -0
- package/dist/log-audit.js +194 -3
- package/dist/log-audit.js.map +1 -0
- package/dist/log-stream.d.ts +31 -18
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +1214 -12
- package/dist/log-stream.js.map +1 -0
- package/dist/model-presets.d.ts.map +1 -0
- package/dist/model-presets.js +19 -1
- package/dist/model-presets.js.map +1 -0
- package/dist/name-validator.d.ts.map +1 -0
- package/dist/name-validator.js +22 -1
- package/dist/name-validator.js.map +1 -0
- package/dist/opencode-drone.d.ts.map +1 -0
- package/dist/opencode-drone.js +438 -4
- package/dist/opencode-drone.js.map +1 -0
- package/dist/opencode-plugin.d.ts +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -0
- package/dist/opencode-plugin.js +29 -4
- package/dist/opencode-plugin.js.map +1 -0
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -0
- package/dist/parse-assimilate-args.js +125 -1
- package/dist/parse-assimilate-args.js.map +1 -0
- package/dist/parse-launch-all-args.d.ts.map +1 -0
- package/dist/parse-launch-all-args.js +70 -1
- package/dist/parse-launch-all-args.js.map +1 -0
- package/dist/postinstall-banner.d.ts.map +1 -0
- package/dist/postinstall-banner.js +27 -2
- package/dist/postinstall-banner.js.map +1 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +32 -5
- package/dist/postinstall.js.map +1 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +9 -1
- package/dist/readiness-probe.js.map +1 -0
- package/dist/regen-format.d.ts.map +1 -0
- package/dist/regen-format.js +545 -13
- package/dist/regen-format.js.map +1 -0
- package/dist/regen.d.ts.map +1 -0
- package/dist/regen.js +141 -5
- package/dist/regen.js.map +1 -0
- package/dist/remote-client.d.ts +65 -175
- package/dist/remote-client.d.ts.map +1 -0
- package/dist/remote-client.js +942 -1
- package/dist/remote-client.js.map +1 -0
- package/dist/reset-local-seat-cmd.d.ts +50 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -0
- package/dist/reset-local-seat-cmd.js +169 -0
- package/dist/reset-local-seat-cmd.js.map +1 -0
- package/dist/role-match.d.ts.map +1 -0
- package/dist/role-match.js +23 -1
- package/dist/role-match.js.map +1 -0
- package/dist/role-resolver.d.ts.map +1 -0
- package/dist/role-resolver.js +60 -1
- package/dist/role-resolver.js.map +1 -0
- package/dist/roster-render.d.ts.map +1 -0
- package/dist/roster-render.js +121 -3
- package/dist/roster-render.js.map +1 -0
- package/dist/seat-probe.d.ts +32 -13
- package/dist/seat-probe.d.ts.map +1 -0
- package/dist/seat-probe.js +78 -1
- package/dist/seat-probe.js.map +1 -0
- package/dist/seat-store.d.ts +76 -0
- package/dist/seat-store.d.ts.map +1 -0
- package/dist/seat-store.js +309 -0
- package/dist/seat-store.js.map +1 -0
- package/dist/seats.d.ts +303 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +551 -0
- package/dist/seats.js.map +1 -0
- package/dist/self-path.d.ts +27 -0
- package/dist/self-path.d.ts.map +1 -0
- package/dist/self-path.js +43 -0
- package/dist/self-path.js.map +1 -0
- package/dist/server-endpoint.d.ts +9 -0
- package/dist/server-endpoint.d.ts.map +1 -0
- package/dist/server-endpoint.js +38 -0
- package/dist/server-endpoint.js.map +1 -0
- package/dist/server-errors.d.ts +50 -0
- package/dist/server-errors.d.ts.map +1 -0
- package/dist/server-errors.js +79 -0
- package/dist/server-errors.js.map +1 -0
- package/dist/server-handshake.d.ts +197 -0
- package/dist/server-handshake.d.ts.map +1 -0
- package/dist/server-handshake.js +505 -0
- package/dist/server-handshake.js.map +1 -0
- package/dist/server-response.d.ts +3 -0
- package/dist/server-response.d.ts.map +1 -0
- package/dist/server-response.js +67 -0
- package/dist/server-response.js.map +1 -0
- package/dist/server-trust.d.ts +14 -0
- package/dist/server-trust.d.ts.map +1 -0
- package/dist/server-trust.js +225 -0
- package/dist/server-trust.js.map +1 -0
- package/dist/setup-confirm.d.ts +3 -3
- package/dist/setup-confirm.d.ts.map +1 -0
- package/dist/setup-confirm.js +126 -2
- package/dist/setup-confirm.js.map +1 -0
- package/dist/setup.d.ts +5 -3
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +169 -46
- package/dist/setup.js.map +1 -0
- package/dist/shell-escape.d.ts.map +1 -0
- package/dist/shell-escape.js +22 -1
- package/dist/shell-escape.js.map +1 -0
- package/dist/spawn.d.ts +2 -2
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +29 -10
- package/dist/spawn.js.map +1 -0
- package/dist/startup-services.d.ts +0 -1
- package/dist/startup-services.d.ts.map +1 -0
- package/dist/startup-services.js +26 -1
- package/dist/startup-services.js.map +1 -0
- package/dist/stream-owner.d.ts +6 -2
- package/dist/stream-owner.d.ts.map +1 -0
- package/dist/stream-owner.js +547 -2
- package/dist/stream-owner.js.map +1 -0
- package/dist/stream-status.d.ts +1 -1
- package/dist/stream-status.d.ts.map +1 -0
- package/dist/stream-status.js +268 -3
- package/dist/stream-status.js.map +1 -0
- package/dist/sync-roles-render.d.ts.map +1 -0
- package/dist/sync-roles-render.js +118 -3
- package/dist/sync-roles-render.js.map +1 -0
- package/dist/sync.d.ts +1 -1
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +286 -22
- package/dist/sync.js.map +1 -0
- package/dist/terminal-title.d.ts.map +1 -0
- package/dist/terminal-title.js +68 -1
- package/dist/terminal-title.js.map +1 -0
- package/dist/token-store.d.ts +17 -111
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +81 -1
- package/dist/token-store.js.map +1 -0
- package/dist/tool-manifest.d.ts +4 -8
- package/dist/tool-manifest.d.ts.map +1 -0
- package/dist/tool-manifest.js +554 -1
- package/dist/tool-manifest.js.map +1 -0
- package/dist/tool-scope.d.ts +8 -12
- package/dist/tool-scope.d.ts.map +1 -0
- package/dist/tool-scope.js +97 -1
- package/dist/tool-scope.js.map +1 -0
- package/dist/types.d.ts +1 -5
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -0
- package/dist/unknown-subcommand.js +37 -1
- package/dist/unknown-subcommand.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +78 -2
- package/dist/version.js.map +1 -0
- package/dist/working-repo.d.ts.map +1 -0
- package/dist/working-repo.js +81 -1
- package/dist/working-repo.js.map +1 -0
- package/dist/worktree-lifecycle.d.ts.map +1 -0
- package/dist/worktree-lifecycle.js +205 -2
- package/dist/worktree-lifecycle.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +40 -0
- package/docs/LOCAL_SERVER.md +103 -0
- package/docs/RELEASING.md +120 -0
- package/package.json +52 -34
- package/src/agent-runtime.ts +69 -0
- package/src/assimilate-cmd.ts +1864 -0
- package/src/assimilate-deps.ts +444 -0
- package/src/assimilate-guard.ts +84 -0
- package/src/assimilate-welcome.ts +39 -0
- package/src/auth-env.ts +21 -0
- package/src/backends/launch-all-pastelist.ts +18 -0
- package/src/backends/launch-all-tmux.ts +75 -0
- package/src/backends/launch-all-windows.ts +110 -0
- package/src/bare-launch-menu.ts +148 -0
- package/src/claude-launch-args.ts +35 -0
- package/src/claude.ts +561 -0
- package/src/cleanup-cmd.ts +581 -0
- package/src/clear-rewake-core.ts +34 -0
- package/src/clear-rewake.ts +18 -0
- package/src/cli-help.ts +116 -0
- package/src/cli-platform.ts +127 -0
- package/src/cli-tool-approval.ts +716 -0
- package/src/codex-app-server.ts +238 -0
- package/src/codex-app-wake.ts +512 -0
- package/src/codex-launch.ts +140 -0
- package/src/codex-remote.ts +500 -0
- package/src/codex-wake-resolve.ts +155 -0
- package/src/config-utils.ts +811 -0
- package/src/config.ts +595 -0
- package/src/console-prefix.ts +99 -0
- package/src/cube-name.ts +69 -0
- package/src/cubes.ts +583 -0
- package/src/debug.ts +73 -0
- package/src/direct-log.ts +9 -0
- package/src/docs-sections.ts +129 -0
- package/src/drone-lifecycle.ts +62 -0
- package/src/drone-management.ts +162 -0
- package/src/ensure-mcp-config.ts +54 -0
- package/src/evict-drone.ts +82 -0
- package/src/gc-orphan-inboxes.ts +229 -0
- package/src/inbox-monitor.ts +1005 -0
- package/src/index.ts +1201 -0
- package/src/launch-all-cmd.ts +450 -0
- package/src/launch-all-command.ts +30 -0
- package/src/launch-all-deps.ts +188 -0
- package/src/launch-all-discovery.ts +118 -0
- package/src/launch-all-locks.ts +74 -0
- package/src/launch-gate.ts +80 -0
- package/src/lifecycle-log-guard.ts +144 -0
- package/src/list-roles-render.ts +52 -0
- package/src/local-log-routing.ts +106 -0
- package/src/local-manage-tool-result.ts +10 -0
- package/src/local-server-cursor.ts +188 -0
- package/src/log-audit.ts +207 -0
- package/src/log-stream.ts +1505 -0
- package/src/model-presets.ts +22 -0
- package/src/name-validator.ts +23 -0
- package/src/opencode-drone.ts +528 -0
- package/src/opencode-plugin.ts +38 -0
- package/src/parse-assimilate-args.ts +125 -0
- package/src/parse-launch-all-args.ts +100 -0
- package/src/postinstall-banner.ts +37 -0
- package/src/postinstall.ts +36 -0
- package/src/readiness-probe.ts +12 -0
- package/src/regen-format.ts +651 -0
- package/src/regen.ts +150 -0
- package/src/remote-client.ts +1379 -0
- package/src/reset-local-seat-cmd.ts +227 -0
- package/src/role-match.ts +29 -0
- package/src/role-resolver.ts +93 -0
- package/src/roster-render.ts +196 -0
- package/src/seat-probe.ts +115 -0
- package/src/seat-store.ts +333 -0
- package/src/seats.ts +741 -0
- package/src/self-path.ts +51 -0
- package/src/server-endpoint.ts +38 -0
- package/src/server-errors.ts +93 -0
- package/src/server-handshake.ts +783 -0
- package/src/server-response.ts +76 -0
- package/src/server-trust.ts +245 -0
- package/src/setup-confirm.ts +171 -0
- package/src/setup.ts +186 -0
- package/src/shell-escape.ts +22 -0
- package/src/spawn.ts +31 -0
- package/src/startup-services.ts +36 -0
- package/src/stream-owner.ts +686 -0
- package/src/stream-status.ts +337 -0
- package/src/sync-roles-render.ts +163 -0
- package/src/sync.ts +363 -0
- package/src/terminal-title.ts +76 -0
- package/src/token-store.ts +102 -0
- package/src/tool-manifest.ts +610 -0
- package/src/tool-scope.ts +110 -0
- package/src/types.ts +5 -0
- package/src/unknown-subcommand.ts +35 -0
- package/src/version.ts +82 -0
- package/src/working-repo.ts +99 -0
- package/src/worktree-lifecycle.ts +254 -0
- package/dist/auth-recovery.d.ts +0 -33
- package/dist/auth-recovery.js +0 -1
- package/dist/auth.d.ts +0 -103
- package/dist/auth.js +0 -39
- package/dist/device-auth.d.ts +0 -75
- package/dist/device-auth.js +0 -1
- package/dist/drone-address.d.ts +0 -26
- package/dist/drone-address.js +0 -1
- package/dist/get-started.d.ts +0 -20
- package/dist/get-started.js +0 -2
- package/dist/health-beat.d.ts +0 -134
- package/dist/health-beat.js +0 -1
- package/dist/role-section.d.ts +0 -110
- package/dist/role-section.js +0 -8
- package/dist/setup-action.d.ts +0 -20
- package/dist/setup-action.js +0 -1
- package/dist/stale-version-check.d.ts +0 -40
- package/dist/stale-version-check.js +0 -1
- package/dist/subscription-retry.d.ts +0 -40
- package/dist/subscription-retry.js +0 -1
- package/dist/templates.d.ts +0 -107
- package/dist/templates.js +0 -359
- package/dist/token-crypto.d.ts +0 -50
- package/dist/token-crypto.js +0 -1
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { mkdirSync, chmodSync, readdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { CodexAppServerClient } from './codex-app-server.js';
|
|
7
|
+
import { codexBorgSessionConfigArgs, BORG_SESSION_ENV } from './launch-gate.js';
|
|
8
|
+
import { codexAppServerSocketConfigArgs } from './codex-wake-resolve.js';
|
|
9
|
+
import {
|
|
10
|
+
BORG_AGENT_KIND_ENV,
|
|
11
|
+
BORG_CODEX_REMOTE_WAKE_ENV,
|
|
12
|
+
codexAgentKindConfigArgs,
|
|
13
|
+
codexRemoteWakeConfigArgs,
|
|
14
|
+
} from './agent-runtime.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* gh#528 — npm-managed Codex remote-wake.
|
|
18
|
+
*
|
|
19
|
+
* borg owns a per-launch DIRECT Codex app-server (`codex app-server --listen
|
|
20
|
+
* unix://<socket>`) as the primary wake path, instead of the standalone
|
|
21
|
+
* `codex app-server daemon start` (which only exists for standalone-installer
|
|
22
|
+
* Codex — npm-managed Codex has no daemon, so those sessions had no push-wake
|
|
23
|
+
* and relied on periodic log-drain catch-up). Per-launch direct
|
|
24
|
+
* app-server works for both install kinds and starts fresh each launch (so it
|
|
25
|
+
* always loads the current borgmcp MCP — no daemon-restart version refresh
|
|
26
|
+
* needed). The TUI is then launched with `codex --remote unix://<socket>` and
|
|
27
|
+
* Borg delivers wakes by connecting to that socket (see codex-app-wake.ts).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface CodexAppServerHandle {
|
|
31
|
+
pid: number | undefined;
|
|
32
|
+
socketPath: string;
|
|
33
|
+
/** Kill the owned app-server + remove its socket/pidfile. Wire to TUI exit. */
|
|
34
|
+
cleanup: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CodexRemoteLaunch {
|
|
38
|
+
args: string[];
|
|
39
|
+
env: Record<string, string>;
|
|
40
|
+
warning?: string;
|
|
41
|
+
/** Present when borg owns a per-launch app-server that must be cleaned up on TUI exit. */
|
|
42
|
+
server?: CodexAppServerHandle;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CodexChild {
|
|
46
|
+
pid: number | undefined;
|
|
47
|
+
kill: () => void;
|
|
48
|
+
/** Snapshot app-server exit state + bounded stderr captured by the production spawn. */
|
|
49
|
+
diagnostics?: () => CodexChildDiagnostics;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CodexChildDiagnostics {
|
|
53
|
+
exited: boolean;
|
|
54
|
+
exitCode: number | null;
|
|
55
|
+
signal: string | null;
|
|
56
|
+
error?: string | null;
|
|
57
|
+
stderr: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PrepareCodexRemoteDeps {
|
|
61
|
+
/** Spawn the long-lived `codex app-server --listen unix://<socketPath>` child. */
|
|
62
|
+
spawnAppServer: (socketPath: string) => CodexChild;
|
|
63
|
+
/** Readiness probe: a real CodexAppServerClient connect + thread/loaded/list round-trip. */
|
|
64
|
+
probeReady: (socketPath: string) => Promise<boolean>;
|
|
65
|
+
/** Delay between readiness polls (injected so tests don't actually wait). */
|
|
66
|
+
sleep: (ms: number) => Promise<void>;
|
|
67
|
+
/** 0700 runtime dir (default ~/.config/borgmcp/codex-remote). */
|
|
68
|
+
runtimeDir?: string;
|
|
69
|
+
/** Unique socket id generator (default 32-hex). Injected for deterministic tests. */
|
|
70
|
+
socketId?: () => string;
|
|
71
|
+
/** Readiness timeout (default 30000ms) + poll interval (default 250ms). */
|
|
72
|
+
readyTimeoutMs?: number;
|
|
73
|
+
pollIntervalMs?: number;
|
|
74
|
+
/** Whether a pid is alive (default process.kill(pid, 0)). Injected for tests. */
|
|
75
|
+
isAlive?: (pid: number) => boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const DEFAULT_CODEX_REMOTE_DIR = join(homedir(), '.config', 'borgmcp', 'codex-remote');
|
|
79
|
+
const DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS = 30_000;
|
|
80
|
+
const MAX_CODEX_APP_SERVER_STDERR_CHARS = 16_384;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Keep app-server failure details useful without copying credentials or local
|
|
84
|
+
* filesystem locations into the parent process warning.
|
|
85
|
+
*/
|
|
86
|
+
function sanitizeCodexDiagnostic(input: string): string {
|
|
87
|
+
let sanitized = String(input).replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
88
|
+
sanitized = sanitized.replace(
|
|
89
|
+
/\b((?:authorization\s*:\s*)?bearer)\s+[^\s,;]+/gi,
|
|
90
|
+
'$1 <REDACTED>'
|
|
91
|
+
);
|
|
92
|
+
sanitized = sanitized.replace(
|
|
93
|
+
/\b([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/gi,
|
|
94
|
+
'$1<REDACTED>@'
|
|
95
|
+
);
|
|
96
|
+
sanitized = sanitized.replace(
|
|
97
|
+
/([?&](?:access_token|auth_token|id_token|refresh_token|session_token|token|api_key|apikey|secret|password)=)[^&#\s]*/gi,
|
|
98
|
+
'$1<REDACTED>'
|
|
99
|
+
);
|
|
100
|
+
sanitized = sanitized.replace(
|
|
101
|
+
/\b((?:[a-z0-9_-]*(?:token|secret|password)|api[_-]?key)\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)/gi,
|
|
102
|
+
'$1<REDACTED>'
|
|
103
|
+
);
|
|
104
|
+
sanitized = sanitized.replace(
|
|
105
|
+
/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
|
|
106
|
+
'<REDACTED>'
|
|
107
|
+
);
|
|
108
|
+
sanitized = sanitized.replace(
|
|
109
|
+
/(^|[\s("'=])(?:file:\/\/)?\/(?:Users|home|private|tmp|var|etc|opt|Volumes)\/[^\s"'`<>)}\]]+/gm,
|
|
110
|
+
'$1<REDACTED_PATH>'
|
|
111
|
+
);
|
|
112
|
+
sanitized = sanitized.replace(
|
|
113
|
+
/\b[A-Za-z]:\\(?:Users|Documents and Settings|ProgramData|Temp)\\[^\s"'`<>)}\]]+/g,
|
|
114
|
+
'<REDACTED_PATH>'
|
|
115
|
+
);
|
|
116
|
+
return sanitized.slice(-MAX_CODEX_APP_SERVER_STDERR_CHARS);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function withCodexCwdArg(args: string[], cwd: string): string[] {
|
|
120
|
+
if (hasCodexCwdArg(args)) return args;
|
|
121
|
+
return ['--cd', cwd, ...args];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Resolve the project directory Codex itself will use for this launch.
|
|
125
|
+
* Relative explicit paths are interpreted from the wrapper's current working
|
|
126
|
+
* directory, matching Codex's CLI path resolution. The final occurrence wins,
|
|
127
|
+
* matching clap's override behavior for repeated options. */
|
|
128
|
+
export function resolveCodexLaunchCwd(args: string[], fallbackCwd: string): string {
|
|
129
|
+
const baseCwd = resolve(fallbackCwd);
|
|
130
|
+
let selected = baseCwd;
|
|
131
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
132
|
+
const arg = args[index];
|
|
133
|
+
if (arg === '--') break;
|
|
134
|
+
if (arg === '--cd' || arg === '-C') {
|
|
135
|
+
const value = args[index + 1];
|
|
136
|
+
if (value !== undefined && value.length > 0) {
|
|
137
|
+
selected = resolve(baseCwd, value);
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (arg.startsWith('--cd=')) {
|
|
143
|
+
const value = arg.slice('--cd='.length);
|
|
144
|
+
if (value.length > 0) selected = resolve(baseCwd, value);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (arg.startsWith('-C') && arg.length > 2) {
|
|
148
|
+
const value = arg.slice(2).replace(/^=/, '');
|
|
149
|
+
if (value.length > 0) selected = resolve(baseCwd, value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return selected;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hasCodexCwdArg(args: string[]): boolean {
|
|
156
|
+
for (const arg of args) {
|
|
157
|
+
if (arg === '--') return false;
|
|
158
|
+
if (arg === '--cd' || arg.startsWith('--cd=') || arg === '-C' || (arg.startsWith('-C') && arg.length > 2)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function defaultIsAlive(pid: number): boolean {
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
// EPERM ⇒ the process exists but we can't signal it (still alive).
|
|
171
|
+
return err?.code === 'EPERM';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* gh#633: process-liveness probe for the borg-owned codex app-server — the
|
|
177
|
+
* transport-agnostic analogue of the claude tail-F Monitor's pgrep check
|
|
178
|
+
* (checkInboxMonitorHealthy / stream-status.ts:48). Codex drones wake via this
|
|
179
|
+
* app-server bridge, NOT a tail-F Monitor, so wake_path_client_monitor_armed is
|
|
180
|
+
* false-by-design for them; the HOP-2 wake-path-deaf classifier mis-read that
|
|
181
|
+
* as deaf (gh#633). This gives HOP-2 the codex wake path's ACTUAL health.
|
|
182
|
+
*
|
|
183
|
+
* Uses the app-server PIDFILE (written at spawn, beside the socket) +
|
|
184
|
+
* process.kill(pid, 0) — NOT pgrep. The codex TUI is also launched
|
|
185
|
+
* `codex --remote unix://<socketPath>`, so a `pgrep -f <socketPath>` would ALSO
|
|
186
|
+
* match the live TUI and FALSE-ARM when the app-server has crashed but the TUI
|
|
187
|
+
* still runs (the deaf-but-alive case). The pidfile holds the EXACT app-server
|
|
188
|
+
* pid, so kill(0) reflects the app-server's liveness specifically — and it's
|
|
189
|
+
* cheaper than pgrep (no subprocess). Mirrors pruneStaleSockets' pid check.
|
|
190
|
+
*
|
|
191
|
+
* Tri-state (mirrors checkInboxMonitorHealthy's boolean|null contract):
|
|
192
|
+
* - true: pidfile resolves to a LIVE pid → app-server (bridge) is up → armed.
|
|
193
|
+
* - false: pidfile resolves to a DEAD pid → an unclean exit (crash/kill -9)
|
|
194
|
+
* left a stale pidfile (cleanup never ran) → bridge down → HOP-2
|
|
195
|
+
* correctly flags a genuinely-deaf codex drone (no SLI-lie).
|
|
196
|
+
* - null: pidfile missing / unreadable / unparseable → cannot determine →
|
|
197
|
+
* caller maps null→armed (false-deaf-avoidance, same as the claude
|
|
198
|
+
* monitor branch). A CLEAN app-server exit removes the pidfile, but
|
|
199
|
+
* then the drone is shutting down → the silent-stall watchdog
|
|
200
|
+
* backstops it via a separate layer.
|
|
201
|
+
*
|
|
202
|
+
* Residual (negligible, gh#633 / Coordinator 6f28fe3f): PID reuse — if the
|
|
203
|
+
* crashed app-server's pid is recycled by an unrelated process before the next
|
|
204
|
+
* launch's pruneStaleSockets removes the stale pidfile, kill(0) reports alive →
|
|
205
|
+
* a brief false-arm. The window is tiny (exact-pid reuse during the crash gap)
|
|
206
|
+
* and self-heals on the next launch's prune; far smaller than existsSync's
|
|
207
|
+
* always-on stale-file masking.
|
|
208
|
+
*/
|
|
209
|
+
export function checkCodexBridgeHealthy(
|
|
210
|
+
socketPath: string | null,
|
|
211
|
+
deps: {
|
|
212
|
+
isAlive?: (pid: number) => boolean;
|
|
213
|
+
readPidFile?: (pidPath: string) => string;
|
|
214
|
+
} = {}
|
|
215
|
+
): boolean | null {
|
|
216
|
+
if (!socketPath) return null;
|
|
217
|
+
const isAlive = deps.isAlive ?? defaultIsAlive;
|
|
218
|
+
const readPidFile =
|
|
219
|
+
deps.readPidFile ?? ((pidPath: string) => readFileSync(pidPath, 'utf-8'));
|
|
220
|
+
const pidPath = socketPath.replace(/\.sock$/, '.pid');
|
|
221
|
+
try {
|
|
222
|
+
const pid = Number.parseInt(readPidFile(pidPath).trim(), 10);
|
|
223
|
+
if (Number.isNaN(pid)) return null;
|
|
224
|
+
return isAlive(pid);
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function safeRm(p: string): void {
|
|
231
|
+
try {
|
|
232
|
+
rmSync(p, { force: true });
|
|
233
|
+
} catch {
|
|
234
|
+
// best-effort
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Remove sockets in the owned dir whose owning app-server pid is no longer
|
|
240
|
+
* alive (crashed prior launches), leaving live concurrent sessions' sockets
|
|
241
|
+
* untouched. Operates ONLY inside the borg-owned runtime dir.
|
|
242
|
+
*/
|
|
243
|
+
function pruneStaleSockets(runtimeDir: string, isAlive: (pid: number) => boolean): void {
|
|
244
|
+
let entries: string[];
|
|
245
|
+
try {
|
|
246
|
+
entries = readdirSync(runtimeDir);
|
|
247
|
+
} catch {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
for (const name of entries) {
|
|
251
|
+
if (!name.endsWith('.pid')) continue;
|
|
252
|
+
const pidPath = join(runtimeDir, name);
|
|
253
|
+
const sockPath = join(runtimeDir, name.replace(/\.pid$/, '.sock'));
|
|
254
|
+
let pid: number;
|
|
255
|
+
try {
|
|
256
|
+
pid = Number.parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
257
|
+
} catch {
|
|
258
|
+
safeRm(pidPath);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (Number.isNaN(pid) || !isAlive(pid)) {
|
|
262
|
+
safeRm(sockPath);
|
|
263
|
+
safeRm(pidPath);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function failLoud(reason: string): CodexRemoteLaunch {
|
|
269
|
+
return { args: [], env: {}, warning: reason };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatExitReason(diagnostics: CodexChildDiagnostics): string {
|
|
273
|
+
if (diagnostics.error) return `process error: ${sanitizeCodexDiagnostic(diagnostics.error)}`;
|
|
274
|
+
if (diagnostics.exitCode != null) return `exit code ${diagnostics.exitCode}`;
|
|
275
|
+
if (diagnostics.signal) return `signal ${diagnostics.signal}`;
|
|
276
|
+
return 'unknown exit status';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function formatStderr(stderr: string): string {
|
|
280
|
+
const trimmed = sanitizeCodexDiagnostic(stderr).trim();
|
|
281
|
+
return trimmed ? ` Stderr: ${trimmed}` : '';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Start a borg-owned per-launch Codex app-server, probe it for readiness, and
|
|
286
|
+
* return the `--remote` launch args + an owned handle (or a fail-loud warning).
|
|
287
|
+
* Async + lifecycle-owning: the caller MUST call `result.server?.cleanup()` on
|
|
288
|
+
* TUI exit.
|
|
289
|
+
*/
|
|
290
|
+
export async function prepareCodexRemoteLaunch(
|
|
291
|
+
deps: PrepareCodexRemoteDeps
|
|
292
|
+
): Promise<CodexRemoteLaunch> {
|
|
293
|
+
const runtimeDir = deps.runtimeDir ?? DEFAULT_CODEX_REMOTE_DIR;
|
|
294
|
+
const isAlive = deps.isAlive ?? defaultIsAlive;
|
|
295
|
+
const readyTimeoutMs = deps.readyTimeoutMs ?? DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS;
|
|
296
|
+
const pollIntervalMs = deps.pollIntervalMs ?? 250;
|
|
297
|
+
|
|
298
|
+
// 1. 0700 owned dir + prune crashed prior sockets (concurrent-safe via pid liveness).
|
|
299
|
+
try {
|
|
300
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
301
|
+
chmodSync(runtimeDir, 0o700); // enforce 0700 even if it pre-existed with looser perms
|
|
302
|
+
pruneStaleSockets(runtimeDir, isAlive);
|
|
303
|
+
} catch (err: any) {
|
|
304
|
+
return failLoud(
|
|
305
|
+
`Codex remote-wake disabled: could not prepare ${runtimeDir} (${err?.message ?? err}); run borg_regen manually.`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 2. unique, non-predictable socket path inside the owned dir.
|
|
310
|
+
const id = (deps.socketId ?? (() => randomBytes(16).toString('hex')))();
|
|
311
|
+
const socketPath = join(runtimeDir, `${id}.sock`);
|
|
312
|
+
const pidPath = join(runtimeDir, `${id}.pid`);
|
|
313
|
+
|
|
314
|
+
// 3. spawn the long-lived app-server.
|
|
315
|
+
let child: CodexChild;
|
|
316
|
+
try {
|
|
317
|
+
child = deps.spawnAppServer(socketPath);
|
|
318
|
+
} catch (err: any) {
|
|
319
|
+
safeRm(socketPath);
|
|
320
|
+
return failLoud(
|
|
321
|
+
`Codex remote-wake disabled: could not start \`codex app-server\` (${sanitizeCodexDiagnostic(String(err?.message ?? err))}) — ` +
|
|
322
|
+
`confirm the Codex executable is installed and available on PATH. ` +
|
|
323
|
+
`Remote wake is unavailable for this session; run borg_regen manually to catch up.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (child.pid != null) {
|
|
327
|
+
try {
|
|
328
|
+
writeFileSync(pidPath, String(child.pid));
|
|
329
|
+
} catch {
|
|
330
|
+
// pidfile is only for stale-prune; launch still proceeds.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const cleanup = () => {
|
|
335
|
+
try {
|
|
336
|
+
child.kill();
|
|
337
|
+
} catch {
|
|
338
|
+
// best-effort
|
|
339
|
+
}
|
|
340
|
+
safeRm(socketPath);
|
|
341
|
+
safeRm(pidPath);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// 4. readiness via a real protocol round-trip — bounded attempts (no clock dep).
|
|
345
|
+
const attempts = Math.max(1, Math.ceil(readyTimeoutMs / pollIntervalMs));
|
|
346
|
+
let ready = false;
|
|
347
|
+
let exitDiagnostics: CodexChildDiagnostics | undefined;
|
|
348
|
+
for (let i = 0; i < attempts && !ready; i++) {
|
|
349
|
+
try {
|
|
350
|
+
ready = await deps.probeReady(socketPath);
|
|
351
|
+
} catch {
|
|
352
|
+
ready = false;
|
|
353
|
+
}
|
|
354
|
+
if (!ready) {
|
|
355
|
+
const diagnostics = child.diagnostics?.();
|
|
356
|
+
if (diagnostics?.exited) {
|
|
357
|
+
exitDiagnostics = diagnostics;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!ready && i < attempts - 1) await deps.sleep(pollIntervalMs);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!ready) {
|
|
365
|
+
const diagnostics = exitDiagnostics ?? child.diagnostics?.();
|
|
366
|
+
cleanup();
|
|
367
|
+
if (diagnostics?.exited) {
|
|
368
|
+
return failLoud(
|
|
369
|
+
`Codex remote-wake disabled: \`codex app-server\` exited before becoming ready ` +
|
|
370
|
+
`(${formatExitReason(diagnostics)}).${formatStderr(diagnostics.stderr)} ` +
|
|
371
|
+
`Run borg_regen manually to catch up.`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return failLoud(
|
|
375
|
+
`Codex remote-wake disabled: \`codex app-server\` remained running but did not become ready ` +
|
|
376
|
+
`at ${sanitizeCodexDiagnostic(socketPath)} within ${readyTimeoutMs}ms.${formatStderr(diagnostics?.stderr ?? '')} ` +
|
|
377
|
+
`Run borg_regen manually to catch up.`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 5. ready → owned remote launch.
|
|
382
|
+
return {
|
|
383
|
+
args: ['--remote', `unix://${socketPath}`],
|
|
384
|
+
env: { [BORG_CODEX_REMOTE_WAKE_ENV]: '1' },
|
|
385
|
+
server: { pid: child.pid, socketPath, cleanup },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Production deps for prepareCodexRemoteLaunch — spawn the real `codex
|
|
391
|
+
* app-server` child + probe it with the real CodexAppServerClient. Shared by
|
|
392
|
+
* claude.ts and assimilate-deps.ts so there's ONE wiring.
|
|
393
|
+
*
|
|
394
|
+
* The readiness probe uses Codex app-server RPCs ONLY (connect + thread/loaded/
|
|
395
|
+
* list) — it never calls a Borg coordination endpoint — so it can never advance
|
|
396
|
+
* last_seen/last_regen_at and mask a deaf Codex (the gh#46/gh#406 signal-truth
|
|
397
|
+
* invariant; the app-server socket is the wake-DELIVERY wire, not a liveness
|
|
398
|
+
* signal).
|
|
399
|
+
*/
|
|
400
|
+
export function defaultCodexRemoteDeps(): Pick<
|
|
401
|
+
PrepareCodexRemoteDeps,
|
|
402
|
+
'spawnAppServer' | 'probeReady' | 'sleep'
|
|
403
|
+
> {
|
|
404
|
+
return {
|
|
405
|
+
spawnAppServer: (socketPath) => {
|
|
406
|
+
// gh#851: this app-server — NOT the `codex --remote` TUI — spawns the
|
|
407
|
+
// borg-mcp MCP child, so the BORG_SESSION activation marker (gh#673) must
|
|
408
|
+
// ride HERE too, or the child gates dormant. Load-bearing = the `-c
|
|
409
|
+
// mcp_servers.borg.env.BORG_SESSION="1"` override (codex MCP children read
|
|
410
|
+
// only the pinned env, injected via the app-server's -c — same V2b
|
|
411
|
+
// mechanism the TUI launch uses, applied at the app-server boundary). The
|
|
412
|
+
// env BORG_SESSION=1 is a defensive belt (inherited env never reaches the
|
|
413
|
+
// codex MCP child by itself). Still activation-only per the launch-gate
|
|
414
|
+
// SR-BINDING — never a security/access signal.
|
|
415
|
+
//
|
|
416
|
+
// gh#855: pin THIS app-server's live socket into the same child env via the
|
|
417
|
+
// same `-c` channel, so the waking borg-mcp child is authoritative about
|
|
418
|
+
// its OWN socket and re-resolves the loaded thread FRESH each wake — a
|
|
419
|
+
// missed/stale launch probe can no longer cause permanent deafness. The
|
|
420
|
+
// socketPath is borg-generated (randomBytes), never user input, TOML-quoted
|
|
421
|
+
// exactly like the BORG_SESSION override — zero injection surface.
|
|
422
|
+
const child = spawn(
|
|
423
|
+
'codex',
|
|
424
|
+
[
|
|
425
|
+
'app-server',
|
|
426
|
+
...codexBorgSessionConfigArgs(),
|
|
427
|
+
...codexAgentKindConfigArgs(),
|
|
428
|
+
...codexRemoteWakeConfigArgs(),
|
|
429
|
+
...codexAppServerSocketConfigArgs(socketPath),
|
|
430
|
+
'--listen',
|
|
431
|
+
`unix://${socketPath}`,
|
|
432
|
+
],
|
|
433
|
+
{
|
|
434
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
435
|
+
shell: false,
|
|
436
|
+
env: {
|
|
437
|
+
...process.env,
|
|
438
|
+
[BORG_SESSION_ENV]: '1',
|
|
439
|
+
[BORG_AGENT_KIND_ENV]: 'codex',
|
|
440
|
+
[BORG_CODEX_REMOTE_WAKE_ENV]: '1',
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
let stderr = '';
|
|
445
|
+
let exited = false;
|
|
446
|
+
let exitCode: number | null = null;
|
|
447
|
+
let signal: string | null = null;
|
|
448
|
+
let error: string | null = null;
|
|
449
|
+
child.stderr?.setEncoding('utf8');
|
|
450
|
+
child.stderr?.on('data', (chunk: string | Buffer) => {
|
|
451
|
+
stderr = `${stderr}${String(chunk)}`.slice(-MAX_CODEX_APP_SERVER_STDERR_CHARS);
|
|
452
|
+
});
|
|
453
|
+
// `close` follows stdio drain; using it instead of `exit` keeps the final
|
|
454
|
+
// stderr chunk available when readiness reports the child failure.
|
|
455
|
+
child.once('close', (code, exitSignal) => {
|
|
456
|
+
exited = true;
|
|
457
|
+
exitCode = code;
|
|
458
|
+
signal = exitSignal;
|
|
459
|
+
});
|
|
460
|
+
child.once('error', (childError) => {
|
|
461
|
+
exited = true;
|
|
462
|
+
error = sanitizeCodexDiagnostic(childError.message);
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
pid: child.pid,
|
|
466
|
+
kill: () => {
|
|
467
|
+
try {
|
|
468
|
+
child.kill();
|
|
469
|
+
} catch {
|
|
470
|
+
// best-effort
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
diagnostics: () => ({
|
|
474
|
+
exited,
|
|
475
|
+
exitCode,
|
|
476
|
+
signal,
|
|
477
|
+
error: error == null ? null : sanitizeCodexDiagnostic(error),
|
|
478
|
+
stderr: sanitizeCodexDiagnostic(stderr),
|
|
479
|
+
}),
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
probeReady: async (socketPath) => {
|
|
483
|
+
const probe = new CodexAppServerClient(socketPath);
|
|
484
|
+
try {
|
|
485
|
+
await probe.connect();
|
|
486
|
+
await probe.loadedThreadIds();
|
|
487
|
+
return true;
|
|
488
|
+
} catch {
|
|
489
|
+
return false;
|
|
490
|
+
} finally {
|
|
491
|
+
try {
|
|
492
|
+
probe.close();
|
|
493
|
+
} catch {
|
|
494
|
+
// best-effort
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#855 — pure helpers for FRESH codex wake-target re-resolution.
|
|
3
|
+
*
|
|
4
|
+
* Root cause of codex deaf-when-idle: the wake target (socket + thread) was
|
|
5
|
+
* resolved once at launch and never refreshed, so a missed/stale launch probe
|
|
6
|
+
* left the drone permanently deaf. Phase 1 makes the waking borg-mcp child
|
|
7
|
+
* authoritative about its OWN live app-server socket — the socket is injected
|
|
8
|
+
* into the child's pinned env at spawn (codex-remote.ts, via the #851 `-c
|
|
9
|
+
* mcp_servers.borg.env.X` channel) — and re-resolves the loaded thread FRESH on
|
|
10
|
+
* every wake (loadedThreadIds is a re-runnable RPC).
|
|
11
|
+
*
|
|
12
|
+
* These are the pure pieces; the IO orchestration lives in codex-app-wake.ts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Pinned-env var carrying THIS drone's live app-server socket (set at spawn). */
|
|
16
|
+
export const BORG_CODEX_APP_SERVER_SOCKET_ENV = 'BORG_CODEX_APP_SERVER_SOCKET';
|
|
17
|
+
|
|
18
|
+
/** The live app-server socket for this borg-mcp child, or null (un-upgraded launch). */
|
|
19
|
+
export function codexAppServerSocketFromEnv(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
20
|
+
const v = env[BORG_CODEX_APP_SERVER_SOCKET_ENV];
|
|
21
|
+
return v && v.length > 0 ? v : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The per-launch codex config override that pins THIS app-server's live socket
|
|
26
|
+
* into the borg-mcp child's [mcp_servers.borg.env] — the same `-c` channel the
|
|
27
|
+
* #851 BORG_SESSION marker rides (codex MCP children read only the pinned env,
|
|
28
|
+
* never inherited env). The socketPath is borg-generated (randomBytes under
|
|
29
|
+
* ~/.config/borgmcp/codex-remote), never user input; TOML-quoted exactly like
|
|
30
|
+
* the BORG_SESSION override, so there is zero injection surface.
|
|
31
|
+
*/
|
|
32
|
+
export function codexAppServerSocketConfigArgs(socketPath: string): string[] {
|
|
33
|
+
return ['-c', `mcp_servers.borg.env.${BORG_CODEX_APP_SERVER_SOCKET_ENV}="${socketPath}"`];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CodexThreadInfo {
|
|
37
|
+
id: string;
|
|
38
|
+
cwd?: string;
|
|
39
|
+
updatedAt?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pick the loaded thread to wake on the live socket. Each borg-owned app-server
|
|
44
|
+
* is fresh-per-launch / single-session, so the common case is exactly one loaded
|
|
45
|
+
* thread. When more than one is loaded, prefer the thread whose cwd matches this
|
|
46
|
+
* drone's working directory (sibling worktrees have distinct cwds), then the
|
|
47
|
+
* newest by updatedAt — always deterministic. No loaded thread → null (no wake
|
|
48
|
+
* this cycle; the next wake retries, so a transient empty list never causes
|
|
49
|
+
* permanent deafness).
|
|
50
|
+
*/
|
|
51
|
+
export function pickFreshThread(threads: CodexThreadInfo[], opts: { cwd: string }): string | null {
|
|
52
|
+
if (threads.length === 0) return null;
|
|
53
|
+
if (threads.length === 1) return threads[0].id;
|
|
54
|
+
const cwdMatches = threads.filter((t) => t.cwd === opts.cwd);
|
|
55
|
+
const pool = cwdMatches.length > 0 ? cwdMatches : threads;
|
|
56
|
+
let best = pool[0];
|
|
57
|
+
for (const t of pool) {
|
|
58
|
+
if ((t.updatedAt ?? 0) > (best.updatedAt ?? 0)) best = t;
|
|
59
|
+
}
|
|
60
|
+
return best.id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Pure prune: drop wake-target entries whose app-server socket is positively
|
|
65
|
+
* dead (liveness === false), so the file self-heals. Keeps alive (true) and
|
|
66
|
+
* indeterminate (null) entries — false-deaf-avoidance, mirroring
|
|
67
|
+
* checkCodexBridgeHealthy's tri-state. Returns the surviving map + whether
|
|
68
|
+
* anything changed (so the caller writes only on change).
|
|
69
|
+
*/
|
|
70
|
+
export function pruneDeadWakeTargets<T extends { socketPath: string }>(
|
|
71
|
+
targets: Record<string, T>,
|
|
72
|
+
socketLiveness: (socketPath: string) => boolean | null
|
|
73
|
+
): { targets: Record<string, T>; changed: boolean } {
|
|
74
|
+
const out: Record<string, T> = {};
|
|
75
|
+
let changed = false;
|
|
76
|
+
for (const [key, t] of Object.entries(targets)) {
|
|
77
|
+
if (socketLiveness(t.socketPath) === false) {
|
|
78
|
+
changed = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out[key] = t;
|
|
82
|
+
}
|
|
83
|
+
return { targets: out, changed };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether the freshly-resolved target differs from what's already recorded —
|
|
88
|
+
* so the self-healing cache write happens only on change (no file thrash on a
|
|
89
|
+
* busy cube re-resolving the same socket+thread every wake).
|
|
90
|
+
*/
|
|
91
|
+
export function wakeTargetChanged(
|
|
92
|
+
existing: { socketPath: string; threadId: string } | null,
|
|
93
|
+
fresh: { socketPath: string; threadId: string }
|
|
94
|
+
): boolean {
|
|
95
|
+
return (
|
|
96
|
+
!existing || existing.socketPath !== fresh.socketPath || existing.threadId !== fresh.threadId
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── gh#857 Phase 2: durable retry + heartbeat (pure helpers) ───────────────
|
|
101
|
+
|
|
102
|
+
/** Base backoff for the first retry of a dropped/deferred wake. */
|
|
103
|
+
export const WAKE_RETRY_BASE_MS = 5_000;
|
|
104
|
+
/** Backoff ceiling — a wedged thread is retried at most this often. */
|
|
105
|
+
export const WAKE_RETRY_CAP_MS = 60_000;
|
|
106
|
+
/**
|
|
107
|
+
* Age cap: a pending wake older than this is given up (dropped from the queue).
|
|
108
|
+
* Generous on purpose — the WI-2 heartbeat is the backstop beyond it, and the
|
|
109
|
+
* server read-cursor means the next delivery (heartbeat or a fresh entry) drains
|
|
110
|
+
* everything anyway, so an aged-out single wake is never a permanent miss.
|
|
111
|
+
*/
|
|
112
|
+
export const WAKE_RETRY_MAX_AGE_MS = 45 * 60_000;
|
|
113
|
+
/**
|
|
114
|
+
* Hard iteration ceiling for the retry-drain loop — a defensive belt ALONGSIDE
|
|
115
|
+
* the time-based age cap. In prod the age cap (real clock) terminates the loop in
|
|
116
|
+
* ~45-50 attempts; this ceiling only matters if the clock fails to advance
|
|
117
|
+
* (pathological / a non-advancing injected clock in tests) where a time-only
|
|
118
|
+
* guard would hot-spin forever. Set far above any real attempt count.
|
|
119
|
+
*/
|
|
120
|
+
export const WAKE_RETRY_MAX_ATTEMPTS = 1000;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Exponential backoff (ms) for the Nth retry of a pending wake (0-based),
|
|
124
|
+
* doubling from WAKE_RETRY_BASE_MS, saturating at WAKE_RETRY_CAP_MS, plus
|
|
125
|
+
* caller-supplied jitter so co-located sibling drones don't retry in lockstep.
|
|
126
|
+
*/
|
|
127
|
+
export function wakeRetryBackoffMs(attempts: number, jitter = 0): number {
|
|
128
|
+
const n = Math.max(0, attempts);
|
|
129
|
+
const exp = WAKE_RETRY_BASE_MS * 2 ** n;
|
|
130
|
+
return Math.min(exp, WAKE_RETRY_CAP_MS) + jitter;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** True once a pending wake has outlived the age cap (give up; heartbeat backstops). */
|
|
134
|
+
export function wakeRetryExpired(
|
|
135
|
+
firstEnqueuedAt: number,
|
|
136
|
+
now: number,
|
|
137
|
+
maxAgeMs = WAKE_RETRY_MAX_AGE_MS
|
|
138
|
+
): boolean {
|
|
139
|
+
return now - firstEnqueuedAt >= maxAgeMs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* WI-2 double-fire avoidance: the periodic heartbeat fires only when no wake (or
|
|
144
|
+
* prior heartbeat) delivery landed within the cadence window — so an active cube
|
|
145
|
+
* with flowing per-entry wakes doesn't get redundant heartbeat injections. A
|
|
146
|
+
* never-delivered seat (null) always fires.
|
|
147
|
+
*/
|
|
148
|
+
export function shouldFireHeartbeat(
|
|
149
|
+
lastDeliveredAt: number | null,
|
|
150
|
+
now: number,
|
|
151
|
+
cadenceMs: number
|
|
152
|
+
): boolean {
|
|
153
|
+
if (lastDeliveredAt === null) return true;
|
|
154
|
+
return now - lastDeliveredAt >= cadenceMs;
|
|
155
|
+
}
|