borgmcp 1.1.15 → 2.0.2
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 +85 -36
- 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 +9 -11
- package/dist/codex-app-wake.d.ts.map +1 -0
- package/dist/codex-app-wake.js +436 -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 +4 -4
- 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 +35 -18
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +1270 -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 +88 -173
- package/dist/remote-client.d.ts.map +1 -0
- package/dist/remote-client.js +997 -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 +375 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +670 -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 +533 -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/session-continuity.d.ts +33 -0
- package/dist/session-continuity.d.ts.map +1 -0
- package/dist/session-continuity.js +220 -0
- package/dist/session-continuity.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 +42 -0
- package/docs/LOCAL_SERVER.md +103 -0
- package/docs/RELEASING.md +129 -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 +524 -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 +1567 -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 +1457 -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 +911 -0
- package/src/self-path.ts +51 -0
- package/src/server-endpoint.ts +38 -0
- package/src/server-errors.ts +95 -0
- package/src/server-handshake.ts +816 -0
- package/src/server-response.ts +76 -0
- package/src/server-trust.ts +245 -0
- package/src/session-continuity.ts +280 -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,524 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getActiveCube,
|
|
3
|
+
getCodexWakeTarget,
|
|
4
|
+
setCodexWakeTarget,
|
|
5
|
+
type ActiveCube,
|
|
6
|
+
} from './cubes.js';
|
|
7
|
+
import { CodexAppServerClient } from './codex-app-server.js';
|
|
8
|
+
import { checkCodexBridgeHealthy } from './codex-remote.js';
|
|
9
|
+
import { hasPendingWakeActivity } from './remote-client.js';
|
|
10
|
+
import {
|
|
11
|
+
BORG_CODEX_REMOTE_WAKE_ENV,
|
|
12
|
+
resolveSessionAgentKind,
|
|
13
|
+
} from './agent-runtime.js';
|
|
14
|
+
import {
|
|
15
|
+
codexAppServerSocketFromEnv,
|
|
16
|
+
pickFreshThread,
|
|
17
|
+
wakeTargetChanged,
|
|
18
|
+
wakeRetryBackoffMs,
|
|
19
|
+
wakeRetryExpired,
|
|
20
|
+
WAKE_RETRY_MAX_ATTEMPTS,
|
|
21
|
+
shouldFireHeartbeat,
|
|
22
|
+
type CodexThreadInfo,
|
|
23
|
+
} from './codex-wake-resolve.js';
|
|
24
|
+
|
|
25
|
+
export const CODEX_WAKE_PROMPT =
|
|
26
|
+
'New Borg cube-log activity arrived.';
|
|
27
|
+
|
|
28
|
+
export function formatCodexWakePrompt(inboxLine: string): string {
|
|
29
|
+
return `New Borg cube-log activity arrived:\n${inboxLine}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// gh#708: STATIC catch-up/drain prompt (zero interpolation — no token/secret/PII/
|
|
33
|
+
// cube-content; the entry bodies are fetched by codex itself via the
|
|
34
|
+
// grant/visibility-gated borg_read-log, never injected into the wake). Delivered
|
|
35
|
+
// once after a wake is deferred (mid-turn thread) or retried (transient miss), to
|
|
36
|
+
// fire the already-shipped drain so no entry is skipped. gh#857 WI-2 reuses it as
|
|
37
|
+
// the periodic heartbeat prompt.
|
|
38
|
+
export const CODEX_CATCHUP_PROMPT =
|
|
39
|
+
'Borg cube activity arrived while you were busy. Wake triage: run `borg_read-log unread_only=true` and DRAIN — repeat until the returned page is under the limit and behind_by is 0 — so no entries are skipped. Then handle actionable entries; if none, resume the prior interrupted work.';
|
|
40
|
+
|
|
41
|
+
export function isCodexRemoteWakeEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
42
|
+
return env[BORG_CODEX_REMOTE_WAKE_ENV] === '1';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { resolveSessionAgentKind } from './agent-runtime.js';
|
|
46
|
+
|
|
47
|
+
export interface CodexWakeTarget {
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveCodexWakeTarget(env: NodeJS.ProcessEnv = process.env): CodexWakeTarget {
|
|
52
|
+
if (!isCodexRemoteWakeEnabled(env)) {
|
|
53
|
+
return { enabled: false };
|
|
54
|
+
}
|
|
55
|
+
return { enabled: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* gh#633: resolve a codex drone's transport-agnostic "wake-path-armed" signal
|
|
60
|
+
* from its OWN runtime wake mechanism — the app-server bridge's process
|
|
61
|
+
* liveness, the codex analogue of the claude tail-F Monitor health. Fed into
|
|
62
|
+
* the health beat so the HOP-2 wake-path-deaf classifier reads a
|
|
63
|
+
* transport-agnostic armed signal instead of the claude-shaped monitor_armed
|
|
64
|
+
* (which is false-by-design for codex and falsely flagged them, gh#633).
|
|
65
|
+
*
|
|
66
|
+
* Tri-state (boolean|null; caller maps null→armed for false-deaf-avoidance):
|
|
67
|
+
* - false ONLY on a positively-dead bridge: no wake target registered (the
|
|
68
|
+
* bridge cannot deliver wakes), OR the app-server pid is dead.
|
|
69
|
+
* - true when the wake target resolves AND the app-server pid is alive.
|
|
70
|
+
* - null when the bridge health is indeterminate (target read or pid check
|
|
71
|
+
* could not resolve) → armed (don't false-flag on uncertainty).
|
|
72
|
+
*/
|
|
73
|
+
export async function probeCodexBridgeArmed(
|
|
74
|
+
active: { cubeId: string; droneId: string },
|
|
75
|
+
deps: {
|
|
76
|
+
getCodexWakeTarget?: typeof getCodexWakeTarget;
|
|
77
|
+
checkBridge?: typeof checkCodexBridgeHealthy;
|
|
78
|
+
} = {}
|
|
79
|
+
): Promise<boolean | null> {
|
|
80
|
+
try {
|
|
81
|
+
const resolve = deps.getCodexWakeTarget ?? getCodexWakeTarget;
|
|
82
|
+
const target = await resolve(active.cubeId, active.droneId);
|
|
83
|
+
// No registered wake target → the bridge cannot deliver a wake → not armed.
|
|
84
|
+
if (!target) return false;
|
|
85
|
+
const check = deps.checkBridge ?? checkCodexBridgeHealthy;
|
|
86
|
+
return check(target.socketPath);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let wakeInFlight = false;
|
|
93
|
+
const pendingWakeRequests: Array<{
|
|
94
|
+
reason: string;
|
|
95
|
+
deps: CodexWakeDeps;
|
|
96
|
+
}> = [];
|
|
97
|
+
const deliveredWakeKeys = new Set<string>();
|
|
98
|
+
const deliveredWakeKeyOrder: string[] = [];
|
|
99
|
+
const DELIVERED_WAKE_KEY_CAP = 100;
|
|
100
|
+
|
|
101
|
+
// gh#708/#857: a single in-flight retry-drain loop coalesces ALL wakes deferred
|
|
102
|
+
// (mid-turn thread) or missed (transient error) into ONE retried-until-delivered
|
|
103
|
+
// drain. The coalesce gate means a burst collapses to one poller, not N.
|
|
104
|
+
let retryDrainInFlight = false;
|
|
105
|
+
|
|
106
|
+
// gh#857 WI-2: timestamp of the last SUCCESSFUL wake delivery (per-entry OR
|
|
107
|
+
// retry-drain OR heartbeat). The heartbeat reads this (shouldFireHeartbeat) to
|
|
108
|
+
// skip when a delivery already landed inside the cadence window. Module-scoped
|
|
109
|
+
// because the wake path and the heartbeat run in the same MCP-client child.
|
|
110
|
+
let lastDeliveredAt: number | null = null;
|
|
111
|
+
|
|
112
|
+
/** gh#857 WI-2: last successful wake delivery time (for the heartbeat gate). */
|
|
113
|
+
export function getLastDeliveredAt(): number | null {
|
|
114
|
+
return lastDeliveredAt;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function markDelivered(deps: CodexWakeDeps): void {
|
|
118
|
+
lastDeliveredAt = (deps.now ?? Date.now)();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// gh#857 WI-2: a single-in-flight guard for the heartbeat tick (mirrors
|
|
122
|
+
// wakeInFlight + retryDrainInFlight). Without it, a tick that stalls in IO past
|
|
123
|
+
// the next interval could let a second tick read the stale (pre-markDelivered)
|
|
124
|
+
// lastDeliveredAt, pass its gate, and double-inject the drain — the exact
|
|
125
|
+
// double-fire the design forbids.
|
|
126
|
+
let heartbeatInFlight = false;
|
|
127
|
+
|
|
128
|
+
// gh#861 finding 1: a SINGLE module-scoped mutex serializing the resolve+inject
|
|
129
|
+
// critical section ACROSS all three injecting paths (per-entry wake, retry-drain,
|
|
130
|
+
// heartbeat tick). The per-path flags above (wakeInFlight / retryDrainInFlight /
|
|
131
|
+
// heartbeatInFlight) each serialize their OWN path but NOT across paths — so a
|
|
132
|
+
// per-entry WAKE prompt and a heartbeat/retry DRAIN prompt could land in the same
|
|
133
|
+
// codex thread concurrently (double-inject collision). This lock closes that
|
|
134
|
+
// cross-path race: a path that cannot acquire it backs off via its own retry
|
|
135
|
+
// mechanism (heartbeat: skip tick; retry-drain: continue loop; per-entry: schedule
|
|
136
|
+
// a retry-drain so the entry is not lost).
|
|
137
|
+
let injectInFlight = false;
|
|
138
|
+
|
|
139
|
+
function tryAcquireInjectLock(): boolean {
|
|
140
|
+
if (injectInFlight) return false;
|
|
141
|
+
injectInFlight = true;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function releaseInjectLock(): void {
|
|
146
|
+
injectInFlight = false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* gh#861 finding 3: a positively-dead codex app-server socket. ENOENT means the
|
|
151
|
+
* socket file is gone (the app-server never created it / unlinked it on exit) —
|
|
152
|
+
* the wake path cannot deliver, so the heartbeat timer should be torn down. Kept
|
|
153
|
+
* narrow (ENOENT only): a transient ECONNREFUSED during a momentary blip must NOT
|
|
154
|
+
* tear the backstop down.
|
|
155
|
+
*/
|
|
156
|
+
function isAppServerDeadError(err: unknown): boolean {
|
|
157
|
+
return (err as { code?: string } | null)?.code === 'ENOENT';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function defaultSleep(ms: number): Promise<void> {
|
|
161
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface CodexWakeDeps {
|
|
165
|
+
getActiveCube?: typeof getActiveCube;
|
|
166
|
+
getCodexWakeTarget?: typeof getCodexWakeTarget;
|
|
167
|
+
// gh#855: self-heal write of the freshly-resolved target (write-only-on-change).
|
|
168
|
+
setCodexWakeTarget?: typeof setCodexWakeTarget;
|
|
169
|
+
createClient?: (
|
|
170
|
+
socketPath: string
|
|
171
|
+
) => Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'loadedThreadIds' | 'close'>;
|
|
172
|
+
// gh#855: injectable env (live app-server socket) + cwd (thread disambiguation).
|
|
173
|
+
env?: NodeJS.ProcessEnv;
|
|
174
|
+
cwd?: () => string;
|
|
175
|
+
// gh#708/#857: injectable for the retry-drain loop (real timers in prod; fake in tests).
|
|
176
|
+
sleep?: (ms: number) => Promise<void>;
|
|
177
|
+
now?: () => number;
|
|
178
|
+
// gh#857: injectable backoff jitter (default Math.random()*500) so co-located
|
|
179
|
+
// sibling drones don't retry in lockstep; deterministic (0) in tests.
|
|
180
|
+
jitter?: () => number;
|
|
181
|
+
// gh#857: injectable hard iteration ceiling for the retry-drain loop (default
|
|
182
|
+
// WAKE_RETRY_MAX_ATTEMPTS); small values let tests prove the loop terminates
|
|
183
|
+
// under a non-advancing clock without running thousands of iterations.
|
|
184
|
+
maxAttempts?: number;
|
|
185
|
+
// client#76: token-free, non-mutating preflight for the periodic backstop.
|
|
186
|
+
// Per-entry and retry-drain paths do not use this: their pending obligation is
|
|
187
|
+
// already established by a concrete delivered/deferred event.
|
|
188
|
+
hasPendingWork?: (active: ActiveCube) => Promise<boolean>;
|
|
189
|
+
// gh#861 finding 2: lease-ownership gate for the heartbeat tick — a lease-LOSING
|
|
190
|
+
// duplicate child must NOT tick/inject (symmetry with the per-entry path, which
|
|
191
|
+
// only fires inside an SSE session that holds the stream lease). Heartbeat-only;
|
|
192
|
+
// when omitted the gate is skipped (direct unit calls / the per-entry + drain
|
|
193
|
+
// paths are already lease-silenced upstream). Production wiring passes a
|
|
194
|
+
// predicate reading the stream loop's current lease ownership.
|
|
195
|
+
isStreamOwner?: () => boolean;
|
|
196
|
+
// gh#861 finding 3: invoked when the heartbeat tick detects the codex app-server
|
|
197
|
+
// socket is positively gone (ENOENT) — the wake path is dead, so the production
|
|
198
|
+
// wiring tears down the heartbeat timer (re-armed when an active cube returns).
|
|
199
|
+
onAppServerSocketDead?: () => void;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* gh#855: FRESH wake-target resolution. Prefer THIS drone's live app-server
|
|
204
|
+
* socket (pinned into the child's env at spawn) and re-resolve the loaded thread
|
|
205
|
+
* NOW (loadedThreadIds is re-runnable) — so a missed/stale launch probe or a
|
|
206
|
+
* thread change can never cause permanent deafness. Self-heals the file cache
|
|
207
|
+
* (write-only-on-change) so other readers (probeCodexBridgeArmed / health-beat)
|
|
208
|
+
* stay current. Falls back to the launch-recorded file when the env socket is
|
|
209
|
+
* absent (un-upgraded launch) — no regression. Returns the resolved target, or
|
|
210
|
+
* null (caller skips this wake; the next one retries). Does NOT keep a
|
|
211
|
+
* connection open — the env path opens a short-lived probe client to re-resolve
|
|
212
|
+
* the thread, then closes it, so the caller can dedup BEFORE opening the wake
|
|
213
|
+
* connection (no reconnect on an already-delivered wake).
|
|
214
|
+
*/
|
|
215
|
+
function makeCodexClient(
|
|
216
|
+
sock: string,
|
|
217
|
+
deps: CodexWakeDeps
|
|
218
|
+
): Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'loadedThreadIds' | 'close'> {
|
|
219
|
+
return deps.createClient ? deps.createClient(sock) : new CodexAppServerClient(sock);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function resolveFreshCodexWakeTarget(
|
|
223
|
+
active: { cubeId: string; droneId: string },
|
|
224
|
+
deps: CodexWakeDeps
|
|
225
|
+
): Promise<{ socketPath: string; threadId: string } | null> {
|
|
226
|
+
const envSocket = codexAppServerSocketFromEnv(deps.env ?? process.env);
|
|
227
|
+
if (envSocket) {
|
|
228
|
+
const probe = makeCodexClient(envSocket, deps);
|
|
229
|
+
await probe.connect();
|
|
230
|
+
try {
|
|
231
|
+
const ids = await probe.loadedThreadIds();
|
|
232
|
+
const summaries: CodexThreadInfo[] = [];
|
|
233
|
+
for (const id of ids) {
|
|
234
|
+
const t = await probe.readThread(id);
|
|
235
|
+
if (t) summaries.push({ id: t.id, cwd: t.cwd, updatedAt: t.updatedAt });
|
|
236
|
+
}
|
|
237
|
+
const threadId = pickFreshThread(summaries, { cwd: (deps.cwd ?? (() => process.cwd()))() });
|
|
238
|
+
if (!threadId) return null; // no loaded thread yet — next wake retries (no permanent fail)
|
|
239
|
+
await maybePersistWakeTarget(active, { socketPath: envSocket, threadId }, deps);
|
|
240
|
+
return { socketPath: envSocket, threadId };
|
|
241
|
+
} finally {
|
|
242
|
+
probe.close();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Fallback: the launch-recorded file (un-upgraded launch / env absent) — no
|
|
247
|
+
// connect needed to resolve, so the caller dedups before opening any socket.
|
|
248
|
+
const target = await (deps.getCodexWakeTarget ?? getCodexWakeTarget)(active.cubeId, active.droneId);
|
|
249
|
+
if (!target) return null;
|
|
250
|
+
return { socketPath: target.socketPath, threadId: target.threadId };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Self-healing cache write — only when the resolved target actually changed. */
|
|
254
|
+
async function maybePersistWakeTarget(
|
|
255
|
+
active: { cubeId: string; droneId: string },
|
|
256
|
+
fresh: { socketPath: string; threadId: string },
|
|
257
|
+
deps: CodexWakeDeps
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
try {
|
|
260
|
+
const get = deps.getCodexWakeTarget ?? getCodexWakeTarget;
|
|
261
|
+
const set = deps.setCodexWakeTarget ?? setCodexWakeTarget;
|
|
262
|
+
const existing = await get(active.cubeId, active.droneId);
|
|
263
|
+
const prev = existing ? { socketPath: existing.socketPath, threadId: existing.threadId } : null;
|
|
264
|
+
if (wakeTargetChanged(prev, fresh)) {
|
|
265
|
+
await set(active.cubeId, active.droneId, fresh);
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// best-effort cache write; never break the wake path
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function wakeCodexViaAppServer(
|
|
273
|
+
reason: string = CODEX_WAKE_PROMPT,
|
|
274
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
275
|
+
deps: CodexWakeDeps = {}
|
|
276
|
+
): void {
|
|
277
|
+
const target = resolveCodexWakeTarget(env);
|
|
278
|
+
if (!target.enabled) return;
|
|
279
|
+
pendingWakeRequests.push({ reason, deps });
|
|
280
|
+
if (wakeInFlight) return;
|
|
281
|
+
|
|
282
|
+
wakeInFlight = true;
|
|
283
|
+
void drainCodexWakeQueue().finally(() => {
|
|
284
|
+
wakeInFlight = false;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function drainCodexWakeQueue(): Promise<void> {
|
|
289
|
+
while (pendingWakeRequests.length > 0) {
|
|
290
|
+
const request = pendingWakeRequests.shift()!;
|
|
291
|
+
await wakeCodexTargeted(request.reason, request.deps);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function wakeCodexTargeted(reason: string, deps: CodexWakeDeps): Promise<void> {
|
|
296
|
+
// gh#861 finding 1: another path (heartbeat/retry-drain) is mid-inject into the
|
|
297
|
+
// same thread — defer to the retry-drain so this entry isn't double-injected nor
|
|
298
|
+
// lost (the drain re-syncs the whole burst via the server read-cursor).
|
|
299
|
+
if (!tryAcquireInjectLock()) {
|
|
300
|
+
scheduleRetryDrain(deps);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const active = await (deps.getActiveCube ?? getActiveCube)();
|
|
305
|
+
if (!active) return;
|
|
306
|
+
// gh#855: resolve FRESH (live env socket + re-resolved thread), falling back
|
|
307
|
+
// to the launch-recorded file only when the env socket is absent.
|
|
308
|
+
const resolved = await resolveFreshCodexWakeTarget(active, deps);
|
|
309
|
+
if (!resolved) return;
|
|
310
|
+
const { socketPath, threadId } = resolved;
|
|
311
|
+
const wakeKey = `${threadId}\0${reason}`;
|
|
312
|
+
if (deliveredWakeKeys.has(wakeKey)) return; // dedup before opening the wake socket
|
|
313
|
+
const client = makeCodexClient(socketPath, deps);
|
|
314
|
+
await client.connect();
|
|
315
|
+
try {
|
|
316
|
+
const thread = await client.readThread(threadId);
|
|
317
|
+
if (thread?.status?.type === 'active') {
|
|
318
|
+
// gh#708/#857: the thread is mid-turn — this per-entry wake can't land
|
|
319
|
+
// now. Schedule the retry-drain (coalesced, retried-until-delivered) so
|
|
320
|
+
// the burst's entries are drained once the thread goes idle; codex has no
|
|
321
|
+
// on-disk tail fallback like Claude's borg-inbox-monitor.
|
|
322
|
+
scheduleRetryDrain(deps);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await client.startTurn(threadId, reason);
|
|
326
|
+
rememberDeliveredWake(wakeKey);
|
|
327
|
+
markDelivered(deps);
|
|
328
|
+
} finally {
|
|
329
|
+
client.close();
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// gh#857: a transient connect/read/startTurn failure must NOT be silently
|
|
333
|
+
// swallowed (the old best-effort drop let a single blip lose an entry).
|
|
334
|
+
// Schedule the retry-drain so the wake is retried-until-delivered; the SSE
|
|
335
|
+
// stream is never broken (this is fire-and-forget).
|
|
336
|
+
scheduleRetryDrain(deps);
|
|
337
|
+
} finally {
|
|
338
|
+
releaseInjectLock();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* gh#708/#857 WI-1: schedule a single coalesced retry-drain. Multiple wakes
|
|
344
|
+
* deferred (mid-turn) or missed (transient) collapse into ONE loop
|
|
345
|
+
* (retryDrainInFlight gate). The loop retries — with exponential backoff
|
|
346
|
+
* (wakeRetryBackoffMs) — until the thread is reachable+idle and the
|
|
347
|
+
* CODEX_CATCHUP_PROMPT drain is delivered (server read-cursor then drains ALL
|
|
348
|
+
* unread, so one drain covers the whole burst). Durable: unlike the old
|
|
349
|
+
* 15-min-give-up catch-up poller, it retries until a generous age cap
|
|
350
|
+
* (wakeRetryExpired); the gh#857 WI-2 heartbeat is the backstop beyond that.
|
|
351
|
+
* Never throws into the SSE path (fire-and-forget).
|
|
352
|
+
*/
|
|
353
|
+
function scheduleRetryDrain(deps: CodexWakeDeps): void {
|
|
354
|
+
if (retryDrainInFlight) return; // coalesce: one loop covers all deferred/missed wakes
|
|
355
|
+
retryDrainInFlight = true;
|
|
356
|
+
void runRetryDrainLoop(deps).finally(() => {
|
|
357
|
+
retryDrainInFlight = false;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function runRetryDrainLoop(deps: CodexWakeDeps): Promise<void> {
|
|
362
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
363
|
+
const now = deps.now ?? Date.now;
|
|
364
|
+
const jitter = deps.jitter ?? (() => Math.random() * 500);
|
|
365
|
+
const maxAttempts = deps.maxAttempts ?? WAKE_RETRY_MAX_ATTEMPTS;
|
|
366
|
+
const startedAt = now();
|
|
367
|
+
let attempts = 0;
|
|
368
|
+
// Terminate on the time-based age cap OR a hard iteration ceiling (defensive
|
|
369
|
+
// belt: a non-advancing clock would make the time-only guard hot-spin).
|
|
370
|
+
while (!wakeRetryExpired(startedAt, now()) && attempts < maxAttempts) {
|
|
371
|
+
await sleep(wakeRetryBackoffMs(attempts, jitter()));
|
|
372
|
+
attempts++;
|
|
373
|
+
// gh#861 finding 1: another path (heartbeat/per-entry wake) holds the inject
|
|
374
|
+
// lock — back off and retry rather than double-inject into the same thread.
|
|
375
|
+
if (!tryAcquireInjectLock()) continue;
|
|
376
|
+
try {
|
|
377
|
+
const active = await (deps.getActiveCube ?? getActiveCube)();
|
|
378
|
+
if (!active) continue; // no active cube yet → keep retrying (until age cap)
|
|
379
|
+
// gh#855: same FRESH resolution as the per-entry wake, so a stale launch
|
|
380
|
+
// probe can't defeat the retry-drain either.
|
|
381
|
+
const resolved = await resolveFreshCodexWakeTarget(active, deps);
|
|
382
|
+
if (!resolved) continue; // thread not loaded yet → retry (age-capped)
|
|
383
|
+
const { socketPath, threadId } = resolved;
|
|
384
|
+
const client = makeCodexClient(socketPath, deps);
|
|
385
|
+
await client.connect();
|
|
386
|
+
try {
|
|
387
|
+
const thread = await client.readThread(threadId);
|
|
388
|
+
if (thread?.status?.type === 'active') {
|
|
389
|
+
continue; // re-defer: still mid-turn (backoff before next poll)
|
|
390
|
+
}
|
|
391
|
+
await client.startTurn(threadId, CODEX_CATCHUP_PROMPT);
|
|
392
|
+
markDelivered(deps);
|
|
393
|
+
return; // drain delivered → server read-cursor drains all unread → done
|
|
394
|
+
} finally {
|
|
395
|
+
client.close();
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
// transient socket/read error must not abort the loop — keep retrying with
|
|
399
|
+
// backoff until reachable+idle or the age cap; never throws into SSE.
|
|
400
|
+
} finally {
|
|
401
|
+
releaseInjectLock();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// aged out: the gh#857 WI-2 periodic heartbeat is the ultimate backstop.
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* gh#857 WI-2: codex /loop-equivalent heartbeat cadence. Codex retains this
|
|
409
|
+
* independent 20-minute drain because it has no Claude-style per-entry inbox
|
|
410
|
+
* Monitor. Claude recovery is adaptive: 3h ±30m while the Monitor is healthy
|
|
411
|
+
* or indeterminate, and 15m ±3m only while it is explicitly broken.
|
|
412
|
+
*/
|
|
413
|
+
export const CODEX_HEARTBEAT_CADENCE_MS = 20 * 60_000;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* gh#857/client#76: one tick of the codex catch-up backstop. The cadence only
|
|
417
|
+
* initiates a token-free unread-state preflight; a DRAIN turn is injected when
|
|
418
|
+
* that authoritative scan finds real work that a per-entry wake missed. Recent
|
|
419
|
+
* delivery still suppresses redundant preflights. Unlike the per-entry path it
|
|
420
|
+
* does not consult deliveredWakeKeys because the unread cursor is authoritative.
|
|
421
|
+
* Best-effort: a failed preflight, mid-turn thread, transient error, or unresolved
|
|
422
|
+
* target skips this tick and lets the next cadence retry. Never throws.
|
|
423
|
+
*/
|
|
424
|
+
export async function fireCodexHeartbeatTick(
|
|
425
|
+
deps: CodexWakeDeps = {},
|
|
426
|
+
cadenceMs: number = CODEX_HEARTBEAT_CADENCE_MS
|
|
427
|
+
): Promise<void> {
|
|
428
|
+
if (heartbeatInFlight) return; // a prior tick's IO is still running → no overlap
|
|
429
|
+
const now = (deps.now ?? Date.now)();
|
|
430
|
+
if (!shouldFireHeartbeat(lastDeliveredAt, now, cadenceMs)) return; // a wake landed recently
|
|
431
|
+
// gh#861 finding 2: a lease-LOSING duplicate child must not tick/inject — mirror
|
|
432
|
+
// the per-entry path, which only fires inside an SSE session holding the stream
|
|
433
|
+
// lease. When the gate is provided and we don't own the lease, skip this tick.
|
|
434
|
+
if (deps.isStreamOwner && !deps.isStreamOwner()) return;
|
|
435
|
+
// gh#861 finding 1: another path is mid-inject into the thread → skip this tick
|
|
436
|
+
// (next tick retries) rather than collide a DRAIN prompt with an in-flight wake.
|
|
437
|
+
if (!tryAcquireInjectLock()) return;
|
|
438
|
+
// Set BEFORE the first await — the check+gate+set above are synchronous, so a
|
|
439
|
+
// concurrent tick can't interleave before the flag is set (single-threaded).
|
|
440
|
+
heartbeatInFlight = true;
|
|
441
|
+
try {
|
|
442
|
+
const active = await (deps.getActiveCube ?? getActiveCube)();
|
|
443
|
+
if (!active) return;
|
|
444
|
+
// Elapsed time alone must never cross the model boundary. Query the
|
|
445
|
+
// authoritative unread state without advancing its cursor; only then touch
|
|
446
|
+
// the app-server socket or resolve a thread.
|
|
447
|
+
const hasPendingWork = deps.hasPendingWork ?? hasPendingWakeActivity;
|
|
448
|
+
if (!(await hasPendingWork(active))) return;
|
|
449
|
+
const resolved = await resolveFreshCodexWakeTarget(active, deps);
|
|
450
|
+
if (!resolved) return; // thread not loaded yet → next tick retries
|
|
451
|
+
const client = makeCodexClient(resolved.socketPath, deps);
|
|
452
|
+
await client.connect();
|
|
453
|
+
try {
|
|
454
|
+
const thread = await client.readThread(resolved.threadId);
|
|
455
|
+
if (thread?.status?.type === 'active') return; // mid-turn → skip; next tick retries
|
|
456
|
+
await client.startTurn(resolved.threadId, CODEX_CATCHUP_PROMPT);
|
|
457
|
+
// markDelivered updates local heartbeat-gating state only.
|
|
458
|
+
markDelivered(deps);
|
|
459
|
+
} finally {
|
|
460
|
+
client.close();
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
// gh#861 finding 3: a positively-dead app-server socket (ENOENT) → the wake
|
|
464
|
+
// path is gone; signal teardown so the timer stops ticking against a dead
|
|
465
|
+
// socket (re-armed when an active cube returns). Other (transient) errors are
|
|
466
|
+
// best-effort skips — never break the SSE stream; next tick retries.
|
|
467
|
+
if (isAppServerDeadError(err)) deps.onAppServerSocketDead?.();
|
|
468
|
+
} finally {
|
|
469
|
+
heartbeatInFlight = false;
|
|
470
|
+
releaseInjectLock();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* gh#857 WI-2: start the codex /loop-equivalent heartbeat — a setInterval firing
|
|
476
|
+
* fireCodexHeartbeatTick every cadence. CODEX-ONLY: claude wakes via the tail-F
|
|
477
|
+
* inbox Monitor + /loop ScheduleWakeup and has NO app-server socket to inject
|
|
478
|
+
* into, so the heartbeat is intrinsically a codex mechanism. The gate reads
|
|
479
|
+
* agentKind and remote-wake capability LOCALLY from this child's own env,
|
|
480
|
+
* never a mutable/server-recorded field, so a mislabel can't silently defeat
|
|
481
|
+
* the backstop (gh#633 lesson). Agent CLI identity and remote transport are
|
|
482
|
+
* separate: a Codex CLI without a live remote transport has no app-server
|
|
483
|
+
* heartbeat to run. The timer is unref'd so it never keeps the process alive.
|
|
484
|
+
* Returns the timer, or null when this is not a remotely-wakeable Codex
|
|
485
|
+
* session. Injectable for tests.
|
|
486
|
+
*/
|
|
487
|
+
export function startCodexHeartbeat(
|
|
488
|
+
opts: {
|
|
489
|
+
agentKind?: 'claude' | 'codex' | 'opencode';
|
|
490
|
+
remoteWakeEnabled?: boolean;
|
|
491
|
+
intervalMs?: number;
|
|
492
|
+
tick?: () => void;
|
|
493
|
+
} = {}
|
|
494
|
+
): ReturnType<typeof setInterval> | null {
|
|
495
|
+
const agentKind = opts.agentKind ?? resolveSessionAgentKind();
|
|
496
|
+
const remoteWakeEnabled = opts.remoteWakeEnabled ?? isCodexRemoteWakeEnabled();
|
|
497
|
+
if (agentKind !== 'codex' || !remoteWakeEnabled) return null;
|
|
498
|
+
const intervalMs = opts.intervalMs ?? CODEX_HEARTBEAT_CADENCE_MS;
|
|
499
|
+
const tick = opts.tick ?? (() => void fireCodexHeartbeatTick());
|
|
500
|
+
const timer = setInterval(tick, intervalMs);
|
|
501
|
+
timer.unref?.();
|
|
502
|
+
return timer;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function resetCodexWakeForTests(): void {
|
|
506
|
+
wakeInFlight = false;
|
|
507
|
+
pendingWakeRequests.length = 0;
|
|
508
|
+
deliveredWakeKeys.clear();
|
|
509
|
+
deliveredWakeKeyOrder.length = 0;
|
|
510
|
+
retryDrainInFlight = false;
|
|
511
|
+
lastDeliveredAt = null;
|
|
512
|
+
heartbeatInFlight = false;
|
|
513
|
+
injectInFlight = false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function rememberDeliveredWake(key: string): void {
|
|
517
|
+
if (deliveredWakeKeys.has(key)) return;
|
|
518
|
+
deliveredWakeKeys.add(key);
|
|
519
|
+
deliveredWakeKeyOrder.push(key);
|
|
520
|
+
while (deliveredWakeKeyOrder.length > DELIVERED_WAKE_KEY_CAP) {
|
|
521
|
+
const oldKey = deliveredWakeKeyOrder.shift();
|
|
522
|
+
if (oldKey) deliveredWakeKeys.delete(oldKey);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { BorgCli } from './cubes.js';
|
|
2
|
+
import { wakePathArming, type AgentKind } from './regen-format.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The claude kickoff prompt's wake-path section (gh#929) — the SAME shared
|
|
6
|
+
* `wakePathArming` the SessionStart hook + /clear orientation use (one place,
|
|
7
|
+
* not three), plus a one-line NEVER-TaskStop safety reminder preserved from
|
|
8
|
+
* the pre-gh#929 monitorClause. Built by both launch call sites
|
|
9
|
+
* (claude.ts + assimilate-cmd.ts) and passed to `buildAgentKickoffPrompt` as
|
|
10
|
+
* `monitorClause`. Codex wakes via the app-server (no tail-Monitor / `/loop`
|
|
11
|
+
* to arm) → empty; no active cube (no inboxPath) → empty.
|
|
12
|
+
*/
|
|
13
|
+
export function buildKickoffWakePathClause(
|
|
14
|
+
agentKind: AgentKind,
|
|
15
|
+
inboxPath: string | null,
|
|
16
|
+
monitorStateRoot?: string | null
|
|
17
|
+
): string {
|
|
18
|
+
if (agentKind === 'claude' && inboxPath) {
|
|
19
|
+
return (
|
|
20
|
+
wakePathArming('claude', inboxPath, monitorStateRoot) +
|
|
21
|
+
'\nKeep this Monitor armed for the whole session — NEVER TaskStop it (the generic ' +
|
|
22
|
+
'/loop "TaskStop any Monitor you armed" step does NOT apply to the cube inbox Monitor; ' +
|
|
23
|
+
'it targets throwaway loop-scratch watches only). The sole exception is a confirmed ' +
|
|
24
|
+
'terminal eviction (410 DRONE_EVICTED). '
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CodexWakeTargetDeps {
|
|
31
|
+
setCodexWakeTarget: (
|
|
32
|
+
cubeId: string,
|
|
33
|
+
droneId: string,
|
|
34
|
+
target: { threadId: string; socketPath: string }
|
|
35
|
+
) => Promise<void>;
|
|
36
|
+
findLoadedCodexThread: (options: {
|
|
37
|
+
socketPath: string;
|
|
38
|
+
cwd: string;
|
|
39
|
+
previewIncludes: string;
|
|
40
|
+
updatedAfter: number;
|
|
41
|
+
}) => Promise<string | null>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildAgentKickoffPrompt(options: {
|
|
45
|
+
cli: BorgCli;
|
|
46
|
+
codexWakeNonce: string | null;
|
|
47
|
+
monitorClause: string;
|
|
48
|
+
codexWakePathClause?: string;
|
|
49
|
+
}): string {
|
|
50
|
+
// gh#929: compacted to the load-bearing launch essentials (lean/explicit/
|
|
51
|
+
// imperative, #914 treatment). STRIPPED: the read-log-triage paragraph (the
|
|
52
|
+
// playbook owns it post-#914) + the role-specific anti-passive-Standing
|
|
53
|
+
// clause (Coordinator/Queen-only; belongs in role-text, not injected for
|
|
54
|
+
// ALL). KEPT: core call + MCP-disconnect recovery + the wake-path arming
|
|
55
|
+
// (claude via the shared monitorClause = buildKickoffWakePathClause; codex
|
|
56
|
+
// via codexWakePathClause) + the claude/codex/opencode cli-branching.
|
|
57
|
+
const codexNonceClause = options.codexWakeNonce
|
|
58
|
+
? `Wake target nonce: ${options.codexWakeNonce}. `
|
|
59
|
+
: '';
|
|
60
|
+
const codexWakePathClause =
|
|
61
|
+
options.codexWakePathClause ??
|
|
62
|
+
`Codex Borg wakeups use remote-control when available; if no wake arrives, run borg_regen manually when returning to the session.`;
|
|
63
|
+
const opencodeWakePathClause = `OpenCode wakes: the inbox Monitor is not available yet for OpenCode; check activity by calling borg_read-log periodically.`;
|
|
64
|
+
const loopOrEmpty = options.cli === 'claude' ? '/loop ' : '';
|
|
65
|
+
const wakeClause = options.cli === 'claude'
|
|
66
|
+
? options.monitorClause
|
|
67
|
+
: options.cli === 'codex'
|
|
68
|
+
? codexWakePathClause
|
|
69
|
+
: opencodeWakePathClause;
|
|
70
|
+
return (
|
|
71
|
+
`${loopOrEmpty}Call borg_regen and follow the playbook in its response. ` +
|
|
72
|
+
codexNonceClause +
|
|
73
|
+
`Note: at session start the borg MCP server is still spinning up in ` +
|
|
74
|
+
`parallel — if a system reminder claims "MCP server disconnected" or ` +
|
|
75
|
+
`the borg tools are not yet registered, do NOT bail. Recover via ` +
|
|
76
|
+
`\`ToolSearch({query: "select:mcp__borg__borg_regen,mcp__borg__borg_log,Monitor", max_results: 3})\` ` +
|
|
77
|
+
`to load the bootstrap tools in one call, then call borg_regen. ` +
|
|
78
|
+
`The server typically becomes available within a few seconds. ` +
|
|
79
|
+
wakeClause
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function socketPathFromRemoteArgs(args: string[]): string | null {
|
|
84
|
+
const index = args.indexOf('--remote');
|
|
85
|
+
if (index < 0) return null;
|
|
86
|
+
const value = args[index + 1];
|
|
87
|
+
if (!value?.startsWith('unix://')) return null;
|
|
88
|
+
return value.slice('unix://'.length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function threadIdFromPassthroughArgs(args: string[]): string | null {
|
|
92
|
+
if (args[0] === 'resume' && args[1] && !args[1].startsWith('-')) return args[1];
|
|
93
|
+
const resumeIndex = args.indexOf('--resume');
|
|
94
|
+
if (resumeIndex >= 0 && args[resumeIndex + 1]) return args[resumeIndex + 1];
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function recordCodexWakeTarget(options: {
|
|
99
|
+
deps: CodexWakeTargetDeps;
|
|
100
|
+
cubeId: string;
|
|
101
|
+
droneId: string;
|
|
102
|
+
socketPath: string;
|
|
103
|
+
cwd: string;
|
|
104
|
+
previewNeedle: string;
|
|
105
|
+
launchedAtSeconds: number;
|
|
106
|
+
passthroughArgs?: string[];
|
|
107
|
+
}): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
const explicitThreadId = options.passthroughArgs
|
|
110
|
+
? threadIdFromPassthroughArgs(options.passthroughArgs)
|
|
111
|
+
: null;
|
|
112
|
+
if (explicitThreadId) {
|
|
113
|
+
await options.deps.setCodexWakeTarget(options.cubeId, options.droneId, {
|
|
114
|
+
threadId: explicitThreadId,
|
|
115
|
+
socketPath: options.socketPath,
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const deadline = Date.now() + 15_000;
|
|
121
|
+
while (Date.now() < deadline) {
|
|
122
|
+
const threadId = await options.deps.findLoadedCodexThread({
|
|
123
|
+
socketPath: options.socketPath,
|
|
124
|
+
cwd: options.cwd,
|
|
125
|
+
previewIncludes: options.previewNeedle,
|
|
126
|
+
updatedAfter: options.launchedAtSeconds - 5,
|
|
127
|
+
});
|
|
128
|
+
if (threadId) {
|
|
129
|
+
await options.deps.setCodexWakeTarget(options.cubeId, options.droneId, {
|
|
130
|
+
threadId,
|
|
131
|
+
socketPath: options.socketPath,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Best-effort mapping: launch still succeeds and manual regen remains available.
|
|
139
|
+
}
|
|
140
|
+
}
|